From 84b32655e4824508d377d041a940f35d9fb48ae1 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:15:17 +0100 Subject: [PATCH 01/37] fix(terminal): clamp resize to minimum 1 col/row to prevent panic Prevents alacritty from panicking when receiving zero-dimension resize events, which can occur during layout transitions on mobile. Co-Authored-By: Claude Opus 4.6 --- crates/okena-terminal/src/terminal/resize.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/okena-terminal/src/terminal/resize.rs b/crates/okena-terminal/src/terminal/resize.rs index 6205c59e..69628e62 100644 --- a/crates/okena-terminal/src/terminal/resize.rs +++ b/crates/okena-terminal/src/terminal/resize.rs @@ -20,6 +20,11 @@ impl Terminal { pub fn resize(&self, new_size: TerminalSize) { let debounce_ms = self.transport.resize_debounce_ms(); + // Clamp to at least 1 col/row - alacritty_terminal panics on zero dimensions + let cols = new_size.cols.max(1); + let rows = new_size.rows.max(1); + let new_size = TerminalSize { cols, rows, ..new_size }; + // Always update local size immediately (optimistic UI) { let mut rs = self.resize_state.lock(); From 395f016a257e840c510479e16b33d26b8154afca Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:15:28 +0100 Subject: [PATCH 02/37] feat(core): add Backspace and Delete special keys Co-Authored-By: Claude Opus 4.6 --- crates/okena-core/src/keys.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/okena-core/src/keys.rs b/crates/okena-core/src/keys.rs index a9023841..d4697d1f 100644 --- a/crates/okena-core/src/keys.rs +++ b/crates/okena-core/src/keys.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; pub enum SpecialKey { Enter, Escape, + Backspace, + Delete, CtrlC, CtrlD, CtrlZ, @@ -27,6 +29,8 @@ impl SpecialKey { match self { SpecialKey::Enter => b"\r", SpecialKey::Escape => b"\x1b", + SpecialKey::Backspace => b"\x7f", + SpecialKey::Delete => b"\x1b[3~", SpecialKey::CtrlC => b"\x03", SpecialKey::CtrlD => b"\x04", SpecialKey::CtrlZ => b"\x1a", @@ -54,6 +58,8 @@ mod tests { let keys = vec![ SpecialKey::Enter, SpecialKey::Escape, + SpecialKey::Backspace, + SpecialKey::Delete, SpecialKey::CtrlC, SpecialKey::CtrlD, SpecialKey::CtrlZ, @@ -80,6 +86,8 @@ mod tests { fn special_key_to_bytes() { assert_eq!(SpecialKey::Enter.to_bytes(), b"\r"); assert_eq!(SpecialKey::Escape.to_bytes(), b"\x1b"); + assert_eq!(SpecialKey::Backspace.to_bytes(), b"\x7f"); + assert_eq!(SpecialKey::Delete.to_bytes(), b"\x1b[3~"); assert_eq!(SpecialKey::CtrlC.to_bytes(), b"\x03"); assert_eq!(SpecialKey::CtrlD.to_bytes(), b"\x04"); assert_eq!(SpecialKey::CtrlZ.to_bytes(), b"\x1a"); From 8f1ad135c6da8fdc69805a7bc41e0f0b82cd4c47 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:15:36 +0100 Subject: [PATCH 03/37] feat(core): add folders, project ordering, and folder colors to API Add ApiFolder struct, folders/project_order fields to StateResponse, folder_color to ApiProject, and cols/rows to ApiLayoutNode::Terminal for terminal size propagation. Co-Authored-By: Claude Opus 4.6 --- crates/okena-core/src/keys.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/okena-core/src/keys.rs b/crates/okena-core/src/keys.rs index d4697d1f..19f3d4fd 100644 --- a/crates/okena-core/src/keys.rs +++ b/crates/okena-core/src/keys.rs @@ -19,8 +19,6 @@ pub enum SpecialKey { End, PageUp, PageDown, - Backspace, - Delete, } impl SpecialKey { @@ -43,8 +41,6 @@ impl SpecialKey { SpecialKey::End => b"\x1b[F", SpecialKey::PageUp => b"\x1b[5~", SpecialKey::PageDown => b"\x1b[6~", - SpecialKey::Backspace => b"\x7f", - SpecialKey::Delete => b"\x1b[3~", } } } @@ -72,8 +68,6 @@ mod tests { SpecialKey::End, SpecialKey::PageUp, SpecialKey::PageDown, - SpecialKey::Backspace, - SpecialKey::Delete, ]; for key in keys { let json = serde_json::to_string(&key).unwrap(); @@ -100,7 +94,5 @@ mod tests { assert_eq!(SpecialKey::End.to_bytes(), b"\x1b[F"); assert_eq!(SpecialKey::PageUp.to_bytes(), b"\x1b[5~"); assert_eq!(SpecialKey::PageDown.to_bytes(), b"\x1b[6~"); - assert_eq!(SpecialKey::Backspace.to_bytes(), b"\x7f"); - assert_eq!(SpecialKey::Delete.to_bytes(), b"\x1b[3~"); } } From 0ec087ad2e1e907d55c33fbe33983c009c33bc7c Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:15:44 +0100 Subject: [PATCH 04/37] feat(core): add terminal size propagation in layout tree Add collect_terminal_sizes() to walk the layout tree and build a size map. Update ConnectionHandler::create_terminal to accept cols/rows. Co-Authored-By: Claude Opus 4.6 --- crates/okena-core/src/api.rs | 16 ++++ crates/okena-core/src/client/connection.rs | 17 ++++- crates/okena-core/src/client/mod.rs | 2 +- crates/okena-core/src/client/state.rs | 77 ++++++++++++++++++++ crates/okena-layout/src/lib.rs | 4 + crates/okena-remote-client/src/connection.rs | 9 ++- crates/okena-workspace/src/state.rs | 1 + src/remote/types.rs | 12 +++ 8 files changed, 134 insertions(+), 4 deletions(-) diff --git a/crates/okena-core/src/api.rs b/crates/okena-core/src/api.rs index 90eebfc2..21f21776 100644 --- a/crates/okena-core/src/api.rs +++ b/crates/okena-core/src/api.rs @@ -233,6 +233,10 @@ pub enum ApiLayoutNode { terminal_id: Option, minimized: bool, detached: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + cols: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + rows: Option, }, Split { direction: SplitDirection, @@ -602,6 +606,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -609,6 +615,8 @@ mod tests { terminal_id: Some("t2".into()), minimized: true, detached: true, + cols: None, + rows: None, }], }, ], @@ -946,6 +954,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -954,16 +964,22 @@ mod tests { terminal_id: Some("t2".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Terminal { terminal_id: None, minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Terminal { terminal_id: Some("t3".into()), minimized: false, detached: true, + cols: None, + rows: None, }, ], }, diff --git a/crates/okena-core/src/client/connection.rs b/crates/okena-core/src/client/connection.rs index 577da86c..aa0b318f 100644 --- a/crates/okena-core/src/client/connection.rs +++ b/crates/okena-core/src/client/connection.rs @@ -1,7 +1,7 @@ use crate::api::StateResponse; use crate::client::config::RemoteConnectionConfig; use crate::client::id::make_prefixed_id; -use crate::client::state::{collect_all_terminal_ids, collect_state_terminal_ids, diff_states}; +use crate::client::state::{collect_all_terminal_ids, collect_state_terminal_ids, collect_terminal_sizes, diff_states}; use crate::client::types::{ ConnectionEvent, ConnectionStatus, SessionError, WsClientMessage, TOKEN_REFRESH_AGE_SECS, }; @@ -17,12 +17,15 @@ use tokio_tungstenite::tungstenite; pub trait ConnectionHandler: Send + Sync + 'static { /// Terminal discovered — create platform terminal object. /// `ws_sender` is for constructing a transport that sends WS commands. + /// `cols`/`rows` are the server's current terminal dimensions (0 if unknown). fn create_terminal( &self, connection_id: &str, terminal_id: &str, prefixed_id: &str, ws_sender: async_channel::Sender, + cols: u16, + rows: u16, ); /// Binary PTY output arrived — route to the terminal's emulator. fn on_terminal_output(&self, prefixed_id: &str, data: &[u8]); @@ -673,9 +676,11 @@ impl RemoteClient { handler.remove_terminals_except(&config.id, ¤t_ids); let terminal_ids = collect_state_terminal_ids(&state); + let size_map = collect_terminal_sizes(&state); for tid in &terminal_ids { let prefixed = make_prefixed_id(&config.id, tid); - handler.create_terminal(&config.id, tid, &prefixed, ws_tx.clone()); + let (cols, rows) = size_map.get(tid).copied().unwrap_or((0, 0)); + handler.create_terminal(&config.id, tid, &prefixed, ws_tx.clone(), cols, rows); } // Notify state received @@ -892,16 +897,24 @@ impl RemoteClient { { let diff = diff_states(&cached_state, &new_state); + let new_size_map = + collect_terminal_sizes(&new_state); // Add new terminals via handler for tid in &diff.added_terminals { let prefixed = make_prefixed_id(&config_id, tid); + let (cols, rows) = new_size_map + .get(tid) + .copied() + .unwrap_or((0, 0)); handler_clone.create_terminal( &config_id, tid, &prefixed, ws_tx_clone.clone(), + cols, + rows, ); } diff --git a/crates/okena-core/src/client/mod.rs b/crates/okena-core/src/client/mod.rs index 6ab38d5b..24632f7d 100644 --- a/crates/okena-core/src/client/mod.rs +++ b/crates/okena-core/src/client/mod.rs @@ -8,5 +8,5 @@ pub mod types; pub use config::RemoteConnectionConfig; pub use connection::{ConnectionHandler, RemoteClient}; pub use id::{is_remote_terminal, make_prefixed_id, strip_prefix}; -pub use state::{collect_all_terminal_ids, collect_state_terminal_ids, diff_states, StateDiff}; +pub use state::{collect_all_terminal_ids, collect_state_terminal_ids, collect_terminal_sizes, diff_states, StateDiff}; pub use types::{ConnectionEvent, ConnectionStatus, WsClientMessage, TOKEN_REFRESH_AGE_SECS}; diff --git a/crates/okena-core/src/client/state.rs b/crates/okena-core/src/client/state.rs index 6df1a70b..a84b67c0 100644 --- a/crates/okena-core/src/client/state.rs +++ b/crates/okena-core/src/client/state.rs @@ -112,10 +112,48 @@ fn collect_layout_ids_vec(node: &ApiLayoutNode, ids: &mut Vec) { } } +/// Collect terminal sizes from all projects in a StateResponse. +/// +/// Returns a map of terminal_id → (cols, rows) for terminals that have +/// size information in the layout tree. +pub fn collect_terminal_sizes(state: &StateResponse) -> std::collections::HashMap { + let mut sizes = std::collections::HashMap::new(); + for project in &state.projects { + if let Some(ref layout) = project.layout { + collect_layout_terminal_sizes(layout, &mut sizes); + } + } + sizes +} + +fn collect_layout_terminal_sizes( + node: &ApiLayoutNode, + sizes: &mut std::collections::HashMap, +) { + match node { + ApiLayoutNode::Terminal { + terminal_id, + cols, + rows, + .. + } => { + if let (Some(id), Some(c), Some(r)) = (terminal_id, cols, rows) { + sizes.insert(id.clone(), (*c, *r)); + } + } + ApiLayoutNode::Split { children, .. } | ApiLayoutNode::Tabs { children, .. } => { + for child in children { + collect_layout_terminal_sizes(child, sizes); + } + } + } +} + #[cfg(test)] mod tests { use super::*; use crate::api::{ApiLayoutNode, ApiProject, StateResponse}; + use crate::theme::FolderColor; use crate::types::SplitDirection; fn make_state(projects: Vec) -> StateResponse { @@ -137,6 +175,8 @@ mod tests { terminal_id: Some(terminal_ids[0].to_string()), minimized: false, detached: false, + cols: None, + rows: None, }) } else { Some(ApiLayoutNode::Split { @@ -148,6 +188,8 @@ mod tests { terminal_id: Some(tid.to_string()), minimized: false, detached: false, + cols: None, + rows: None, }) .collect(), }) @@ -202,4 +244,39 @@ mod tests { assert!(diff.removed_terminals.is_empty()); assert!(diff.changed_projects.is_empty()); } + + #[test] + fn collect_terminal_sizes_extracts_from_layout() { + let state = make_state(vec![ApiProject { + id: "p1".into(), + name: "p1".into(), + path: "/tmp".into(), + is_visible: true, + layout: Some(ApiLayoutNode::Split { + direction: SplitDirection::Horizontal, + sizes: vec![50.0, 50.0], + children: vec![ + ApiLayoutNode::Terminal { + terminal_id: Some("t1".into()), + minimized: false, + detached: false, + cols: Some(120), + rows: Some(40), + }, + ApiLayoutNode::Terminal { + terminal_id: Some("t2".into()), + minimized: false, + detached: false, + cols: None, + rows: None, + }, + ], + }), + terminal_names: Default::default(), + folder_color: FolderColor::default(), + }]); + let sizes = collect_terminal_sizes(&state); + assert_eq!(sizes.get("t1"), Some(&(120, 40))); + assert_eq!(sizes.get("t2"), None); + } } diff --git a/crates/okena-layout/src/lib.rs b/crates/okena-layout/src/lib.rs index a25060e2..a1659613 100644 --- a/crates/okena-layout/src/lib.rs +++ b/crates/okena-layout/src/lib.rs @@ -739,6 +739,7 @@ impl LayoutNode { terminal_id, minimized, detached, + .. } => LayoutNode::Terminal { terminal_id: terminal_id.clone(), minimized: *minimized, @@ -773,6 +774,7 @@ impl LayoutNode { terminal_id, minimized, detached, + .. } => LayoutNode::Terminal { terminal_id: terminal_id.as_ref().map(|id| format!("{}:{}", prefix, id)), minimized: *minimized, @@ -817,6 +819,8 @@ impl LayoutNode { terminal_id: terminal_id.clone(), minimized: *minimized, detached: *detached, + cols: None, + rows: None, }, LayoutNode::Split { direction, diff --git a/crates/okena-remote-client/src/connection.rs b/crates/okena-remote-client/src/connection.rs index fbae8475..42d0c98c 100644 --- a/crates/okena-remote-client/src/connection.rs +++ b/crates/okena-remote-client/src/connection.rs @@ -37,6 +37,8 @@ impl ConnectionHandler for DesktopConnectionHandler { _terminal_id: &str, prefixed_id: &str, ws_sender: async_channel::Sender, + cols: u16, + rows: u16, ) { let mut terminals = self.terminals.lock(); // Skip if terminal already exists — on reconnect the server re-sends @@ -50,9 +52,14 @@ impl ConnectionHandler for DesktopConnectionHandler { ws_tx: ws_sender, connection_id: connection_id.to_string(), }); + let size = if cols > 0 && rows > 0 { + TerminalSize { cols, rows, ..TerminalSize::default() } + } else { + TerminalSize::default() + }; let terminal = Arc::new(Terminal::new( prefixed_id.to_string(), - TerminalSize::default(), + size, transport, String::new(), )); diff --git a/crates/okena-workspace/src/state.rs b/crates/okena-workspace/src/state.rs index 660f1781..e3134f04 100644 --- a/crates/okena-workspace/src/state.rs +++ b/crates/okena-workspace/src/state.rs @@ -871,6 +871,7 @@ impl Workspace { } } + #[cfg(test)] mod workspace_tests { use crate::state::{ diff --git a/src/remote/types.rs b/src/remote/types.rs index e1976b0f..df0218e2 100644 --- a/src/remote/types.rs +++ b/src/remote/types.rs @@ -24,6 +24,8 @@ mod tests { terminal_id: Some("abc-123".into()), minimized: false, detached: false, + cols: None, + rows: None, }; let node = LayoutNode::from_api_prefixed(&api, "remote:conn1"); match node { @@ -40,6 +42,8 @@ mod tests { terminal_id: None, minimized: true, detached: false, + cols: None, + rows: None, }; let node = LayoutNode::from_api_prefixed(&api, "remote:x"); match node { @@ -65,6 +69,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -73,11 +79,15 @@ mod tests { terminal_id: Some("t2".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Terminal { terminal_id: Some("t3".into()), minimized: false, detached: true, + cols: None, + rows: None, }, ], }, @@ -94,6 +104,8 @@ mod tests { terminal_id: Some("raw-id".into()), minimized: false, detached: false, + cols: None, + rows: None, }; let node = LayoutNode::from_api(&api); match node { From 9be6bea2b8405a0578a776b6cb3f2c5b3202c506 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:15:51 +0100 Subject: [PATCH 05/37] feat(desktop): include folders, project order, and terminal sizes in state response Build terminal size map from registry and use to_api_with_sizes() to populate cols/rows in layout nodes. Include folders and project_order in state responses for mobile/web clients. Co-Authored-By: Claude Opus 4.6 --- crates/okena-layout/src/lib.rs | 37 ++++++++++++++++++++++++---------- src/app/remote_commands.rs | 11 +++++++++- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/crates/okena-layout/src/lib.rs b/crates/okena-layout/src/lib.rs index a1659613..b27db1ed 100644 --- a/crates/okena-layout/src/lib.rs +++ b/crates/okena-layout/src/lib.rs @@ -809,33 +809,48 @@ impl LayoutNode { /// Convert to API layout node. pub fn to_api(&self) -> okena_core::api::ApiLayoutNode { + self.to_api_with_sizes(&std::collections::HashMap::new()) + } + + /// Convert to API, populating terminal `cols`/`rows` from the given size map. + pub fn to_api_with_sizes( + &self, + sizes: &std::collections::HashMap, + ) -> okena_core::api::ApiLayoutNode { match self { LayoutNode::Terminal { terminal_id, minimized, detached, .. - } => okena_core::api::ApiLayoutNode::Terminal { - terminal_id: terminal_id.clone(), - minimized: *minimized, - detached: *detached, - cols: None, - rows: None, - }, + } => { + let (cols, rows) = terminal_id + .as_ref() + .and_then(|id| sizes.get(id)) + .map(|&(c, r)| (Some(c), Some(r))) + .unwrap_or((None, None)); + okena_core::api::ApiLayoutNode::Terminal { + terminal_id: terminal_id.clone(), + minimized: *minimized, + detached: *detached, + cols, + rows, + } + } LayoutNode::Split { direction, - sizes, + sizes: split_sizes, children, } => okena_core::api::ApiLayoutNode::Split { direction: *direction, - sizes: sizes.clone(), - children: children.iter().map(LayoutNode::to_api).collect(), + sizes: split_sizes.clone(), + children: children.iter().map(|c| c.to_api_with_sizes(sizes)).collect(), }, LayoutNode::Tabs { children, active_tab, } => okena_core::api::ApiLayoutNode::Tabs { - children: children.iter().map(LayoutNode::to_api).collect(), + children: children.iter().map(|c| c.to_api_with_sizes(sizes)).collect(), active_tab: *active_tab, }, } diff --git a/src/app/remote_commands.rs b/src/app/remote_commands.rs index 9f8b9da6..a7c2a515 100644 --- a/src/app/remote_commands.rs +++ b/src/app/remote_commands.rs @@ -156,6 +156,15 @@ pub(crate) async fn remote_command_loop( let git_statuses = git_status_tx.borrow().clone(); let data = ws.data(); + // Build terminal size map from the registry + let size_map: HashMap = { + let registry = terminals.lock(); + registry.iter().map(|(id, term)| { + let size = term.resize_state.lock().size; + (id.clone(), (size.cols, size.rows)) + }).collect() + }; + // Build a lookup map for projects let project_map: std::collections::HashMap<&str, &crate::workspace::state::ProjectData> = data.projects.iter().map(|p| (p.id.as_str(), p)).collect(); @@ -200,7 +209,7 @@ pub(crate) async fn remote_command_loop( name: p.name.clone(), path: p.path.clone(), show_in_overview: api_project_visibility(&p.id, hidden_project_ids), - layout: p.layout.as_ref().map(|l| l.to_api()), + layout: p.layout.as_ref().map(|l| l.to_api_with_sizes(&size_map)), terminal_names: p.terminal_names.clone(), git_status, folder_color: p.folder_color, From 41f841c241fbc527b9b03db415f690f8abb2e970 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:16:06 +0100 Subject: [PATCH 06/37] chore(mobile/ios): configure iOS project with CocoaPods and signing Set up CocoaPods integration, development team, scene manifest, local networking permissions, and rename native library to okena_mobile_native. Co-Authored-By: Claude Opus 4.6 --- mobile/ios/Flutter/AppFrameworkInfo.plist | 2 - mobile/ios/Flutter/Debug.xcconfig | 1 + mobile/ios/Flutter/Release.xcconfig | 1 + mobile/ios/Podfile | 43 ++++++ mobile/ios/Podfile.lock | 35 +++++ mobile/ios/Runner.xcodeproj/project.pbxproj | 129 +++++++++++++++++- .../contents.xcworkspacedata | 3 + mobile/ios/Runner/AppDelegate.swift | 7 +- mobile/ios/Runner/Info.plist | 34 ++++- 9 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 mobile/ios/Podfile create mode 100644 mobile/ios/Podfile.lock diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ b/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/mobile/ios/Flutter/Debug.xcconfig +++ b/mobile/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/mobile/ios/Flutter/Release.xcconfig +++ b/mobile/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile new file mode 100644 index 00000000..620e46eb --- /dev/null +++ b/mobile/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock new file mode 100644 index 00000000..e4313dd1 --- /dev/null +++ b/mobile/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - rust_lib_mobile (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - rust_lib_mobile (from `.symlinks/plugins/rust_lib_mobile/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + rust_lib_mobile: + :path: ".symlinks/plugins/rust_lib_mobile/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + rust_lib_mobile: dea72a6cd79b7b0f9290832863b286c0e1089e95 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 1f1b1aae..628a8575 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -10,7 +10,9 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5D927AE7C85E7E21F166320A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A00D3A07A3D7AB504E49C92B /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8CDAFE869349F09635AB6E97 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDC7A6B878DA9CD9A6F58B28 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -40,11 +42,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 08742543B10EB622BFFF46D1 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2DA56008CD86AEF940B2FB34 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 2E6912DF4BFB900C5A6D5BDA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3E8F5284E6CBECDEFD0CB98B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 4C3662B941C3A1379E40692E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -55,6 +62,9 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 97CC3AB8249A32BF8FCC1482 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + A00D3A07A3D7AB504E49C92B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EDC7A6B878DA9CD9A6F58B28 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,12 +72,34 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8CDAFE869349F09635AB6E97 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A920B32E1280E1AD7354E52F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5D927AE7C85E7E21F166320A /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1200D467201819F9E21D60B5 /* Pods */ = { + isa = PBXGroup; + children = ( + 97CC3AB8249A32BF8FCC1482 /* Pods-Runner.debug.xcconfig */, + 4C3662B941C3A1379E40692E /* Pods-Runner.release.xcconfig */, + 3E8F5284E6CBECDEFD0CB98B /* Pods-Runner.profile.xcconfig */, + 2DA56008CD86AEF940B2FB34 /* Pods-RunnerTests.debug.xcconfig */, + 2E6912DF4BFB900C5A6D5BDA /* Pods-RunnerTests.release.xcconfig */, + 08742543B10EB622BFFF46D1 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -94,6 +126,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 1200D467201819F9E21D60B5 /* Pods */, + BCB270922978D966DAFBE887 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +155,15 @@ path = Runner; sourceTree = ""; }; + BCB270922978D966DAFBE887 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EDC7A6B878DA9CD9A6F58B28 /* Pods_Runner.framework */, + A00D3A07A3D7AB504E49C92B /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +171,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 3A2067B0755B6C7A1CDF4EFD /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + A920B32E1280E1AD7354E52F /* Frameworks */, ); buildRules = ( ); @@ -145,12 +190,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + ED4B0DDE84CC1AC9C309073D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + EF29B406BDA8A4DF6634F855 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +269,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 3A2067B0755B6C7A1CDF4EFD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +322,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + ED4B0DDE84CC1AC9C309073D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EF29B406BDA8A4DF6634F855 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -327,6 +435,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -335,6 +444,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = UA8K24B574; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -349,6 +459,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -362,13 +473,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UA8K24B574; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile; + PRODUCT_BUNDLE_IDENTIFIER = com.example.okena.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -378,6 +490,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 2DA56008CD86AEF940B2FB34 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -395,6 +508,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 2E6912DF4BFB900C5A6D5BDA /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -410,6 +524,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 08742543B10EB622BFFF46D1 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -447,6 +562,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -455,6 +571,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = UA8K24B574; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -476,6 +593,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -504,6 +622,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -512,6 +631,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = UA8K24B574; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -526,6 +646,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -541,13 +662,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UA8K24B574; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile; + PRODUCT_BUNDLE_IDENTIFIER = com.example.okena.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -563,13 +685,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UA8K24B574; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile; + PRODUCT_BUNDLE_IDENTIFIER = com.example.okena.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 62666446..c30b367e 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 43427ad3..f3a46535 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,34 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +71,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - From dbd1d94c7a71710beff7d85078e0f5d6ef652bfb Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:17:03 +0100 Subject: [PATCH 07/37] feat(mobile): add design system with OkenaColors and OkenaTypography Introduce centralized color palette (backgrounds, borders, accent, text hierarchy, glass effects) and typography system (SF Pro Text) for iOS-native dark theme. Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/theme/app_theme.dart | 118 ++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/mobile/lib/src/theme/app_theme.dart b/mobile/lib/src/theme/app_theme.dart index 02977931..0d9ad2b9 100644 --- a/mobile/lib/src/theme/app_theme.dart +++ b/mobile/lib/src/theme/app_theme.dart @@ -1,12 +1,120 @@ -import 'dart:ui'; +import 'package:flutter/painting.dart'; + +// ── Color system ──────────────────────────────────────────────────────── + +class OkenaColors { + OkenaColors._(); + + // Backgrounds + static const background = Color(0xFF000000); + static const surface = Color(0xFF0A0A0A); + static const surfaceElevated = Color(0xFF161616); + static const surfaceOverlay = Color(0xFF1C1C1C); + + // Borders + static const border = Color(0xFF1E1E1E); + static const borderLight = Color(0xFF2A2A2A); + + // Accent + static const accent = Color(0xFF7C7FFF); + + // Text + static const textPrimary = Color(0xFFE8E8EC); + static const textSecondary = Color(0xFF98989F); + static const textTertiary = Color(0xFF5A5A62); + + // Semantic + static const success = Color(0xFF4ADE80); + static const warning = Color(0xFFFBBF24); + static const error = Color(0xFFF87171); + + // Glass + static const glassBg = Color(0xCC0A0A0A); // 80% opacity surface + static const glassStroke = Color(0x18FFFFFF); // subtle white border + + // Key toolbar + static const keyBg = Color(0xFF161616); + static const keyBorder = Color(0xFF2A2A2A); + static const keyText = Color(0xFFB0B0B8); +} + +// ── Typography ────────────────────────────────────────────────────────── + +class OkenaTypography { + OkenaTypography._(); + + static const _fontFamily = '.SF Pro Text'; + + static const largeTitle = TextStyle( + fontFamily: _fontFamily, + fontSize: 28, + fontWeight: FontWeight.w700, + letterSpacing: -0.5, + color: OkenaColors.textPrimary, + ); + + static const title = TextStyle( + fontFamily: _fontFamily, + fontSize: 20, + fontWeight: FontWeight.w600, + letterSpacing: -0.3, + color: OkenaColors.textPrimary, + ); + + static const headline = TextStyle( + fontFamily: _fontFamily, + fontSize: 17, + fontWeight: FontWeight.w600, + color: OkenaColors.textPrimary, + ); + + static const body = TextStyle( + fontFamily: _fontFamily, + fontSize: 15, + fontWeight: FontWeight.w400, + color: OkenaColors.textPrimary, + ); + + static const callout = TextStyle( + fontFamily: _fontFamily, + fontSize: 14, + fontWeight: FontWeight.w400, + color: OkenaColors.textSecondary, + ); + + static const caption = TextStyle( + fontFamily: _fontFamily, + fontSize: 12, + fontWeight: FontWeight.w500, + color: OkenaColors.textSecondary, + ); + + static const caption2 = TextStyle( + fontFamily: _fontFamily, + fontSize: 11, + fontWeight: FontWeight.w500, + color: OkenaColors.textTertiary, + ); +} + +// ── Terminal theme ────────────────────────────────────────────────────── class TerminalTheme { static const fontFamily = 'JetBrainsMono'; + static const fontFamilyFallback = [ + 'Menlo', + 'Consolas', + 'DejaVu Sans Mono', + 'monospace', + ]; static const defaultFontSize = 13.0; + static const minFontSize = 6.0; + static const maxFontSize = 24.0; + static const defaultColumns = 80; static const lineHeightFactor = 1.2; - static const bgColor = Color(0xFF1E1E1E); - static const fgColor = Color(0xFFCCCCCC); - static const cursorColor = Color(0xFFAEAFAD); - static const selectionColor = Color(0x40264F78); + static const bgColor = Color(0xFF000000); + static const fgColor = Color(0xFFCDD6F4); + static const cursorColor = Color(0xFFF5E0DC); + static const selectionColor = Color(0x40585B70); } From e15073e3bdb22ba8e3c3273b651f3e19f10b69a5 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:17:10 +0100 Subject: [PATCH 08/37] feat(mobile): add terminal scroll, resize, and display offset FFI Add scroll_terminal, get_display_offset, and resize_local functions. Improve wide char spacer handling and move inverse flag to painter. Co-Authored-By: Claude Opus 4.6 --- mobile/native/src/api/terminal.rs | 10 ++++++++++ mobile/native/src/client/handler.rs | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/mobile/native/src/api/terminal.rs b/mobile/native/src/api/terminal.rs index 70d40a19..0996e19f 100644 --- a/mobile/native/src/api/terminal.rs +++ b/mobile/native/src/api/terminal.rs @@ -185,3 +185,13 @@ pub fn resize_terminal( let mgr = ConnectionManager::get(); mgr.resize_terminal(&conn_id, &terminal_id, cols, rows); } + +/// Resize only the local alacritty terminal — does NOT send a WS resize message to the server. +/// Used when mobile adapts to the server's terminal size. +#[flutter_rust_bridge::frb(sync)] +pub fn resize_local(conn_id: String, terminal_id: String, cols: u16, rows: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.resize(cols, rows); + }); +} diff --git a/mobile/native/src/client/handler.rs b/mobile/native/src/client/handler.rs index b7427cad..ff054d0f 100644 --- a/mobile/native/src/client/handler.rs +++ b/mobile/native/src/client/handler.rs @@ -40,13 +40,16 @@ impl ConnectionHandler for MobileConnectionHandler { _terminal_id: &str, prefixed_id: &str, _ws_sender: async_channel::Sender, + cols: u16, + rows: u16, ) { // Skip if terminal already exists — avoids leaking the old TerminalHolder // (and its alacritty grid) on reconnect when the server re-sends creates. if self.terminals.read().contains_key(prefixed_id) { return; } - let holder = TerminalHolder::new(80, 24); + let (c, r) = if cols > 0 && rows > 0 { (cols, rows) } else { (80, 24) }; + let holder = TerminalHolder::new(c, r); self.terminals .write() .insert(prefixed_id.to_string(), holder); @@ -115,7 +118,7 @@ mod tests { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); assert!(handler.terminals().read().contains_key("remote:conn1:t1")); handler.remove_terminal("remote:conn1:t1"); @@ -127,14 +130,14 @@ mod tests { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone()); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone(), 0, 0); let ptr1 = { let terminals = handler.terminals().read(); terminals.get("remote:conn1:t1").unwrap() as *const TerminalHolder }; // Second create with same prefixed_id should be a no-op - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); let ptr2 = { let terminals = handler.terminals().read(); terminals.get("remote:conn1:t1").unwrap() as *const TerminalHolder @@ -149,9 +152,9 @@ mod tests { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone()); - handler.create_terminal("conn1", "t2", "remote:conn1:t2", tx.clone()); - handler.create_terminal("conn2", "t3", "remote:conn2:t3", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx.clone(), 0, 0); + handler.create_terminal("conn1", "t2", "remote:conn1:t2", tx.clone(), 0, 0); + handler.create_terminal("conn2", "t3", "remote:conn2:t3", tx, 0, 0); handler.remove_all_terminals("conn1"); @@ -166,7 +169,7 @@ mod tests { let handler = make_handler(); let (tx, _rx) = async_channel::bounded(1); - handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx); + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); handler.on_terminal_output("remote:conn1:t1", b"hello"); let terminals = handler.terminals().read(); From 8e569485d936d34447f2f8be65548c572a342a57 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:17:18 +0100 Subject: [PATCH 09/37] feat(mobile): add folders, project ordering, and terminal management FFI Add folder info, project ordering, server terminal size, and create/close/focus terminal actions. Update handler to use server terminal dimensions when creating terminals. Co-Authored-By: Claude Opus 4.6 --- mobile/native/src/api/state.rs | 87 +++++++++++++++++++++++++++++ mobile/native/src/client/handler.rs | 24 ++++++++ mobile/native/src/client/manager.rs | 49 ++++++++++++++++ mobile/native/src/frb_generated.rs | 4 ++ 4 files changed, 164 insertions(+) diff --git a/mobile/native/src/api/state.rs b/mobile/native/src/api/state.rs index 7ae29f3b..fec00000 100644 --- a/mobile/native/src/api/state.rs +++ b/mobile/native/src/api/state.rs @@ -14,6 +14,23 @@ pub struct ProjectInfo { pub show_in_overview: bool, pub terminal_ids: Vec, pub terminal_names: HashMap, + pub folder_color: String, +} + +/// FFI-friendly folder info. +#[derive(Debug, Clone)] +pub struct FolderInfo { + pub id: String, + pub name: String, + pub project_ids: Vec, + pub folder_color: String, +} + +/// Server terminal size returned via FFI. +#[derive(Debug, Clone)] +pub struct ServerTerminalSize { + pub cols: u16, + pub rows: u16, } /// Get all projects from the cached remote state. @@ -43,6 +60,7 @@ pub fn get_projects(conn_id: String) -> Vec { show_in_overview: p.show_in_overview, terminal_ids, terminal_names: p.terminal_names.clone(), + folder_color: format!("{:?}", p.folder_color), } }) .collect() @@ -139,3 +157,72 @@ pub async fn close_terminal( .await } +/// Focus a terminal in a project. +pub async fn focus_terminal( + conn_id: String, + project_id: String, + terminal_id: String, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::FocusTerminal { + project_id, + terminal_id, + }, + ) + .await +} + +/// Get the server-side terminal size from the cached state. +#[flutter_rust_bridge::frb(sync)] +pub fn get_server_terminal_size(conn_id: String, terminal_id: String) -> ServerTerminalSize { + let mgr = ConnectionManager::get(); + let state = match mgr.get_state(&conn_id) { + Some(s) => s, + None => return ServerTerminalSize { cols: 0, rows: 0 }, + }; + + for project in &state.projects { + if let Some(ref layout) = project.layout { + if let Some(size) = find_terminal_size(layout, &terminal_id) { + return size; + } + } + } + + ServerTerminalSize { cols: 0, rows: 0 } +} + +fn find_terminal_size( + node: &okena_core::api::ApiLayoutNode, + target_id: &str, +) -> Option { + match node { + okena_core::api::ApiLayoutNode::Terminal { + terminal_id, + cols, + rows, + .. + } => { + if terminal_id.as_deref() == Some(target_id) { + match (cols, rows) { + (Some(c), Some(r)) => Some(ServerTerminalSize { cols: *c, rows: *r }), + _ => None, + } + } else { + None + } + } + okena_core::api::ApiLayoutNode::Split { children, .. } + | okena_core::api::ApiLayoutNode::Tabs { children, .. } => { + for child in children { + if let Some(size) = find_terminal_size(child, target_id) { + return Some(size); + } + } + None + } + } +} + diff --git a/mobile/native/src/client/handler.rs b/mobile/native/src/client/handler.rs index ff054d0f..ff1de1b4 100644 --- a/mobile/native/src/client/handler.rs +++ b/mobile/native/src/client/handler.rs @@ -164,6 +164,30 @@ mod tests { assert!(terminals.contains_key("remote:conn2:t3")); } + #[test] + fn create_terminal_uses_server_size() { + let handler = make_handler(); + let (tx, _rx) = async_channel::bounded(1); + + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 160, 48); + let terminals = handler.terminals().read(); + let holder = terminals.get("remote:conn1:t1").unwrap(); + let cells = holder.get_visible_cells(&okena_core::theme::DARK_THEME); + assert_eq!(cells.len(), 160 * 48); + } + + #[test] + fn create_terminal_falls_back_to_default_on_zero_size() { + let handler = make_handler(); + let (tx, _rx) = async_channel::bounded(1); + + handler.create_terminal("conn1", "t1", "remote:conn1:t1", tx, 0, 0); + let terminals = handler.terminals().read(); + let holder = terminals.get("remote:conn1:t1").unwrap(); + let cells = holder.get_visible_cells(&okena_core::theme::DARK_THEME); + assert_eq!(cells.len(), 80 * 24); + } + #[test] fn on_terminal_output_routes_data() { let handler = make_handler(); diff --git a/mobile/native/src/client/manager.rs b/mobile/native/src/client/manager.rs index a1a26c11..5b598e1b 100644 --- a/mobile/native/src/client/manager.rs +++ b/mobile/native/src/client/manager.rs @@ -261,6 +261,55 @@ impl ConnectionManager { } } + /// Execute an action on the remote server via HTTP POST /v1/actions. + /// Returns the response body as a string. + pub async fn execute_action( + &self, + conn_id: &str, + action: okena_core::api::ActionRequest, + ) -> anyhow::Result { + let (host, port, token) = { + let connections = self.connections.read(); + let conn = connections + .get(conn_id) + .ok_or_else(|| anyhow::anyhow!("Connection not found: {}", conn_id))?; + let client = conn.client.read(); + let config = client.config(); + let token = config + .saved_token + .clone() + .ok_or_else(|| anyhow::anyhow!("No auth token for connection {}", conn_id))?; + (config.host.clone(), config.port, token) + }; + + let url = format!("http://{}:{}/v1/actions", host, port); + let client = reqwest::Client::new(); + let resp = client + .post(&url) + .header("Authorization", format!("Bearer {}", token)) + .json(&action) + .timeout(std::time::Duration::from_secs(10)) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + anyhow::bail!("Action failed: HTTP {} - {}", status, body); + } + + Ok(resp.text().await.unwrap_or_default()) + } + + /// Spawn an async task on the connection manager's runtime. + pub fn spawn(&self, future: F) -> tokio::task::JoinHandle + where + F: std::future::Future + Send + 'static, + F::Output: Send + 'static, + { + self.runtime.spawn(future) + } + /// Resize a terminal holder and send the resize message to the server. pub fn resize_terminal(&self, conn_id: &str, terminal_id: &str, cols: u16, rows: u16) { let connections = self.connections.read(); diff --git a/mobile/native/src/frb_generated.rs b/mobile/native/src/frb_generated.rs index b9b4ea32..3e3f9fb6 100644 --- a/mobile/native/src/frb_generated.rs +++ b/mobile/native/src/frb_generated.rs @@ -1180,6 +1180,7 @@ impl SseDecode for crate::api::state::ProjectInfo { let mut var_terminalIds = >::sse_decode(deserializer); let mut var_terminalNames = >::sse_decode(deserializer); + let mut var_folderColor = ::sse_decode(deserializer); return crate::api::state::ProjectInfo { id: var_id, name: var_name, @@ -1187,6 +1188,7 @@ impl SseDecode for crate::api::state::ProjectInfo { show_in_overview: var_isVisible, terminal_ids: var_terminalIds, terminal_names: var_terminalNames, + folder_color: var_folderColor, }; } } @@ -1417,6 +1419,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::state::ProjectInfo { self.show_in_overview.into_into_dart().into_dart(), self.terminal_ids.into_into_dart().into_dart(), self.terminal_names.into_into_dart().into_dart(), + self.folder_color.into_into_dart().into_dart(), ] .into_dart() } @@ -1663,6 +1666,7 @@ impl SseEncode for crate::api::state::ProjectInfo { ::sse_encode(self.show_in_overview, serializer); >::sse_encode(self.terminal_ids, serializer); >::sse_encode(self.terminal_names, serializer); + ::sse_encode(self.folder_color, serializer); } } From 2a7b2ef2ac3639170b77be33e76e34014a70bce4 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:17:27 +0100 Subject: [PATCH 10/37] feat(mobile): redesign app with iOS-native dark theme and terminal improvements Rewrite all screens and widgets with Cupertino-inspired design: frosted glass headers, card-based layouts, haptic feedback, animated status indicators. Add pinch-to-zoom, scroll support, auto-fit font sizing, modifier key system, and text batching optimization to terminal rendering. Co-Authored-By: Claude Opus 4.6 --- mobile/lib/main.dart | 120 ++- .../lib/src/providers/workspace_provider.dart | 37 + mobile/lib/src/screens/pairing_screen.dart | 244 +++-- .../lib/src/screens/server_list_screen.dart | 265 ++++-- mobile/lib/src/screens/workspace_screen.dart | 565 ++++++------ mobile/lib/src/widgets/key_toolbar.dart | 835 ++++++++++-------- mobile/lib/src/widgets/layout_renderer.dart | 200 +++++ mobile/lib/src/widgets/project_drawer.dart | 505 +++++++---- mobile/lib/src/widgets/status_indicator.dart | 126 ++- mobile/lib/src/widgets/terminal_painter.dart | 137 ++- mobile/lib/src/widgets/terminal_view.dart | 361 +++++--- 11 files changed, 2222 insertions(+), 1173 deletions(-) create mode 100644 mobile/lib/src/widgets/layout_renderer.dart diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 1d1cfafa..25f205da 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,4 +1,8 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:provider/provider.dart'; import 'src/providers/connection_provider.dart'; @@ -6,10 +10,27 @@ import 'src/providers/workspace_provider.dart'; import 'src/screens/server_list_screen.dart'; import 'src/screens/pairing_screen.dart'; import 'src/screens/workspace_screen.dart'; +import 'src/theme/app_theme.dart'; import 'src/rust/frb_generated.dart'; Future main() async { - await RustLib.init(); + WidgetsFlutterBinding.ensureInitialized(); + + await RustLib.init( + externalLibrary: Platform.isIOS + ? ExternalLibrary.process(iKnowHowToUseIt: true) + : null, + ); + + // Edge-to-edge, dark status bar + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + systemNavigationBarColor: OkenaColors.background, + )); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + runApp(const OkenaApp()); } @@ -31,13 +52,71 @@ class OkenaApp extends StatelessWidget { child: MaterialApp( title: 'Okena', theme: ThemeData.dark(useMaterial3: true).copyWith( + scaffoldBackgroundColor: OkenaColors.background, colorScheme: const ColorScheme.dark( - primary: Color(0xFF007ACC), - surface: Color(0xFF1E1E1E), + primary: OkenaColors.accent, + surface: OkenaColors.surface, + error: OkenaColors.error, ), - scaffoldBackgroundColor: const Color(0xFF1E1E1E), appBarTheme: const AppBarTheme( - backgroundColor: Color(0xFF323233), + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + ), + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: OkenaColors.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + dragHandleColor: OkenaColors.textTertiary, + showDragHandle: true, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: OkenaColors.surfaceElevated, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: OkenaColors.border, width: 0.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: OkenaColors.accent, width: 1), + ), + labelStyle: OkenaTypography.callout, + hintStyle: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: OkenaColors.accent, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: OkenaTypography.headline, + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: OkenaColors.textPrimary, + side: const BorderSide(color: OkenaColors.border), + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: OkenaTypography.headline, + ), + ), + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + }, ), ), home: const AppRouter(), @@ -53,12 +132,33 @@ class AppRouter extends StatelessWidget { Widget build(BuildContext context) { final connection = context.watch(); + Widget child; if (connection.isConnected) { - return const WorkspaceScreen(); - } - if (connection.activeServer != null) { - return const PairingScreen(); + child = const WorkspaceScreen(key: ValueKey('workspace')); + } else if (connection.activeServer != null) { + child = const PairingScreen(key: ValueKey('pairing')); + } else { + child = const ServerListScreen(key: ValueKey('servers')); } - return const ServerListScreen(); + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.02), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + )), + child: child, + ), + ); + }, + child: child, + ); } } diff --git a/mobile/lib/src/providers/workspace_provider.dart b/mobile/lib/src/providers/workspace_provider.dart index aa51d962..21b95545 100644 --- a/mobile/lib/src/providers/workspace_provider.dart +++ b/mobile/lib/src/providers/workspace_provider.dart @@ -9,6 +9,8 @@ import '../../src/rust/api/connection.dart' as conn_ffi; class WorkspaceProvider extends ChangeNotifier { final ConnectionProvider _connection; List _projects = []; + List _folders = []; + List _projectOrder = []; String? _selectedProjectId; String? _selectedTerminalId; Set? _previousTerminalIds; @@ -16,6 +18,8 @@ class WorkspaceProvider extends ChangeNotifier { double _secondsSinceActivity = 0; List get projects => _projects; + List get folders => _folders; + List get projectOrder => _projectOrder; String? get selectedProjectId => _selectedProjectId; String? get selectedTerminalId => _selectedTerminalId; double get secondsSinceActivity => _secondsSinceActivity; @@ -52,6 +56,8 @@ class WorkspaceProvider extends ChangeNotifier { } else { _stopPolling(); _projects = []; + _folders = []; + _projectOrder = []; _selectedProjectId = null; _selectedTerminalId = null; notifyListeners(); @@ -78,6 +84,8 @@ class WorkspaceProvider extends ChangeNotifier { if (connId == null) return; final newProjects = ffi.getProjects(connId: connId); + final newFolders = ffi.getFolders(connId: connId); + final newProjectOrder = ffi.getProjectOrder(connId: connId); final focusedId = ffi.getFocusedProjectId(connId: connId); bool changed = false; @@ -87,6 +95,16 @@ class WorkspaceProvider extends ChangeNotifier { changed = true; } + if (!_folderListEquals(newFolders, _folders)) { + _folders = newFolders; + changed = true; + } + + if (!_stringListEquals(newProjectOrder, _projectOrder)) { + _projectOrder = newProjectOrder; + changed = true; + } + // Auto-select the focused project if we don't have a selection if (_selectedProjectId == null && focusedId != null) { _selectedProjectId = focusedId; @@ -135,6 +153,17 @@ class WorkspaceProvider extends ChangeNotifier { bool _projectListEquals( List a, List b) { if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i].id != b[i].id || + a[i].name != b[i].name || + a[i].folderColor != b[i].folderColor) return false; + } + return true; + } + + bool _folderListEquals( + List a, List b) { + if (a.length != b.length) return false; for (int i = 0; i < a.length; i++) { if (a[i].id != b[i].id || a[i].name != b[i].name) return false; if (!listEquals(a[i].terminalIds, b[i].terminalIds)) return false; @@ -142,6 +171,14 @@ class WorkspaceProvider extends ChangeNotifier { return true; } + bool _stringListEquals(List a, List b) { + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + @override void dispose() { _connection.removeListener(_onConnectionChanged); diff --git a/mobile/lib/src/screens/pairing_screen.dart b/mobile/lib/src/screens/pairing_screen.dart index 0abb5d68..82062cb8 100644 --- a/mobile/lib/src/screens/pairing_screen.dart +++ b/mobile/lib/src/screens/pairing_screen.dart @@ -1,7 +1,10 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../providers/connection_provider.dart'; +import '../theme/app_theme.dart'; import '../widgets/status_indicator.dart'; import '../../src/rust/api/connection.dart'; @@ -26,6 +29,7 @@ class _PairingScreenState extends State { final code = _codeController.text.trim(); if (code.isEmpty) return; + HapticFeedback.mediumImpact(); setState(() => _submitting = true); final provider = context.read(); await provider.pair(code); @@ -44,96 +48,168 @@ class _PairingScreenState extends State { : null; return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => provider.disconnect(), - ), - title: Text(provider.activeServer?.displayName ?? 'Connecting'), - ), - body: Padding( - padding: const EdgeInsets.all(24), + backgroundColor: OkenaColors.background, + body: SafeArea( child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Center( - child: StatusIndicator(status: provider.status), - ), - const SizedBox(height: 32), - if (!showCodeInput && !isError) ...[ - const Center(child: CircularProgressIndicator()), - const SizedBox(height: 16), - Text( - 'Connecting to server...', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], - if (showCodeInput) ...[ - Text( - 'Enter Pairing Code', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - 'Check the Okena desktop app for the pairing code.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, + // Header + Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 16, 0), + child: Row( + children: [ + IconButton( + icon: const Icon( + CupertinoIcons.chevron_back, + color: OkenaColors.accent, + size: 22, + ), + onPressed: () => provider.disconnect(), + ), + const SizedBox(width: 4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + provider.activeServer?.displayName ?? 'Server', + style: OkenaTypography.headline, + ), + const SizedBox(height: 1), + Text( + 'Connecting', + style: OkenaTypography.caption2.copyWith( + color: OkenaColors.textTertiary, + ), + ), + ], ), + ), + ], ), - const SizedBox(height: 24), - TextField( - controller: _codeController, - decoration: const InputDecoration( - labelText: 'XXXX-XXXX', - border: OutlineInputBorder(), - ), - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 24, - letterSpacing: 4, - fontFamily: 'monospace', + ), + // Body + Expanded( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: StatusIndicator(status: provider.status), + ), + const SizedBox(height: 40), + if (!showCodeInput && !isError) ...[ + const Center( + child: CupertinoActivityIndicator( + radius: 14, + color: OkenaColors.textSecondary, + ), + ), + const SizedBox(height: 20), + Text( + 'Connecting to server...', + textAlign: TextAlign.center, + style: OkenaTypography.body.copyWith( + color: OkenaColors.textSecondary, + ), + ), + ], + if (showCodeInput) ...[ + Text( + 'Pair with Server', + textAlign: TextAlign.center, + style: OkenaTypography.largeTitle, + ), + const SizedBox(height: 8), + Text( + 'Check the Okena desktop app for the pairing code.', + textAlign: TextAlign.center, + style: OkenaTypography.body.copyWith( + color: OkenaColors.textSecondary, + ), + ), + const SizedBox(height: 32), + TextField( + controller: _codeController, + decoration: const InputDecoration( + hintText: 'XXXX-XXXX', + ), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 28, + letterSpacing: 8, + fontFamily: 'JetBrainsMono', + fontWeight: FontWeight.w500, + color: OkenaColors.textPrimary, + ), + textCapitalization: TextCapitalization.characters, + keyboardType: TextInputType.text, + autofocus: true, + onSubmitted: (_) => _submitCode(), + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _submitting ? null : _submitCode, + child: _submitting + ? const CupertinoActivityIndicator( + radius: 10, + color: Colors.white, + ) + : const Text('Pair'), + ), + ], + if (isError) ...[ + Center( + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: OkenaColors.error.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon( + CupertinoIcons.xmark_circle, + size: 32, + color: OkenaColors.error, + ), + ), + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: OkenaColors.error.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: OkenaColors.error.withOpacity(0.2), + width: 0.5, + ), + ), + child: Text( + errorMessage ?? 'Connection failed', + textAlign: TextAlign.center, + style: OkenaTypography.body.copyWith( + color: OkenaColors.error, + ), + ), + ), + const SizedBox(height: 24), + OutlinedButton( + onPressed: () { + final server = provider.activeServer; + if (server != null) { + provider.disconnect(); + provider.connectTo(server); + } + }, + child: const Text('Try Again'), + ), + ], + ], ), - textCapitalization: TextCapitalization.characters, - keyboardType: TextInputType.text, - autofocus: true, - onSubmitted: (_) => _submitCode(), - ), - const SizedBox(height: 16), - FilledButton( - onPressed: _submitting ? null : _submitCode, - child: _submitting - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Pair'), ), - ], - if (isError) ...[ - Icon(Icons.error_outline, size: 48, color: Colors.red.shade300), - const SizedBox(height: 16), - Text( - errorMessage ?? 'Connection failed', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.red.shade300), - ), - const SizedBox(height: 24), - OutlinedButton( - onPressed: () { - final server = provider.activeServer; - if (server != null) { - provider.disconnect(); - provider.connectTo(server); - } - }, - child: const Text('Retry'), - ), - ], + ), ], ), ), diff --git a/mobile/lib/src/screens/server_list_screen.dart b/mobile/lib/src/screens/server_list_screen.dart index 0e823db1..cac98005 100644 --- a/mobile/lib/src/screens/server_list_screen.dart +++ b/mobile/lib/src/screens/server_list_screen.dart @@ -1,8 +1,11 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../models/saved_server.dart'; import '../providers/connection_provider.dart'; +import '../theme/app_theme.dart'; class ServerListScreen extends StatelessWidget { const ServerListScreen({super.key}); @@ -12,69 +15,186 @@ class ServerListScreen extends StatelessWidget { final provider = context.watch(); return Scaffold( - appBar: AppBar( - title: const Text('Okena'), - centerTitle: true, - ), - body: provider.servers.isEmpty - ? Center( - child: Column( - mainAxisSize: MainAxisSize.min, + backgroundColor: OkenaColors.background, + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Icon( - Icons.terminal, - size: 64, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - 'No servers yet', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text( - 'Add a server to get started', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), + const Text('Servers', style: OkenaTypography.largeTitle), + GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + _showAddServerSheet(context); + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: OkenaColors.surfaceElevated, + shape: BoxShape.circle, + border: Border.all(color: OkenaColors.border, width: 0.5), + ), + child: const Icon( + CupertinoIcons.plus, + color: OkenaColors.accent, + size: 18, + ), + ), ), ], ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: provider.servers.length, - itemBuilder: (context, index) { - final server = provider.servers[index]; - return Dismissible( - key: ValueKey('${server.host}:${server.port}'), - direction: DismissDirection.endToStart, - background: Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 24), - color: Colors.red, - child: const Icon(Icons.delete, color: Colors.white), - ), - onDismissed: (_) => provider.removeServer(server), - child: ListTile( - leading: const Icon(Icons.dns), - title: Text(server.displayName), - subtitle: server.label != null - ? Text('${server.host}:${server.port}') - : null, - trailing: const Icon(Icons.chevron_right), - onTap: () => provider.connectTo(server), - ), - ); - }, ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showAddServerSheet(context), - child: const Icon(Icons.add), + // Content + Expanded( + child: provider.servers.isEmpty + ? _buildEmptyState(context) + : _buildServerList(context, provider), + ), + ], + ), ), ); } + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + OkenaColors.accent.withOpacity(0.15), + OkenaColors.accent.withOpacity(0.0), + ], + ), + ), + child: Icon( + Icons.terminal_rounded, + size: 36, + color: OkenaColors.accent.withOpacity(0.8), + ), + ), + const SizedBox(height: 20), + const Text('No servers yet', style: OkenaTypography.title), + const SizedBox(height: 8), + Text( + 'Add a server to get started', + style: OkenaTypography.body.copyWith(color: OkenaColors.textSecondary), + ), + const SizedBox(height: 28), + SizedBox( + width: 180, + child: FilledButton( + onPressed: () => _showAddServerSheet(context), + child: const Text('Add Server'), + ), + ), + ], + ), + ); + } + + Widget _buildServerList(BuildContext context, ConnectionProvider provider) { + return ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + itemCount: provider.servers.length, + itemBuilder: (context, index) { + final server = provider.servers[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Dismissible( + key: ValueKey('${server.host}:${server.port}'), + direction: DismissDirection.endToStart, + background: Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 24), + decoration: BoxDecoration( + color: OkenaColors.error.withOpacity(0.15), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(CupertinoIcons.delete, color: OkenaColors.error, size: 20), + ), + onDismissed: (_) => provider.removeServer(server), + child: GestureDetector( + onTap: () { + HapticFeedback.selectionClick(); + provider.connectTo(server); + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: OkenaColors.surface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: OkenaColors.border, width: 0.5), + ), + child: Row( + children: [ + // Letter avatar + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: OkenaColors.accent.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + ), + alignment: Alignment.center, + child: Text( + server.displayName[0].toUpperCase(), + style: OkenaTypography.headline.copyWith( + color: OkenaColors.accent, + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + server.displayName, + style: OkenaTypography.body.copyWith( + fontWeight: FontWeight.w500, + ), + ), + if (server.label != null) ...[ + const SizedBox(height: 3), + Text( + '${server.host}:${server.port}', + style: OkenaTypography.caption.copyWith( + fontFamily: 'JetBrainsMono', + color: OkenaColors.textTertiary, + ), + ), + ], + ], + ), + ), + const Icon( + CupertinoIcons.chevron_right, + color: OkenaColors.textTertiary, + size: 16, + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + void _showAddServerSheet(BuildContext context) { final hostController = TextEditingController(); final portController = TextEditingController(text: '19100'); @@ -83,53 +203,72 @@ class ServerListScreen extends StatelessWidget { showModalBottomSheet( context: context, isScrollControlled: true, + backgroundColor: OkenaColors.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), builder: (ctx) => Padding( padding: EdgeInsets.only( left: 24, right: 24, - top: 24, + top: 8, bottom: MediaQuery.of(ctx).viewInsets.bottom + 24, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Drag handle + Center( + child: Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: OkenaColors.textTertiary.withOpacity(0.4), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const Text('Add Server', style: OkenaTypography.title), + const SizedBox(height: 4), Text( - 'Add Server', - style: Theme.of(ctx).textTheme.titleLarge, + 'Enter the host and port of your Okena desktop app', + style: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), ), - const SizedBox(height: 16), + const SizedBox(height: 24), TextField( controller: hostController, decoration: const InputDecoration( labelText: 'Host', - border: OutlineInputBorder(), hintText: '192.168.1.100', ), autofocus: true, keyboardType: TextInputType.url, + style: OkenaTypography.body, ), - const SizedBox(height: 12), + const SizedBox(height: 14), TextField( controller: portController, decoration: const InputDecoration( labelText: 'Port', - border: OutlineInputBorder(), ), keyboardType: TextInputType.number, + style: OkenaTypography.body, ), - const SizedBox(height: 16), + const SizedBox(height: 24), FilledButton( onPressed: () { final host = hostController.text.trim(); final port = int.tryParse(portController.text.trim()) ?? 19100; if (host.isNotEmpty) { + HapticFeedback.mediumImpact(); provider.addServer(SavedServer(host: host, port: port)); Navigator.of(ctx).pop(); } }, - child: const Text('Add'), + child: const Text('Add Server'), ), ], ), diff --git a/mobile/lib/src/screens/workspace_screen.dart b/mobile/lib/src/screens/workspace_screen.dart index f708dade..06ee5324 100644 --- a/mobile/lib/src/screens/workspace_screen.dart +++ b/mobile/lib/src/screens/workspace_screen.dart @@ -1,331 +1,340 @@ +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../providers/connection_provider.dart'; import '../providers/workspace_provider.dart'; import '../rust/api/state.dart' as state_ffi; import '../widgets/project_drawer.dart'; -import '../widgets/key_toolbar.dart'; +import '../widgets/key_toolbar.dart' show KeyToolbar, KeyModifiers; import '../widgets/terminal_view.dart'; +import '../theme/app_theme.dart'; -class WorkspaceScreen extends StatelessWidget { +class WorkspaceScreen extends StatefulWidget { const WorkspaceScreen({super.key}); + @override + State createState() => _WorkspaceScreenState(); +} + +class _WorkspaceScreenState extends State { + late PageController _pageController; + final _keyModifiers = KeyModifiers(); + int _currentPage = 0; + String? _lastProjectId; + + @override + void initState() { + super.initState(); + _pageController = PageController(); + } + + @override + void dispose() { + _pageController.dispose(); + _keyModifiers.dispose(); + super.dispose(); + } + + void _syncState(String projectId, List terminalIds) { + if (projectId != _lastProjectId) { + _lastProjectId = projectId; + _currentPage = 0; + if (_pageController.hasClients) { + _pageController.jumpToPage(0); + } + } + if (_currentPage >= terminalIds.length && terminalIds.isNotEmpty) { + _currentPage = terminalIds.length - 1; + } + } + @override Widget build(BuildContext context) { final workspace = context.watch(); final connection = context.watch(); final project = workspace.selectedProject; final connId = connection.connId; - final selectedTerminalId = workspace.selectedTerminalId; - return Scaffold( - appBar: AppBar( - title: _ProjectSwitcher( - projects: workspace.projects, - selectedProjectId: workspace.selectedProjectId, - onSelect: (id) => workspace.selectProject(id), - ), - leading: Builder( - builder: (ctx) => IconButton( - icon: const Icon(Icons.menu), - onPressed: () => Scaffold.of(ctx).openDrawer(), + if (connId == null || project == null) { + return Scaffold( + backgroundColor: OkenaColors.background, + body: Center( + child: Text( + 'No project selected', + style: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), ), ), - actions: [ - // Connection quality indicator - if (connId != null) - Padding( - padding: const EdgeInsets.only(right: 4), - child: _ConnectionDot( - secondsSinceActivity: workspace.secondsSinceActivity, - ), - ), - if (connId != null && project != null) - IconButton( - icon: const Icon(Icons.add), - tooltip: 'New Terminal', - onPressed: () { - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); + ); + } + + final terminalIds = project.terminalIds; + _syncState(project.id, terminalIds); + + final safeCurrentPage = terminalIds.isNotEmpty + ? _currentPage.clamp(0, terminalIds.length - 1) + : 0; + final currentTerminalId = terminalIds.isNotEmpty + ? terminalIds[safeCurrentPage] + : null; + + return Scaffold( + backgroundColor: OkenaColors.background, + body: SafeArea( + bottom: false, + child: Column( + children: [ + _Header( + projectName: project.name, + folderColor: project.folderColor, + terminalCount: terminalIds.length, + currentPage: safeCurrentPage, + onCreateTerminal: () { + HapticFeedback.mediumImpact(); + state_ffi.createTerminal(connId: connId, projectId: project.id); }, - ), - ], - ), - drawer: const ProjectDrawer(), - body: connId == null || project == null - ? const Center(child: Text('No project selected')) - : selectedTerminalId == null - ? Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'No terminals', - style: TextStyle(color: Colors.grey), - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: () { - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); - }, - icon: const Icon(Icons.add), - label: const Text('New Terminal'), - ), - ], - ), - ) - : Column( - children: [ - if (project.terminalIds.length > 1) - _TerminalTabBar( - terminalIds: project.terminalIds, - terminalNames: project.terminalNames, - selectedTerminalId: selectedTerminalId, - projectId: project.id, + onCloseTerminal: currentTerminalId != null + ? () { + HapticFeedback.mediumImpact(); + state_ffi.closeTerminal( connId: connId, - onSelect: (id) => workspace.selectTerminal(id), - ), - Expanded( - child: TerminalView( + projectId: project.id, + terminalId: currentTerminalId, + ); + } + : null, + ), + Expanded( + child: terminalIds.isEmpty + ? _buildEmptyState(connId, project.id) + : PageView.builder( + controller: _pageController, + itemCount: terminalIds.length, + onPageChanged: (i) => setState(() => _currentPage = i), + itemBuilder: (context, index) => TerminalView( connId: connId, - terminalId: selectedTerminalId, - onTerminalSwipe: (direction) { - final ids = project.terminalIds; - if (ids.length <= 1) return; - final idx = ids.indexOf(selectedTerminalId); - if (idx < 0) return; - final newIdx = (idx + direction).clamp(0, ids.length - 1); - if (newIdx != idx) { - workspace.selectTerminal(ids[newIdx]); - } - }, + terminalId: terminalIds[index], + modifiers: _keyModifiers, ), ), - KeyToolbar( - connId: connId, - terminalId: selectedTerminalId, - ), - ], - ), + ), + KeyToolbar( + connId: connId, + terminalId: currentTerminalId, + modifiers: _keyModifiers, + ), + ], + ), + ), ); } -} - -/// Tappable project name in AppBar that opens a dropdown to switch projects. -class _ProjectSwitcher extends StatelessWidget { - final List projects; - final String? selectedProjectId; - final ValueChanged onSelect; - - const _ProjectSwitcher({ - required this.projects, - required this.selectedProjectId, - required this.onSelect, - }); - @override - Widget build(BuildContext context) { - final selected = projects - .where((p) => p.id == selectedProjectId) - .firstOrNull ?? - projects.firstOrNull; - final name = selected?.name ?? 'No Project'; - - if (projects.length <= 1) { - return Text(name); - } - - return GestureDetector( - onTap: () => _showProjectMenu(context), - child: Row( + Widget _buildEmptyState(String connId, String projectId) { + return Center( + child: Column( mainAxisSize: MainAxisSize.min, children: [ - Flexible( - child: Text( - name, - overflow: TextOverflow.ellipsis, + Icon(Icons.terminal_rounded, color: OkenaColors.textTertiary.withOpacity(0.3), size: 48), + const SizedBox(height: 12), + Text( + 'No terminals', + style: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), + ), + const SizedBox(height: 16), + GestureDetector( + onTap: () { + HapticFeedback.mediumImpact(); + state_ffi.createTerminal(connId: connId, projectId: projectId); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: OkenaColors.accent.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: OkenaColors.accent.withOpacity(0.3), width: 0.5), + ), + child: Text( + 'New Terminal', + style: OkenaTypography.callout.copyWith(color: OkenaColors.accent), + ), ), ), - const SizedBox(width: 4), - const Icon(Icons.arrow_drop_down, size: 20), ], ), ); } +} - void _showProjectMenu(BuildContext context) { - final RenderBox button = context.findRenderObject() as RenderBox; - final overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - final position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(Offset(0, button.size.height), ancestor: overlay), - button.localToGlobal(button.size.bottomRight(Offset.zero), - ancestor: overlay), - ), - Offset.zero & overlay.size, - ); +// ── Header with frosted glass ────────────────────────────────────────── - showMenu( - context: context, - position: position, - items: projects.map((p) { - return PopupMenuItem( - value: p.id, - child: Row( - children: [ - Icon( - Icons.folder, - size: 18, - color: p.id == selectedProjectId - ? Theme.of(context).colorScheme.primary - : null, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - p.name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: p.id == selectedProjectId - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ), - ], - ), - ); - }).toList(), - ).then((value) { - if (value != null) { - onSelect(value); - } - }); - } +Color _folderColorToColor(String colorName) { + return switch (colorName) { + 'red' => const Color(0xFFEF4444), + 'orange' => const Color(0xFFF97316), + 'yellow' => const Color(0xFFEAB308), + 'lime' => const Color(0xFF84CC16), + 'green' => const Color(0xFF22C55E), + 'teal' => const Color(0xFF14B8A6), + 'cyan' => const Color(0xFF06B6D4), + 'blue' => const Color(0xFF3B82F6), + 'indigo' => const Color(0xFF6366F1), + 'purple' => const Color(0xFFA855F7), + 'pink' => const Color(0xFFEC4899), + _ => OkenaColors.textTertiary, + }; } -/// Horizontal tab bar showing terminals in the current project. -class _TerminalTabBar extends StatelessWidget { - final List terminalIds; - final Map terminalNames; - final String selectedTerminalId; - final String projectId; - final String connId; - final ValueChanged onSelect; +class _Header extends StatelessWidget { + final String projectName; + final String folderColor; + final int terminalCount; + final int currentPage; + final VoidCallback onCreateTerminal; + final VoidCallback? onCloseTerminal; - const _TerminalTabBar({ - required this.terminalIds, - required this.terminalNames, - required this.selectedTerminalId, - required this.projectId, - required this.connId, - required this.onSelect, + const _Header({ + required this.projectName, + required this.folderColor, + required this.terminalCount, + required this.currentPage, + required this.onCreateTerminal, + this.onCloseTerminal, }); @override Widget build(BuildContext context) { - return Container( - height: 36, - color: const Color(0xFF252526), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: terminalIds.length, - padding: const EdgeInsets.symmetric(horizontal: 4), - itemBuilder: (context, index) { - final tid = terminalIds[index]; - final isSelected = tid == selectedTerminalId; - final name = terminalNames[tid] ?? 'Terminal ${index + 1}'; + final color = _folderColorToColor(folderColor); - return GestureDetector( - onTap: () => onSelect(tid), - onLongPress: () => _showCloseDialog(context, tid, name), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF3C3C3C) - : Colors.transparent, - borderRadius: BorderRadius.circular(4), + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: const BoxDecoration( + color: OkenaColors.glassBg, + border: Border( + bottom: BorderSide(color: OkenaColors.glassStroke, width: 0.5), + ), + ), + child: Row( + children: [ + // Project avatar + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(7), + ), + alignment: Alignment.center, + child: Text( + projectName.isNotEmpty ? projectName[0].toUpperCase() : '?', + style: TextStyle( + color: color, + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), ), - alignment: Alignment.center, - child: Text( - name, - style: TextStyle( - color: isSelected ? Colors.white : Colors.white54, - fontSize: 12, - fontFamily: 'JetBrainsMono', - fontWeight: - isSelected ? FontWeight.bold : FontWeight.normal, + const SizedBox(width: 10), + // Project name + chevron (tappable → opens project sheet) + Expanded( + child: GestureDetector( + onTap: () { + HapticFeedback.selectionClick(); + ProjectSheet.show(context); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + projectName, + style: OkenaTypography.callout.copyWith( + color: OkenaColors.textPrimary, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + const Icon( + CupertinoIcons.chevron_down, + color: OkenaColors.textTertiary, + size: 12, + ), + ], + ), ), ), - ), - ); - }, - ), - ); - } - - void _showCloseDialog( - BuildContext context, String terminalId, String name) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Close terminal'), - content: Text('Close "$name"?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(ctx).pop(); - state_ffi.closeTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - child: const Text('Close', - style: TextStyle(color: Colors.redAccent)), + // Page indicator dots (only when >1 terminal) + if (terminalCount > 1) ...[ + const SizedBox(width: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(terminalCount, (i) { + final isActive = i == currentPage; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.5), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: isActive ? 8 : 6, + height: isActive ? 8 : 6, + decoration: BoxDecoration( + color: isActive ? OkenaColors.accent : OkenaColors.textTertiary, + shape: BoxShape.circle, + ), + ), + ); + }), + ), + ], + const SizedBox(width: 8), + // Close terminal button + if (onCloseTerminal != null) + GestureDetector( + onTap: onCloseTerminal, + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: OkenaColors.surfaceElevated, + borderRadius: BorderRadius.circular(7), + ), + alignment: Alignment.center, + child: const Icon( + Icons.close_rounded, + color: OkenaColors.textTertiary, + size: 14, + ), + ), + ), + const SizedBox(width: 4), + // Create terminal button + GestureDetector( + onTap: onCreateTerminal, + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: OkenaColors.surfaceElevated, + borderRadius: BorderRadius.circular(7), + ), + alignment: Alignment.center, + child: const Icon( + Icons.add_rounded, + color: OkenaColors.textSecondary, + size: 16, + ), + ), + ), + ], ), - ], - ), - ); - } -} - -/// Small colored dot indicating connection quality. -class _ConnectionDot extends StatelessWidget { - final double secondsSinceActivity; - - const _ConnectionDot({required this.secondsSinceActivity}); - - @override - Widget build(BuildContext context) { - final Color color; - if (secondsSinceActivity < 3) { - color = Colors.green; - } else if (secondsSinceActivity < 10) { - color = Colors.orange; - } else { - color = Colors.red; - } - - return Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, + ), ), ); } diff --git a/mobile/lib/src/widgets/key_toolbar.dart b/mobile/lib/src/widgets/key_toolbar.dart index 65b99eab..af3ec3a7 100644 --- a/mobile/lib/src/widgets/key_toolbar.dart +++ b/mobile/lib/src/widgets/key_toolbar.dart @@ -1,9 +1,54 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../src/rust/api/state.dart' as state_ffi; import '../../src/rust/api/terminal.dart' as ffi; +import '../theme/app_theme.dart'; + +/// Three-state modifier cycle: inactive -> active (one-shot) -> locked (sticky). +enum ModifierState { inactive, active, locked } + +/// Shared modifier state between [KeyToolbar] and [TerminalView]. +class KeyModifiers extends ChangeNotifier { + ModifierState _ctrl = ModifierState.inactive; + ModifierState _option = ModifierState.inactive; + ModifierState _cmd = ModifierState.inactive; + + bool get ctrl => _ctrl != ModifierState.inactive; + bool get option => _option != ModifierState.inactive; + bool get cmd => _cmd != ModifierState.inactive; + bool get hasAny => ctrl || option || cmd; + + ModifierState get ctrlState => _ctrl; + ModifierState get optionState => _option; + ModifierState get cmdState => _cmd; + + /// Cycle: inactive -> active -> locked -> inactive. + void toggleCtrl() { _ctrl = _nextState(_ctrl); notifyListeners(); } + void toggleOption() { _option = _nextState(_option); notifyListeners(); } + void toggleCmd() { _cmd = _nextState(_cmd); notifyListeners(); } + + static ModifierState _nextState(ModifierState s) => switch (s) { + ModifierState.inactive => ModifierState.active, + ModifierState.active => ModifierState.locked, + ModifierState.locked => ModifierState.inactive, + }; + + /// Reset only one-shot (active) modifiers; locked ones persist. + void reset() { + final changed = _ctrl == ModifierState.active || + _option == ModifierState.active || + _cmd == ModifierState.active; + if (!changed) return; + if (_ctrl == ModifierState.active) _ctrl = ModifierState.inactive; + if (_option == ModifierState.active) _option = ModifierState.inactive; + if (_cmd == ModifierState.active) _cmd = ModifierState.inactive; + notifyListeners(); + } +} const _kComposeHistoryKey = 'compose_history'; const _kMaxHistory = 30; @@ -11,11 +56,13 @@ const _kMaxHistory = 30; class KeyToolbar extends StatefulWidget { final String connId; final String? terminalId; + final KeyModifiers modifiers; const KeyToolbar({ super.key, required this.connId, this.terminalId, + required this.modifiers, }); @override @@ -23,8 +70,15 @@ class KeyToolbar extends StatefulWidget { } class _KeyToolbarState extends State { - bool _ctrlActive = false; - bool _altActive = false; + KeyModifiers get _mod => widget.modifiers; + + // Arrow key name -> xterm suffix character + static const _arrowChar = { + 'ArrowUp': 'A', + 'ArrowDown': 'B', + 'ArrowRight': 'C', + 'ArrowLeft': 'D', + }; // Compose history List _composeHistory = []; @@ -32,9 +86,29 @@ class _KeyToolbarState extends State { @override void initState() { super.initState(); + _mod.addListener(_onModChanged); _loadComposeHistory(); } + @override + void didUpdateWidget(KeyToolbar old) { + super.didUpdateWidget(old); + if (old.modifiers != widget.modifiers) { + old.modifiers.removeListener(_onModChanged); + widget.modifiers.addListener(_onModChanged); + } + } + + @override + void dispose() { + _mod.removeListener(_onModChanged); + super.dispose(); + } + + void _onModChanged() { + if (mounted) setState(() {}); + } + Future _loadComposeHistory() async { final prefs = await SharedPreferences.getInstance(); final history = prefs.getStringList(_kComposeHistoryKey); @@ -77,143 +151,64 @@ class _KeyToolbarState extends State { ); } - void _sendCtrlChar(String letter) { - final code = letter.toLowerCase().codeUnitAt(0); - if (code >= 0x61 && code <= 0x7A) { - _sendText(String.fromCharCode(code - 0x60)); - } - } - - void _onCtrlTap() { - HapticFeedback.lightImpact(); - setState(() => _ctrlActive = !_ctrlActive); - } - - void _onAltTap() { - HapticFeedback.lightImpact(); - setState(() => _altActive = !_altActive); - } - - void _handleKey(String key) { - if (_ctrlActive) { - if (key.length == 1) { - final code = key.codeUnitAt(0); + /// Send a character key, applying any active modifiers. + void _sendCharKey(String char) { + if (_mod.hasAny) { + if (_mod.ctrl) { + final code = char.codeUnitAt(0); if (code >= 0x61 && code <= 0x7A) { _sendText(String.fromCharCode(code - 0x60)); } else if (code >= 0x41 && code <= 0x5A) { _sendText(String.fromCharCode(code - 0x40)); + } else { + _sendText(char); } + } else { + // Option/Cmd: ESC prefix + _sendText('\x1b$char'); } - setState(() => _ctrlActive = false); + _mod.reset(); } else { - _sendSpecialKey(key); + _sendText(char); } - if (_altActive) { - setState(() => _altActive = false); + } + + /// Handle arrow from joystick, respecting modifier state. + void _handleArrow(String key) { + final arrow = _arrowChar[key]; + + if (arrow != null && _mod.hasAny) { + if (_mod.cmd && !_mod.ctrl && !_mod.option) { + switch (key) { + case 'ArrowLeft': + _sendSpecialKey('Home'); + case 'ArrowRight': + _sendSpecialKey('End'); + case 'ArrowUp': + _sendSpecialKey('PageUp'); + case 'ArrowDown': + _sendSpecialKey('PageDown'); + } + } else { + int mod = 1; + if (_mod.ctrl) mod += 4; + if (_mod.option) mod += 2; + _sendText('\x1b[1;$mod$arrow'); + } + _mod.reset(); + } else { + _sendSpecialKey(key); + if (_mod.hasAny) _mod.reset(); } } - void _pasteFromClipboard() async { - HapticFeedback.lightImpact(); - final data = await Clipboard.getData(Clipboard.kTextPlain); + Future _paste() async { + final data = await Clipboard.getData('text/plain'); if (data?.text != null && data!.text!.isNotEmpty) { _sendText(data.text!); } } - void _showCtrlGrid() { - const shortcuts = [ - ('C', 'kill'), - ('D', 'eof'), - ('Z', 'suspend'), - ('L', 'clear'), - ('A', 'bol'), - ('E', 'eol'), - ('R', 'search'), - ('W', 'del word'), - ('U', 'del left'), - ('K', 'del right'), - ('P', 'prev'), - ('N', 'next'), - ]; - - showModalBottomSheet( - context: context, - backgroundColor: const Color(0xFF252526), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) { - return Padding( - padding: const EdgeInsets.fromLTRB(12, 16, 12, 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.only(left: 4, bottom: 12), - child: Text( - 'CTRL + ...', - style: TextStyle( - color: Colors.white54, - fontSize: 13, - fontFamily: 'JetBrainsMono', - ), - ), - ), - GridView.count( - shrinkWrap: true, - crossAxisCount: 4, - mainAxisSpacing: 6, - crossAxisSpacing: 6, - childAspectRatio: 1.6, - physics: const NeverScrollableScrollPhysics(), - children: shortcuts.map((s) { - final (letter, label) = s; - return Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - HapticFeedback.lightImpact(); - _sendCtrlChar(letter); - Navigator.of(ctx).pop(); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '^$letter', - style: const TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - fontFamily: 'JetBrainsMono', - ), - ), - const SizedBox(height: 2), - Text( - label, - style: const TextStyle( - color: Colors.white38, - fontSize: 10, - ), - ), - ], - ), - ), - ); - }).toList(), - ), - SizedBox(height: MediaQuery.of(ctx).padding.bottom), - ], - ), - ); - }, - ); - } - void _showComposeSheet() { final controller = TextEditingController(); bool sendEnter = true; @@ -222,7 +217,7 @@ class _KeyToolbarState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: const Color(0xFF252526), + backgroundColor: OkenaColors.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), @@ -288,15 +283,17 @@ class _KeyToolbarState extends State { IconButton( icon: const Icon(Icons.arrow_upward, size: 20), color: _composeHistory.isNotEmpty - ? Colors.white70 - : Colors.white24, + ? OkenaColors.textSecondary + : OkenaColors.textTertiary, onPressed: _composeHistory.isNotEmpty ? historyUp : null, tooltip: 'Previous command', visualDensity: VisualDensity.compact, ), IconButton( icon: const Icon(Icons.arrow_downward, size: 20), - color: historyIdx > 0 ? Colors.white70 : Colors.white24, + color: historyIdx > 0 + ? OkenaColors.textSecondary + : OkenaColors.textTertiary, onPressed: historyIdx >= 0 ? historyDown : null, tooltip: 'Next command', visualDensity: VisualDensity.compact, @@ -313,8 +310,8 @@ class _KeyToolbarState extends State { ), decoration: BoxDecoration( color: sendEnter - ? const Color(0xFF007ACC) - : const Color(0xFF3C3C3C), + ? OkenaColors.accent + : OkenaColors.surfaceElevated, borderRadius: BorderRadius.circular(12), ), child: Row( @@ -324,7 +321,7 @@ class _KeyToolbarState extends State { Icons.keyboard_return, size: 14, color: - sendEnter ? Colors.white : Colors.white54, + sendEnter ? Colors.white : OkenaColors.textTertiary, ), const SizedBox(width: 4), Text( @@ -332,7 +329,7 @@ class _KeyToolbarState extends State { style: TextStyle( color: sendEnter ? Colors.white - : Colors.white54, + : OkenaColors.textTertiary, fontSize: 12, fontFamily: 'JetBrainsMono', ), @@ -350,21 +347,21 @@ class _KeyToolbarState extends State { maxLines: null, minLines: 3, style: const TextStyle( - color: Colors.white, + color: OkenaColors.textPrimary, fontFamily: 'JetBrainsMono', fontSize: 14, ), decoration: InputDecoration( hintText: 'Enter command...', - hintStyle: const TextStyle(color: Colors.white38), + hintStyle: TextStyle(color: OkenaColors.textTertiary), filled: true, - fillColor: const Color(0xFF3C3C3C), + fillColor: OkenaColors.surfaceElevated, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), suffixIcon: IconButton( - icon: const Icon(Icons.send, color: Color(0xFF007ACC)), + icon: Icon(Icons.send, color: OkenaColors.accent), onPressed: submit, ), ), @@ -378,300 +375,370 @@ class _KeyToolbarState extends State { ); } - void _sendShiftTab() { - HapticFeedback.lightImpact(); - // Shift+Tab = reverse tab escape sequence - _sendText('\x1b[Z'); - } - - void _showMoreKeys() { - showModalBottomSheet( - context: context, - backgroundColor: const Color(0xFF252526), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) { - return Padding( - padding: const EdgeInsets.fromLTRB(12, 16, 12, 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.only(left: 4, bottom: 12), - child: Text( - 'More keys', - style: TextStyle( - color: Colors.white54, - fontSize: 13, - fontFamily: 'JetBrainsMono', - ), - ), - ), - GridView.count( - shrinkWrap: true, - crossAxisCount: 4, - mainAxisSpacing: 6, - crossAxisSpacing: 6, - childAspectRatio: 1.8, - physics: const NeverScrollableScrollPhysics(), + @override + Widget build(BuildContext context) { + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), + child: Container( + decoration: const BoxDecoration( + color: OkenaColors.glassBg, + border: Border( + top: BorderSide(color: OkenaColors.glassStroke, width: 0.5), + ), + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 5), + child: Row( children: [ - _buildGridKey(ctx, 'TAB', () => _sendSpecialKey('Tab')), - _buildGridKey(ctx, 'ALT', () { - _onAltTap(); - Navigator.of(ctx).pop(); - }, toggle: _altActive), - _buildGridKey(ctx, 'DEL', () => _sendSpecialKey('Delete')), - _buildGridKey(ctx, 'HOME', () => _sendSpecialKey('Home')), - _buildGridKey(ctx, 'END', () => _sendSpecialKey('End')), - _buildGridKey(ctx, 'PG\u2191', () => _sendSpecialKey('PageUp')), - _buildGridKey(ctx, 'PG\u2193', () => _sendSpecialKey('PageDown')), + // Scrollable button row + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _Key(label: 'esc', onTap: () => _sendSpecialKey('Escape')), + _ToggleKey( + label: '\u2303', + state: _mod.ctrlState, + onTap: _mod.toggleCtrl, + ), + _ToggleKey( + label: '\u2325', + state: _mod.optionState, + onTap: _mod.toggleOption, + ), + _ToggleKey( + label: '\u2318', + state: _mod.cmdState, + onTap: _mod.toggleCmd, + ), + _Key(label: 'tab', onTap: () => _sendSpecialKey('Tab')), + const SizedBox(width: 12), + _Key(label: '~', onTap: () => _sendCharKey('~')), + _Key(label: '|', onTap: () => _sendCharKey('|')), + _Key(label: '/', onTap: () => _sendCharKey('/')), + _Key(label: '-', onTap: () => _sendCharKey('-')), + const SizedBox(width: 12), + _IconKey( + icon: Icons.edit_note_rounded, + onTap: _showComposeSheet, + ), + _IconKey( + icon: Icons.content_paste_rounded, + onTap: _paste, + ), + _IconKey( + icon: Icons.keyboard_hide_rounded, + onTap: () => FocusScope.of(context).unfocus(), + ), + ], + ), + ), + ), + const SizedBox(width: 6), + // Fixed arrow joystick + _ArrowJoystick(onArrow: _handleArrow), ], ), - SizedBox(height: MediaQuery.of(ctx).padding.bottom), - ], + ), ), - ); - }, + ), + ), ); } +} - Widget _buildGridKey(BuildContext ctx, String label, VoidCallback onTap, - {bool toggle = false}) { - return Material( - color: toggle ? const Color(0xFF007ACC) : const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), +// ── Shared key widgets ───────────────────────────────────────────────── + +class _Key extends StatelessWidget { + final String label; + final VoidCallback onTap; + + const _Key({required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: GestureDetector( onTap: () { HapticFeedback.lightImpact(); onTap(); - if (!toggle) Navigator.of(ctx).pop(); }, - child: Center( + child: Container( + constraints: const BoxConstraints(minWidth: 40), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9), + decoration: BoxDecoration( + color: OkenaColors.keyBg, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: OkenaColors.keyBorder, width: 0.5), + ), + alignment: Alignment.center, child: Text( label, - style: TextStyle( - color: toggle ? Colors.white : Colors.white70, - fontSize: 14, - fontWeight: toggle ? FontWeight.bold : FontWeight.normal, - fontFamily: 'JetBrainsMono', + style: const TextStyle( + color: OkenaColors.keyText, + fontSize: 13, + fontWeight: FontWeight.w500, ), ), ), ), ); } +} + +class _IconKey extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + + const _IconKey({required this.icon, required this.onTap}); @override Widget build(BuildContext context) { - final modifierActive = _ctrlActive || _altActive; - return Container( - color: const Color(0xFF252526), - child: SafeArea( - top: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (modifierActive) - Container(height: 2, color: const Color(0xFF007ACC)), - Padding( - padding: const EdgeInsets.fromLTRB(2, 4, 2, 4), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Left: two rows of action keys (~75% width) - Expanded( - flex: 3, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row(children: [ - _buildIconKey(Icons.edit_note, _showComposeSheet), - _buildIconKey(Icons.content_paste, _pasteFromClipboard), - _buildKey('ESC', () => _sendSpecialKey('Escape')), - _buildKey('ENT', () => _sendSpecialKey('Enter')), - ]), - Row(children: [ - _buildToggleKey( - 'CTRL', _ctrlActive, - onTap: _onCtrlTap, - onLongPress: _showCtrlGrid, - ), - _buildKey('TAB', () => _sendSpecialKey('Tab')), - _buildKey('S+T', _sendShiftTab), - _buildIconKey(Icons.more_horiz, _showMoreKeys), - ]), - ], - ), - ), - // Divider - Container( - width: 1, - color: Colors.white10, - margin: const EdgeInsets.symmetric(horizontal: 3, vertical: 6), - ), - // Right: arrow d-pad (~25% width) - Expanded( - flex: 1, - child: _buildDpad(), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildDpad() { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Top row: spacer | ↑ | spacer - Row( - children: [ - const Expanded(child: SizedBox()), - Expanded(child: _buildDpadKey('\u2191', () => _handleKey('ArrowUp'))), - const Expanded(child: SizedBox()), - ], - ), - const SizedBox(height: 2), - // Bottom row: ← | ↓ | → - Row( - children: [ - Expanded(child: _buildDpadKey('\u2190', () => _handleKey('ArrowLeft'))), - Expanded(child: _buildDpadKey('\u2193', () => _handleKey('ArrowDown'))), - Expanded(child: _buildDpadKey('\u2192', () => _handleKey('ArrowRight'))), - ], + padding: const EdgeInsets.symmetric(horizontal: 2), + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + onTap(); + }, + child: Container( + constraints: const BoxConstraints(minWidth: 40), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + color: OkenaColors.keyBg, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: OkenaColors.keyBorder, width: 0.5), ), - ], + alignment: Alignment.center, + child: Icon(icon, color: OkenaColors.keyText, size: 17), + ), ), ); } +} + +class _ToggleKey extends StatelessWidget { + final String label; + final ModifierState state; + final VoidCallback onTap; + + const _ToggleKey({ + required this.label, + required this.state, + required this.onTap, + }); + + bool get _active => state != ModifierState.inactive; + bool get _locked => state == ModifierState.locked; - Widget _buildDpadKey(String label, VoidCallback onTap) { + @override + Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(1), - child: Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - height: 30, - alignment: Alignment.center, - child: Text( - label, - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - fontFamily: 'JetBrainsMono', - ), + padding: const EdgeInsets.symmetric(horizontal: 2), + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + onTap(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.easeOutCubic, + constraints: const BoxConstraints(minWidth: 40), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7), + decoration: BoxDecoration( + color: _active ? OkenaColors.accent : OkenaColors.keyBg, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: _active ? OkenaColors.accent : OkenaColors.keyBorder, + width: 0.5, ), + boxShadow: _active + ? [ + BoxShadow( + color: OkenaColors.accent.withOpacity(0.35), + blurRadius: 12, + spreadRadius: -2, + ), + ] + : null, ), - ), - ), - ); - } - - Widget _buildKey(String label, VoidCallback onTap) { - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.5, vertical: 5), - child: Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - alignment: Alignment.center, - child: Text( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( label, - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - fontFamily: 'JetBrainsMono', + style: TextStyle( + color: _active ? Colors.white : OkenaColors.keyText, + fontSize: 16, + fontWeight: _active ? FontWeight.w700 : FontWeight.w500, ), ), - ), + // Small bar indicator for locked state + AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.easeOutCubic, + width: 12, + height: 2, + margin: const EdgeInsets.only(top: 1), + decoration: BoxDecoration( + color: _locked ? Colors.white : Colors.transparent, + borderRadius: BorderRadius.circular(1), + ), + ), + ], ), ), ), ); } +} - Widget _buildIconKey(IconData icon, VoidCallback onTap) { - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.5, vertical: 5), - child: Material( - color: const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - alignment: Alignment.center, - child: Icon(icon, color: Colors.white70, size: 16), - ), - ), +// ── Arrow Joystick ───────────────────────────────────────────────────── + +class _ArrowJoystick extends StatefulWidget { + final ValueChanged onArrow; + + const _ArrowJoystick({required this.onArrow}); + + @override + State<_ArrowJoystick> createState() => _ArrowJoystickState(); +} + +class _ArrowJoystickState extends State<_ArrowJoystick> { + static const _size = 52.0; + static const _dragThreshold = 14.0; + + String? _activeDirection; + Offset _panOrigin = Offset.zero; + bool _hasMoved = false; + + void _fire(String direction) { + widget.onArrow(direction); + HapticFeedback.selectionClick(); + setState(() => _activeDirection = direction); + } + + void _onPanStart(DragStartDetails details) { + _panOrigin = details.localPosition; + _hasMoved = false; + } + + void _onPanUpdate(DragUpdateDetails details) { + final delta = details.localPosition - _panOrigin; + if (delta.distance >= _dragThreshold) { + _hasMoved = true; + final dir = delta.dx.abs() > delta.dy.abs() + ? (delta.dx > 0 ? 'ArrowRight' : 'ArrowLeft') + : (delta.dy > 0 ? 'ArrowDown' : 'ArrowUp'); + _fire(dir); + _panOrigin = details.localPosition; + } + } + + void _onPanEnd(DragEndDetails details) { + if (!_hasMoved) { + final center = const Offset(_size / 2, _size / 2); + final delta = _panOrigin - center; + if (delta.distance >= 4) { + final dir = delta.dx.abs() > delta.dy.abs() + ? (delta.dx > 0 ? 'ArrowRight' : 'ArrowLeft') + : (delta.dy > 0 ? 'ArrowDown' : 'ArrowUp'); + _fire(dir); + Future.delayed(const Duration(milliseconds: 120), () { + if (mounted) setState(() => _activeDirection = null); + }); + return; + } + } + setState(() => _activeDirection = null); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + child: Container( + width: _size, + height: _size, + decoration: BoxDecoration( + color: OkenaColors.keyBg, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: OkenaColors.keyBorder, width: 0.5), + ), + child: CustomPaint( + size: const Size(_size, _size), + painter: _JoystickPainter(_activeDirection), ), ), ); } +} - Widget _buildToggleKey( - String label, - bool active, { - required VoidCallback onTap, - VoidCallback? onLongPress, - }) { - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.5, vertical: 5), - child: Material( - color: active ? const Color(0xFF007ACC) : const Color(0xFF3C3C3C), - borderRadius: BorderRadius.circular(6), - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: onTap, - onLongPress: onLongPress, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - alignment: Alignment.center, - child: Text( - label, - style: TextStyle( - color: active ? Colors.white : Colors.white70, - fontSize: 12, - fontWeight: active ? FontWeight.bold : FontWeight.normal, - fontFamily: 'JetBrainsMono', - ), - ), - ), - ), - ), - ), +class _JoystickPainter extends CustomPainter { + final String? activeDirection; + + _JoystickPainter(this.activeDirection); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + const armLength = 12.0; + const gap = 3.0; + const tipSize = 4.0; + + const dirs = { + 'ArrowUp': Offset(0, -1), + 'ArrowDown': Offset(0, 1), + 'ArrowLeft': Offset(-1, 0), + 'ArrowRight': Offset(1, 0), + }; + + for (final entry in dirs.entries) { + final isActive = activeDirection == entry.key; + final color = isActive ? OkenaColors.accent : const Color(0x61FFFFFF); + final paint = Paint() + ..color = color + ..strokeWidth = 1.5 + ..strokeCap = StrokeCap.round; + + final d = entry.value; + final armStart = center + d * gap; + final armEnd = center + d * armLength; + + paint.style = PaintingStyle.stroke; + canvas.drawLine(armStart, armEnd, paint); + + paint.style = PaintingStyle.fill; + final path = Path(); + if (d.dy != 0) { + path.moveTo(armEnd.dx, armEnd.dy); + path.lineTo(armEnd.dx - tipSize, armEnd.dy - d.dy * tipSize); + path.lineTo(armEnd.dx + tipSize, armEnd.dy - d.dy * tipSize); + } else { + path.moveTo(armEnd.dx, armEnd.dy); + path.lineTo(armEnd.dx - d.dx * tipSize, armEnd.dy - tipSize); + path.lineTo(armEnd.dx - d.dx * tipSize, armEnd.dy + tipSize); + } + path.close(); + canvas.drawPath(path, paint); + } + + canvas.drawCircle( + center, + 1.5, + Paint()..color = const Color(0x3DFFFFFF), ); } + + @override + bool shouldRepaint(_JoystickPainter old) => + old.activeDirection != activeDirection; } diff --git a/mobile/lib/src/widgets/layout_renderer.dart b/mobile/lib/src/widgets/layout_renderer.dart new file mode 100644 index 00000000..28fa8664 --- /dev/null +++ b/mobile/lib/src/widgets/layout_renderer.dart @@ -0,0 +1,200 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../../src/rust/api/state.dart' as state_ffi; +import '../models/layout_node.dart'; +import '../theme/app_theme.dart'; +import 'key_toolbar.dart' show KeyModifiers; +import 'terminal_view.dart'; + +class LayoutRenderer extends StatelessWidget { + final String connId; + final String projectId; + final List terminalIds; + final KeyModifiers modifiers; + + const LayoutRenderer({ + super.key, + required this.connId, + required this.projectId, + required this.terminalIds, + required this.modifiers, + }); + + @override + Widget build(BuildContext context) { + final json = state_ffi.getProjectLayoutJson( + connId: connId, + projectId: projectId, + ); + + if (json != null) { + try { + final node = + LayoutNode.fromJson(jsonDecode(json) as Map); + return _buildNode(context, node); + } catch (_) { + // Fall through to fallback + } + } + + // Fallback: show first terminal + if (terminalIds.isEmpty) { + return const Center( + child: Text( + 'No terminals', + style: TextStyle(color: OkenaColors.textTertiary), + ), + ); + } + return TerminalView(connId: connId, terminalId: terminalIds.first, modifiers: modifiers); + } + + Widget _buildNode(BuildContext context, LayoutNode node) { + return switch (node) { + TerminalNode(:final terminalId) => terminalId != null + ? TerminalView(connId: connId, terminalId: terminalId, modifiers: modifiers) + : const Center( + child: + Text('Empty terminal', style: TextStyle(color: OkenaColors.textTertiary)), + ), + SplitNode(:final direction, :final sizes, :final children) => + _buildSplit(context, direction, sizes, children), + TabsNode(:final activeTab, :final children) => + _buildTabs(context, activeTab, children), + }; + } + + Widget _buildSplit( + BuildContext context, + String direction, + List sizes, + List children, + ) { + if (children.isEmpty) { + return const SizedBox.shrink(); + } + + // In portrait mode, force horizontal splits to vertical + final isPortrait = + MediaQuery.of(context).orientation == Orientation.portrait; + final isVertical = + direction == 'vertical' || (direction == 'horizontal' && isPortrait); + + final flexChildren = []; + for (int i = 0; i < children.length; i++) { + final flex = i < sizes.length ? sizes[i].round().clamp(1, 1000) : 1; + if (i > 0) { + flexChildren.add( + isVertical + ? const Divider( + height: 2, thickness: 2, color: OkenaColors.borderLight) + : const VerticalDivider( + width: 2, thickness: 2, color: OkenaColors.borderLight), + ); + } + flexChildren.add( + Expanded(flex: flex, child: _buildNode(context, children[i])), + ); + } + + return Flex( + direction: isVertical ? Axis.vertical : Axis.horizontal, + children: flexChildren, + ); + } + + Widget _buildTabs( + BuildContext context, + int activeTab, + List children, + ) { + if (children.isEmpty) { + return const SizedBox.shrink(); + } + + return _TabsWidget( + initialTab: activeTab.clamp(0, children.length - 1), + children: children, + builder: (node) => _buildNode(context, node), + ); + } +} + +class _TabsWidget extends StatefulWidget { + final int initialTab; + final List children; + final Widget Function(LayoutNode) builder; + + const _TabsWidget({ + required this.initialTab, + required this.children, + required this.builder, + }); + + @override + State<_TabsWidget> createState() => _TabsWidgetState(); +} + +class _TabsWidgetState extends State<_TabsWidget> { + late int _activeTab; + + @override + void initState() { + super.initState(); + _activeTab = widget.initialTab; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 32, + color: OkenaColors.surfaceElevated, + child: Row( + children: [ + for (int i = 0; i < widget.children.length; i++) + GestureDetector( + onTap: () => setState(() => _activeTab = i), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: i == _activeTab + ? OkenaColors.accent + : Colors.transparent, + width: 2, + ), + ), + ), + child: Text( + _tabLabel(widget.children[i], i), + style: TextStyle( + color: i == _activeTab ? OkenaColors.textPrimary : OkenaColors.textSecondary, + fontSize: 12, + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: widget.builder(widget.children[_activeTab]), + ), + ], + ); + } + + String _tabLabel(LayoutNode node, int index) { + if (node is TerminalNode && node.terminalId != null) { + final id = node.terminalId!; + return id.length > 6 ? '...${id.substring(id.length - 6)}' : id; + } + return 'Tab ${index + 1}'; + } +} diff --git a/mobile/lib/src/widgets/project_drawer.dart b/mobile/lib/src/widgets/project_drawer.dart index 5383fc2c..4ac218ba 100644 --- a/mobile/lib/src/widgets/project_drawer.dart +++ b/mobile/lib/src/widgets/project_drawer.dart @@ -1,196 +1,391 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../providers/connection_provider.dart'; import '../providers/workspace_provider.dart'; import '../rust/api/state.dart' as state_ffi; +import '../theme/app_theme.dart'; import 'status_indicator.dart'; -class ProjectDrawer extends StatelessWidget { - const ProjectDrawer({super.key}); +Color _folderColorToColor(String colorName) { + return switch (colorName) { + 'red' => const Color(0xFFEF4444), + 'orange' => const Color(0xFFF97316), + 'yellow' => const Color(0xFFEAB308), + 'lime' => const Color(0xFF84CC16), + 'green' => const Color(0xFF22C55E), + 'teal' => const Color(0xFF14B8A6), + 'cyan' => const Color(0xFF06B6D4), + 'blue' => const Color(0xFF3B82F6), + 'indigo' => const Color(0xFF6366F1), + 'purple' => const Color(0xFFA855F7), + 'pink' => const Color(0xFFEC4899), + _ => OkenaColors.textTertiary, + }; +} + +/// Bottom sheet for project selection (replaces old Drawer). +class ProjectSheet extends StatelessWidget { + const ProjectSheet({super.key}); + + static void show(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 0.9, + snap: true, + snapSizes: const [0.3, 0.6, 0.9], + builder: (context, scrollController) => _SheetContent( + scrollController: scrollController, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} + +class _SheetContent extends StatelessWidget { + final ScrollController scrollController; + + const _SheetContent({required this.scrollController}); @override Widget build(BuildContext context) { final workspace = context.watch(); final connection = context.watch(); - return Drawer( + return Container( + decoration: const BoxDecoration( + color: OkenaColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), child: Column( children: [ - DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, + // Drag handle + Center( + child: Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(top: 10, bottom: 16), + decoration: BoxDecoration( + color: OkenaColors.textTertiary.withOpacity(0.4), + borderRadius: BorderRadius.circular(2), + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + // Header + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), + child: Row( children: [ - Text( - 'Okena', - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 4), - if (connection.activeServer != null) - Text( - connection.activeServer!.displayName, - style: Theme.of(context).textTheme.bodySmall, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Projects', style: OkenaTypography.title), + if (connection.activeServer != null) ...[ + const SizedBox(height: 3), + Text( + connection.activeServer!.displayName, + style: OkenaTypography.caption2.copyWith( + color: OkenaColors.textTertiary, + ), + ), + ], + ], ), - const Spacer(), + ), StatusIndicator(status: connection.status), ], ), ), + // Divider + Container( + height: 0.5, + color: OkenaColors.border, + ), + // Project list Expanded( - child: ListView.builder( - itemCount: workspace.projects.length, - itemBuilder: (context, index) { - final project = workspace.projects[index]; - final isSelected = project.id == workspace.selectedProjectId; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: Icon( - Icons.folder, - color: isSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - title: Text(project.name), - subtitle: Text( - project.path, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, + child: _ProjectList( + workspace: workspace, + scrollController: scrollController, + ), + ), + // Disconnect footer + Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: OkenaColors.border, width: 0.5), + ), + ), + child: SafeArea( + top: false, + child: InkWell( + onTap: () { + HapticFeedback.mediumImpact(); + Navigator.of(context).pop(); + connection.disconnect(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + children: [ + Icon( + Icons.link_off_rounded, + color: OkenaColors.error.withOpacity(0.7), + size: 16, ), - selected: isSelected, - onTap: () { - workspace.selectProject(project.id); - }, - ), - if (isSelected) ...[ - ...project.terminalIds.asMap().entries.map((entry) { - final idx = entry.key; - final tid = entry.value; - final isTerminalSelected = - tid == workspace.selectedTerminalId; - final name = - project.terminalNames[tid] ?? 'Terminal ${idx + 1}'; - return ListTile( - contentPadding: - const EdgeInsets.only(left: 56, right: 16), - leading: Icon( - Icons.terminal, - size: 20, - color: isTerminalSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - title: Text( - name, - style: TextStyle( - fontSize: 14, - color: isTerminalSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - ), - selected: isTerminalSelected, - dense: true, - onTap: () { - workspace.selectTerminal(tid); - Navigator.of(context).pop(); - }, - onLongPress: () { - _showCloseDialog( - context, - connId: connection.connId!, - projectId: project.id, - terminalId: tid, - name: name, - ); - }, - ); - }), - if (connection.connId != null) - ListTile( - contentPadding: - const EdgeInsets.only(left: 56, right: 16), - leading: Icon( - Icons.add, - size: 20, - color: - Theme.of(context).colorScheme.onSurfaceVariant, - ), - title: Text( - 'New Terminal', - style: TextStyle( - fontSize: 14, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - dense: true, - onTap: () { - state_ffi.createTerminal( - connId: connection.connId!, - projectId: project.id, - ); - Navigator.of(context).pop(); - }, + const SizedBox(width: 10), + Text( + 'Disconnect', + style: OkenaTypography.callout.copyWith( + color: OkenaColors.error.withOpacity(0.8), ), + ), ], - ], - ); - }, + ), + ), + ), ), ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.link_off), - title: const Text('Disconnect'), - onTap: () { - Navigator.of(context).pop(); - connection.disconnect(); - }, - ), ], ), ); } +} - void _showCloseDialog( - BuildContext context, { - required String connId, - required String projectId, - required String terminalId, - required String name, - }) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Close terminal'), - content: Text('Close "$name"?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), +// ── Project list with folder grouping ────────────────────────────── + +class _ProjectList extends StatelessWidget { + final WorkspaceProvider workspace; + final ScrollController scrollController; + + const _ProjectList({ + required this.workspace, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + final projectMap = {for (final p in workspace.projects) p.id: p}; + final folderMap = {for (final f in workspace.folders) f.id: f}; + final projectOrder = workspace.projectOrder; + + // If no project_order, fall back to flat list + if (projectOrder.isEmpty) { + return ListView.builder( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + itemCount: workspace.projects.length, + itemBuilder: (context, index) { + final project = workspace.projects[index]; + return _buildProjectTile(context, project); + }, + ); + } + + // Build ordered items: folders expand to header + children, standalone projects inline + final widgets = []; + for (final id in projectOrder) { + final folder = folderMap[id]; + if (folder != null) { + // Folder header + widgets.add(_FolderHeader( + name: folder.name, + folderColor: _folderColorToColor(folder.folderColor), + )); + // Folder's projects + for (final pid in folder.projectIds) { + final project = projectMap[pid]; + if (project != null) { + widgets.add(Padding( + padding: const EdgeInsets.only(left: 16), + child: _buildProjectTile(context, project), + )); + } + } + } else { + final project = projectMap[id]; + if (project != null) { + widgets.add(_buildProjectTile(context, project)); + } + } + } + + return ListView( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + children: widgets, + ); + } + + Widget _buildProjectTile(BuildContext context, state_ffi.ProjectInfo project) { + final isSelected = project.id == workspace.selectedProjectId; + return _ProjectTile( + name: project.name, + path: project.path, + isSelected: isSelected, + folderColor: _folderColorToColor(project.folderColor), + onTap: () { + HapticFeedback.selectionClick(); + workspace.selectProject(project.id); + Navigator.of(context).pop(); + }, + ); + } +} + +// ── Folder header ────────────────────────────────────────────────── + +class _FolderHeader extends StatelessWidget { + final String name; + final Color folderColor; + + const _FolderHeader({ + required this.name, + required this.folderColor, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 14, 12, 6), + child: Row( + children: [ + Container( + width: 7, + height: 7, + decoration: BoxDecoration( + color: folderColor == OkenaColors.textTertiary + ? OkenaColors.textTertiary + : folderColor.withOpacity(0.8), + shape: BoxShape.circle, + ), ), - TextButton( - onPressed: () { - Navigator.of(ctx).pop(); // dialog - Navigator.of(context).pop(); // drawer - state_ffi.closeTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - child: const Text('Close', - style: TextStyle(color: Colors.redAccent)), + const SizedBox(width: 8), + Text( + name.toUpperCase(), + style: TextStyle( + color: folderColor == OkenaColors.textTertiary + ? OkenaColors.textTertiary + : folderColor.withOpacity(0.7), + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 1.0, + ), ), ], ), ); } } + +// ── Project tile ────────────────────────────────────────────────────── + +class _ProjectTile extends StatelessWidget { + final String name; + final String path; + final bool isSelected; + final Color folderColor; + final VoidCallback onTap; + + const _ProjectTile({ + required this.name, + required this.path, + required this.isSelected, + required this.folderColor, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 1), + child: Material( + color: isSelected ? OkenaColors.surfaceOverlay : Colors.transparent, + borderRadius: BorderRadius.circular(10), + child: InkWell( + borderRadius: BorderRadius.circular(10), + splashColor: OkenaColors.accent.withOpacity(0.08), + highlightColor: OkenaColors.accent.withOpacity(0.04), + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: isSelected + ? BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border( + left: BorderSide( + color: OkenaColors.accent, + width: 3, + ), + ), + ) + : null, + child: Row( + children: [ + // Letter avatar + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: (isSelected ? OkenaColors.accent : folderColor) + .withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + ), + alignment: Alignment.center, + child: Text( + name.isNotEmpty ? name[0].toUpperCase() : '?', + style: TextStyle( + color: isSelected ? OkenaColors.accent : folderColor, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: OkenaTypography.callout.copyWith( + color: isSelected + ? OkenaColors.textPrimary + : OkenaColors.textSecondary, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + const SizedBox(height: 1), + Text( + path, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: OkenaTypography.caption2.copyWith( + fontFamily: 'JetBrainsMono', + fontSize: 10, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/src/widgets/status_indicator.dart b/mobile/lib/src/widgets/status_indicator.dart index 65e2aaf3..a1234772 100644 --- a/mobile/lib/src/widgets/status_indicator.dart +++ b/mobile/lib/src/widgets/status_indicator.dart @@ -1,42 +1,120 @@ import 'package:flutter/material.dart'; import '../../src/rust/api/connection.dart'; +import '../theme/app_theme.dart'; -class StatusIndicator extends StatelessWidget { +class StatusIndicator extends StatefulWidget { final ConnectionStatus status; const StatusIndicator({super.key, required this.status}); + @override + State createState() => _StatusIndicatorState(); +} + +class _StatusIndicatorState extends State + with SingleTickerProviderStateMixin { + late AnimationController _pulseController; + late Animation _pulseAnimation; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + _pulseAnimation = Tween(begin: 0.4, end: 1.0).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + _updatePulse(); + } + + @override + void didUpdateWidget(StatusIndicator oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.status.runtimeType != widget.status.runtimeType) { + _updatePulse(); + } + } + + void _updatePulse() { + final shouldPulse = widget.status is ConnectionStatus_Connecting || + widget.status is ConnectionStatus_Pairing; + if (shouldPulse) { + _pulseController.repeat(reverse: true); + } else { + _pulseController.stop(); + _pulseController.value = 1.0; + } + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - final (color, label) = switch (status) { - ConnectionStatus_Disconnected() => (Colors.grey, 'Disconnected'), - ConnectionStatus_Connecting() => (Colors.orange, 'Connecting'), - ConnectionStatus_Connected() => (Colors.green, 'Connected'), - ConnectionStatus_Pairing() => (Colors.blue, 'Pairing'), - ConnectionStatus_Error(:final message) => (Colors.red, 'Error: $message'), + final (color, label) = switch (widget.status) { + ConnectionStatus_Disconnected() => (OkenaColors.textTertiary, 'Disconnected'), + ConnectionStatus_Connecting() => (OkenaColors.warning, 'Connecting'), + ConnectionStatus_Connected() => (OkenaColors.success, 'Connected'), + ConnectionStatus_Pairing() => (OkenaColors.accent, 'Pairing'), + ConnectionStatus_Error(:final message) => (OkenaColors.error, 'Error: $message'), }; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, + final isConnected = widget.status is ConnectionStatus_Connected; + + return AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, + color: color.withOpacity(0.1 * _pulseAnimation.value), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: color.withOpacity(0.2 * _pulseAnimation.value), + width: 0.5, + ), ), - ), - const SizedBox(width: 6), - Flexible( - child: Text( - label, - style: TextStyle(color: color, fontSize: 12), - overflow: TextOverflow.ellipsis, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: color.withOpacity(_pulseAnimation.value), + shape: BoxShape.circle, + boxShadow: isConnected + ? [ + BoxShadow( + color: color.withOpacity(0.5), + blurRadius: 6, + spreadRadius: 1, + ), + ] + : null, + ), + ), + const SizedBox(width: 6), + Flexible( + child: Text( + label, + style: OkenaTypography.caption2.copyWith( + color: color.withOpacity(_pulseAnimation.value), + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), - ), - ], + ); + }, ); } } diff --git a/mobile/lib/src/widgets/terminal_painter.dart b/mobile/lib/src/widgets/terminal_painter.dart index f4292501..c950b654 100644 --- a/mobile/lib/src/widgets/terminal_painter.dart +++ b/mobile/lib/src/widgets/terminal_painter.dart @@ -13,6 +13,10 @@ const _kStrikethrough = 8; const _kInverse = 16; const _kDim = 32; +// Mask for flags that affect text style (excludes _kInverse which is handled +// separately when computing effective fg/bg). +const _kStyleMask = _kBold | _kItalic | _kUnderline | _kStrikethrough | _kDim; + TextDecoration flagsToDecoration(int flags) { final decorations = []; if (flags & _kUnderline != 0) decorations.add(TextDecoration.underline); @@ -37,6 +41,7 @@ class TerminalPainter extends CustomPainter { final double cellHeight; final double fontSize; final String fontFamily; + final double devicePixelRatio; final SelectionBounds? selection; final ScrollInfo? scrollInfo; @@ -49,10 +54,15 @@ class TerminalPainter extends CustomPainter { required this.cellHeight, required this.fontSize, required this.fontFamily, + required this.devicePixelRatio, this.selection, this.scrollInfo, }); + /// Snap a logical coordinate to device pixel boundaries. + double _snap(double v) => + (v * devicePixelRatio).roundToDouble() / devicePixelRatio; + bool _isCellInSelection(int col, int row, SelectionBounds sel) { // Selection bounds are in buffer coordinates; convert visual row // to buffer row for comparison: buffer_row = visual_row - display_offset @@ -83,8 +93,8 @@ class TerminalPainter extends CustomPainter { final cell = cells[i]; final col = i % cols; final row = i ~/ cols; - final x = col * cellWidth; - final y = row * cellHeight; + final x = _snap(col * cellWidth); + final y = _snap(row * cellHeight); var bgArgb = cell.bg; var fgArgb = cell.fg; @@ -96,7 +106,7 @@ class TerminalPainter extends CustomPainter { final bgColor = argbToColor(bgArgb); // Only draw non-default backgrounds - if (bgColor != TerminalTheme.bgColor && bgColor.a > 0) { + if (bgColor != OkenaColors.background && bgColor.a > 0) { bgPaint.color = bgColor; canvas.drawRect(Rect.fromLTWH(x, y, cellWidth, cellHeight), bgPaint); } @@ -108,56 +118,84 @@ class TerminalPainter extends CustomPainter { } } - // Pass 2: Text characters - for (int i = 0; i < cells.length && i < cols * rows; i++) { - final cell = cells[i]; - if (cell.character.isEmpty || cell.character == ' ') continue; + // Pass 2: Text characters — batched by style runs within each row. + // Consecutive non-space cells with the same effective fg + style flags + // are concatenated into a single TextPainter call, reducing allocations + // from ~cols*rows down to the number of distinct style runs. + for (int row = 0; row < rows; row++) { + int col = 0; + while (col < cols) { + final idx = row * cols + col; + if (idx >= cells.length) break; - final col = i % cols; - final row = i ~/ cols; - final x = col * cellWidth; - final y = row * cellHeight; + final cell = cells[idx]; + if (cell.character.isEmpty || cell.character == ' ') { + col++; + continue; + } - var fgArgb = cell.fg; - var bgArgb = cell.bg; - if (cell.flags & _kInverse != 0) { - fgArgb = bgArgb; - // Don't need bgArgb here for text painting - } + // Determine effective fg for the first cell of the run. + var fgArgb = cell.fg; + if (cell.flags & _kInverse != 0) fgArgb = cell.bg; + final styleFlags = cell.flags & _kStyleMask; - var fgColor = argbToColor(fgArgb); - if (cell.flags & _kDim != 0) { - fgColor = fgColor.withAlpha((fgColor.a * 0.5).round()); - } + final startCol = col; + final buffer = StringBuffer(); + buffer.write(cell.character); + col++; + + // Extend run with consecutive cells sharing the same style. + while (col < cols) { + final ci = row * cols + col; + if (ci >= cells.length) break; + final c = cells[ci]; + if (c.character.isEmpty || c.character == ' ') break; + + var cFg = c.fg; + if (c.flags & _kInverse != 0) cFg = c.bg; + final cStyleFlags = c.flags & _kStyleMask; + + if (cFg != fgArgb || cStyleFlags != styleFlags) break; + + buffer.write(c.character); + col++; + } - final tp = TextPainter( - text: TextSpan( - text: cell.character, - style: TextStyle( - fontFamily: fontFamily, - fontSize: fontSize, - color: fgColor, - fontWeight: - cell.flags & _kBold != 0 ? FontWeight.bold : FontWeight.normal, - fontStyle: - cell.flags & _kItalic != 0 ? FontStyle.italic : FontStyle.normal, - decoration: flagsToDecoration(cell.flags), - decorationColor: fgColor, + var fgColor = argbToColor(fgArgb); + if (styleFlags & _kDim != 0) { + fgColor = fgColor.withAlpha((fgColor.a * 0.5).round()); + } + + final tp = TextPainter( + text: TextSpan( + text: buffer.toString(), + style: TextStyle( + fontFamily: fontFamily, + fontFamilyFallback: TerminalTheme.fontFamilyFallback, + fontSize: fontSize, + color: fgColor, + fontWeight: + styleFlags & _kBold != 0 ? FontWeight.bold : FontWeight.normal, + fontStyle: + styleFlags & _kItalic != 0 ? FontStyle.italic : FontStyle.normal, + decoration: flagsToDecoration(styleFlags), + decorationColor: fgColor, + ), ), - ), - textDirection: ui.TextDirection.ltr, - )..layout(); - - // Center the character in the cell - final dx = x + (cellWidth - tp.width) / 2; - final dy = y + (cellHeight - tp.height) / 2; - tp.paint(canvas, Offset(dx, dy)); + textDirection: ui.TextDirection.ltr, + )..layout(); + + final x = _snap(startCol * cellWidth); + final y = _snap(row * cellHeight); + final dy = y + (cellHeight - tp.height) / 2; + tp.paint(canvas, Offset(x, dy)); + } } // Pass 3: Cursor if (cursor.visible && cursor.col < cols && cursor.row < rows) { - final cx = cursor.col * cellWidth; - final cy = cursor.row * cellHeight; + final cx = _snap(cursor.col * cellWidth); + final cy = _snap(cursor.row * cellHeight); final cursorPaint = Paint()..color = TerminalTheme.cursorColor; switch (cursor.shape) { @@ -214,5 +252,14 @@ class TerminalPainter extends CustomPainter { } @override - bool shouldRepaint(TerminalPainter oldDelegate) => true; + bool shouldRepaint(TerminalPainter oldDelegate) { + return !identical(cells, oldDelegate.cells) || + cursor.col != oldDelegate.cursor.col || + cursor.row != oldDelegate.cursor.row || + cursor.shape != oldDelegate.cursor.shape || + cursor.visible != oldDelegate.cursor.visible || + cols != oldDelegate.cols || + rows != oldDelegate.rows || + fontSize != oldDelegate.fontSize; + } } diff --git a/mobile/lib/src/widgets/terminal_view.dart b/mobile/lib/src/widgets/terminal_view.dart index 712873d3..4df8626a 100644 --- a/mobile/lib/src/widgets/terminal_view.dart +++ b/mobile/lib/src/widgets/terminal_view.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import '../../src/rust/api/terminal.dart' as ffi; import '../../src/rust/api/state.dart' as state_ffi; import '../theme/app_theme.dart'; +import 'key_toolbar.dart' show KeyModifiers; import 'terminal_painter.dart'; // Sentinel buffer: keeps spaces in the TextField so backspace always has @@ -17,16 +18,13 @@ const _kSentinel = ' '; // 8 spaces class TerminalView extends StatefulWidget { final String connId; final String terminalId; - - /// Called when the user swipes horizontally to switch terminals. - /// direction: -1 = swipe right (prev), 1 = swipe left (next). - final ValueChanged? onTerminalSwipe; + final KeyModifiers modifiers; const TerminalView({ super.key, required this.connId, required this.terminalId, - this.onTerminalSwipe, + required this.modifiers, }); @override @@ -43,16 +41,14 @@ class _TerminalViewState extends State { ); int _cols = 80; int _rows = 24; - final double _fontSize = TerminalTheme.defaultFontSize; + double _fontSize = TerminalTheme.defaultFontSize; + double _baseFontSize = TerminalTheme.defaultFontSize; double _cellWidth = 0; double _cellHeight = 0; Timer? _refreshTimer; - Timer? _resizeDebounce; - // Keyboard input: TextField with its own FocusNode, delta-based tracking - late final FocusNode _inputFocusNode; - final _textController = TextEditingController(text: _kSentinel); - String _lastInputText = _kSentinel; + // Resize debounce + Timer? _resizeTimer; // Scroll state ffi.ScrollInfo _scrollInfo = const ffi.ScrollInfo( @@ -62,12 +58,25 @@ class _TerminalViewState extends State { ); double _scrollAccumulator = 0; + // Pinch-to-zoom state + final Map _pointerPositions = {}; + bool _isPinching = false; + bool _hasAutoFit = false; + bool _initialResizeSent = false; + double? _initialPinchDistance; + + // Keyboard input: TextField with its own FocusNode, delta-based tracking + late final FocusNode _inputFocusNode; + final _textController = TextEditingController(text: _kSentinel); + String _lastInputText = _kSentinel; + // Selection state bool _isSelecting = false; ffi.SelectionBounds? _selection; // Gesture tracking for scroll vs swipe disambiguation Offset? _dragStart; + Offset? _lastTapPosition; @override void initState() { @@ -84,15 +93,8 @@ class _TerminalViewState extends State { oldWidget.terminalId != widget.terminalId) { _isSelecting = false; _selection = null; - // Resize first so the grid matches the mobile viewport before fetching cells - if (_cols > 0 && _rows > 0) { - ffi.resizeTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - cols: _cols, - rows: _rows, - ); - } + _hasAutoFit = false; + _initialResizeSent = false; _fetchCells(); } } @@ -100,7 +102,7 @@ class _TerminalViewState extends State { @override void dispose() { _refreshTimer?.cancel(); - _resizeDebounce?.cancel(); + _resizeTimer?.cancel(); _inputFocusNode.dispose(); _textController.dispose(); super.dispose(); @@ -112,6 +114,7 @@ class _TerminalViewState extends State { text: 'M', style: TextStyle( fontFamily: TerminalTheme.fontFamily, + fontFamilyFallback: TerminalTheme.fontFamilyFallback, fontSize: _fontSize, ), ), @@ -165,34 +168,58 @@ class _TerminalViewState extends State { void _onLayout(BoxConstraints constraints) { if (_cellWidth <= 0 || _cellHeight <= 0) return; - final newCols = (constraints.maxWidth / _cellWidth).floor().clamp(1, 500); - final newRows = (constraints.maxHeight / _cellHeight).floor().clamp(1, 200); + // Auto-fit font size once for readable ~80 column display + if (!_hasAutoFit) { + _hasAutoFit = true; + final charWidthRatio = _cellWidth / _fontSize; + _fontSize = (constraints.maxWidth / + (TerminalTheme.defaultColumns * charWidthRatio)) + .clamp(TerminalTheme.minFontSize, TerminalTheme.maxFontSize); + _baseFontSize = _fontSize; + _computeCellSize(); + } + + // Compute cols/rows that fit the mobile screen at the current font size + final newCols = + (constraints.maxWidth / _cellWidth).floor().clamp(1, 500); + final newRows = + (constraints.maxHeight / _cellHeight).floor().clamp(1, 200); if (newCols != _cols || newRows != _rows) { _cols = newCols; _rows = newRows; - _resizeDebounce?.cancel(); - _resizeDebounce = Timer(const Duration(milliseconds: 100), () { - _doResize(); - }); - // First resize is immediate so the terminal doesn't flash at wrong size - if (!_hasResized) { - _doResize(); - } - } - } - bool _hasResized = false; + // Resize local grid immediately for responsive rendering + ffi.resizeLocal( + connId: widget.connId, + terminalId: widget.terminalId, + cols: _cols, + rows: _rows, + ); - void _doResize() { - _hasResized = true; - ffi.resizeTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - cols: _cols, - rows: _rows, - ); - _fetchCells(); + if (!_initialResizeSent) { + // First resize fires immediately — no flash of garbled content + _initialResizeSent = true; + _resizeTimer?.cancel(); + ffi.resizeTerminal( + connId: widget.connId, + terminalId: widget.terminalId, + cols: _cols, + rows: _rows, + ); + } else { + // Debounce subsequent resizes to avoid spamming during layout transitions + _resizeTimer?.cancel(); + _resizeTimer = Timer(const Duration(milliseconds: 200), () { + ffi.resizeTerminal( + connId: widget.connId, + terminalId: widget.terminalId, + cols: _cols, + rows: _rows, + ); + }); + } + } } // --- Touch to cell conversion --- @@ -204,15 +231,15 @@ class _TerminalViewState extends State { // --- Scroll handling --- void _onVerticalDragUpdate(DragUpdateDetails details) { - if (_cellHeight <= 0) return; + if (_isPinching || _cellHeight <= 0) return; _scrollAccumulator += details.delta.dy; - final lines = (_scrollAccumulator / _cellHeight).truncate(); - if (lines != 0) { - _scrollAccumulator -= lines * _cellHeight; - ffi.scroll( + final lineDelta = (_scrollAccumulator / _cellHeight).truncate(); + if (lineDelta != 0) { + _scrollAccumulator -= lineDelta * _cellHeight; + ffi.scrollTerminal( connId: widget.connId, terminalId: widget.terminalId, - delta: lines, + delta: lineDelta, ); _fetchCells(); } @@ -283,8 +310,6 @@ class _TerminalViewState extends State { } } - Offset? _lastTapPosition; - void _onTapDown(TapDownDetails details) { _lastTapPosition = details.localPosition; } @@ -331,18 +356,70 @@ class _TerminalViewState extends State { }); } - // --- Horizontal swipe handling --- - void _onHorizontalDragStart(DragStartDetails details) { - _dragStart = details.localPosition; + // --- Pinch-to-zoom via raw pointer events --- + + double _computePinchDistance() { + final points = _pointerPositions.values.toList(); + if (points.length < 2) return 0; + return (points[0] - points[1]).distance; + } + + void _onPointerDown(PointerDownEvent event) { + _pointerPositions[event.pointer] = event.localPosition; + if (_pointerPositions.length == 2) { + _isPinching = true; + _baseFontSize = _fontSize; + _initialPinchDistance = _computePinchDistance(); + } + } + + void _onPointerMove(PointerMoveEvent event) { + _pointerPositions[event.pointer] = event.localPosition; + if (_pointerPositions.length >= 2 && + _initialPinchDistance != null && + _initialPinchDistance! > 0) { + final scale = _computePinchDistance() / _initialPinchDistance!; + final newSize = (_baseFontSize * scale).clamp( + TerminalTheme.minFontSize, + TerminalTheme.maxFontSize, + ); + if (newSize != _fontSize) { + setState(() { + _fontSize = newSize; + _computeCellSize(); + }); + } + } + } + + void _onPointerUp(PointerUpEvent event) { + _pointerPositions.remove(event.pointer); + if (_pointerPositions.length < 2) { + _isPinching = false; + _initialPinchDistance = null; + } } - void _onHorizontalDragEnd(DragEndDetails details) { - final velocity = details.velocity.pixelsPerSecond; - if (velocity.dx.abs() > 300 && _dragStart != null) { - final direction = velocity.dx > 0 ? -1 : 1; // right swipe = prev, left = next - widget.onTerminalSwipe?.call(direction); + void _onPointerCancel(PointerCancelEvent event) { + _pointerPositions.remove(event.pointer); + if (_pointerPositions.length < 2) { + _isPinching = false; + _initialPinchDistance = null; + } + } + + void _scrollToBottom() { + final offset = ffi.getDisplayOffset( + connId: widget.connId, + terminalId: widget.terminalId, + ); + if (offset > 0) { + ffi.scrollTerminal( + connId: widget.connId, + terminalId: widget.terminalId, + delta: -offset, + ); } - _dragStart = null; } void _resetSentinel() { @@ -352,10 +429,36 @@ class _TerminalViewState extends State { _lastInputText = _kSentinel; } + String _applyModifiers(String chars) { + final mod = widget.modifiers; + if (!mod.hasAny) return chars; + + final buf = StringBuffer(); + for (final ch in chars.codeUnits) { + if (mod.ctrl) { + // Control character: a-z → 0x01-0x1A, A-Z → 0x01-0x1A + if (ch >= 0x61 && ch <= 0x7A) { + buf.writeCharCode(ch - 0x60); + } else if (ch >= 0x41 && ch <= 0x5A) { + buf.writeCharCode(ch - 0x40); + } + } else if (mod.option || mod.cmd) { + // Meta/Option: ESC prefix + character + buf.write('\x1b'); + buf.writeCharCode(ch); + } + } + mod.reset(); + return buf.toString(); + } + void _onTextChanged(String newText) { if (newText.length > _lastInputText.length) { - // Characters added — send the delta - final delta = newText.substring(_lastInputText.length); + // Characters added — send the delta. + // Convert \n (from soft keyboard Return) to \r (terminal Enter). + var delta = newText.substring(_lastInputText.length).replaceAll('\n', '\r'); + delta = _applyModifiers(delta); + _scrollToBottom(); ffi.sendText( connId: widget.connId, terminalId: widget.terminalId, @@ -394,8 +497,6 @@ class _TerminalViewState extends State { if (key == LogicalKeyboardKey.enter) { specialKey = 'Enter'; - } else if (key == LogicalKeyboardKey.backspace) { - specialKey = 'Backspace'; } else if (key == LogicalKeyboardKey.arrowUp) { specialKey = 'ArrowUp'; } else if (key == LogicalKeyboardKey.arrowDown) { @@ -421,6 +522,7 @@ class _TerminalViewState extends State { } if (specialKey != null) { + _scrollToBottom(); state_ffi.sendSpecialKey( connId: widget.connId, terminalId: widget.terminalId, @@ -439,76 +541,75 @@ class _TerminalViewState extends State { builder: (context, constraints) { _onLayout(constraints); - return GestureDetector( - onTapDown: _onTapDown, - onTap: _onTap, - onDoubleTap: _onDoubleTap, - // Vertical drag for scrollback - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - // Long press for selection - onLongPressStart: _onLongPressStart, - onLongPressMoveUpdate: _onLongPressMoveUpdate, - onLongPressEnd: _onLongPressEnd, - // Horizontal drag for terminal switching - onHorizontalDragStart: _onHorizontalDragStart, - onHorizontalDragEnd: _onHorizontalDragEnd, - behavior: HitTestBehavior.opaque, - child: Container( - color: TerminalTheme.bgColor, - width: constraints.maxWidth, - height: constraints.maxHeight, - child: Stack( - children: [ - // Terminal canvas - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: TerminalPainter( - cells: _cells, - cursor: _cursor, - cols: _cols, - rows: _rows, - cellWidth: _cellWidth, - cellHeight: _cellHeight, - fontSize: _fontSize, - fontFamily: TerminalTheme.fontFamily, - selection: _selection, - scrollInfo: _scrollInfo, + return Listener( + onPointerDown: _onPointerDown, + onPointerMove: _onPointerMove, + onPointerUp: _onPointerUp, + onPointerCancel: _onPointerCancel, + child: GestureDetector( + onTapDown: _onTapDown, + onTap: _onTap, + onDoubleTap: _onDoubleTap, + onVerticalDragUpdate: _onVerticalDragUpdate, + onVerticalDragEnd: _onVerticalDragEnd, + // Long press for selection + onLongPressStart: _onLongPressStart, + onLongPressMoveUpdate: _onLongPressMoveUpdate, + onLongPressEnd: _onLongPressEnd, + child: Container( + color: OkenaColors.background, + width: constraints.maxWidth, + height: constraints.maxHeight, + child: Stack( + children: [ + // Terminal canvas + CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: TerminalPainter( + cells: _cells, + cursor: _cursor, + cols: _cols, + rows: _rows, + cellWidth: _cellWidth, + cellHeight: _cellHeight, + fontSize: _fontSize, + fontFamily: TerminalTheme.fontFamily, + devicePixelRatio: MediaQuery.devicePixelRatioOf(context), + selection: _selection, + scrollInfo: _scrollInfo, + ), ), - ), - // Transparent text field for soft keyboard input. - // Sized 1x1 in-layout (not off-screen) so Android shows the - // keyboard. Opacity > 0 to keep IME interaction working. - Positioned( - left: 0, - bottom: 0, - width: 1, - height: 1, - child: Opacity( - opacity: 0.01, - child: TextField( - focusNode: _inputFocusNode, - controller: _textController, - autofocus: false, - enableSuggestions: false, - autocorrect: false, - showCursor: false, - enableInteractiveSelection: false, - onChanged: _onTextChanged, - keyboardType: TextInputType.text, - textInputAction: TextInputAction.none, - decoration: const InputDecoration.collapsed( - hintText: '', - ), - style: const TextStyle( - color: Colors.transparent, - fontSize: 1, - height: 1, + // Transparent text field for soft keyboard input. + // Fills the terminal area so tapping anywhere opens the + // keyboard. Opacity > 0 keeps the iOS IME connected. + Positioned.fill( + child: Opacity( + opacity: 0.01, + child: TextField( + focusNode: _inputFocusNode, + controller: _textController, + autofocus: false, + enableSuggestions: false, + autocorrect: false, + showCursor: false, + enableInteractiveSelection: false, + onChanged: _onTextChanged, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + maxLines: null, + decoration: const InputDecoration.collapsed( + hintText: '', + ), + style: const TextStyle( + color: Colors.transparent, + fontSize: 16, + height: 1, + ), ), ), ), - ), - ], + ], + ), ), ), ); From effd7dd4c8c3b8f99d8efc380cc0e94d1f045741 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:17:33 +0100 Subject: [PATCH 11/37] chore(mobile): regenerate flutter_rust_bridge bindings Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/rust/api/state.dart | 113 ++- mobile/lib/src/rust/api/terminal.dart | 151 +--- mobile/lib/src/rust/frb_generated.dart | 768 ++++++++------------- mobile/lib/src/rust/frb_generated.io.dart | 103 +-- mobile/lib/src/rust/frb_generated.web.dart | 103 +-- 5 files changed, 434 insertions(+), 804 deletions(-) diff --git a/mobile/lib/src/rust/api/state.dart b/mobile/lib/src/rust/api/state.dart index ac4924d9..cf5af845 100644 --- a/mobile/lib/src/rust/api/state.dart +++ b/mobile/lib/src/rust/api/state.dart @@ -6,8 +6,8 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `collect_layout_ids_vec` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt` +// These functions are ignored because they are not marked as `pub`: `collect_layout_ids`, `find_terminal_size`, `folder_color_to_string` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt` /// Get all projects from the cached remote state. List getProjects({required String connId}) => @@ -17,6 +17,14 @@ List getProjects({required String connId}) => String? getFocusedProjectId({required String connId}) => RustLib.instance.api.crateApiStateGetFocusedProjectId(connId: connId); +/// Get all folders from the cached remote state. +List getFolders({required String connId}) => + RustLib.instance.api.crateApiStateGetFolders(connId: connId); + +/// Get the project order from the cached remote state. +List getProjectOrder({required String connId}) => + RustLib.instance.api.crateApiStateGetProjectOrder(connId: connId); + /// Check if a terminal has unprocessed output (dirty flag). bool isDirty({required String connId, required String terminalId}) => RustLib .instance @@ -36,11 +44,33 @@ Future sendSpecialKey({ key: key, ); +/// Get a project's layout tree as JSON. +/// +/// Returns the `ApiLayoutNode` serialized as JSON, or `None` if the project +/// has no layout. Using JSON avoids complex recursive enum FRB codegen. +String? getProjectLayoutJson({ + required String connId, + required String projectId, +}) => RustLib.instance.api.crateApiStateGetProjectLayoutJson( + connId: connId, + projectId: projectId, +); + /// Get all terminal IDs from the cached remote state (flat list). List getAllTerminalIds({required String connId}) => RustLib.instance.api.crateApiStateGetAllTerminalIds(connId: connId); -/// Create a new terminal in the given project via POST /v1/actions. +/// Get the server-side terminal size from the cached state. +/// Returns (0, 0) if the terminal is not found or size is not available. +ServerTerminalSize getServerTerminalSize({ + required String connId, + required String terminalId, +}) => RustLib.instance.api.crateApiStateGetServerTerminalSize( + connId: connId, + terminalId: terminalId, +); + +/// Create a new terminal in a project. Future createTerminal({ required String connId, required String projectId, @@ -49,7 +79,7 @@ Future createTerminal({ projectId: projectId, ); -/// Close a terminal in the given project via POST /v1/actions. +/// Close a terminal in a project. Future closeTerminal({ required String connId, required String projectId, @@ -60,22 +90,62 @@ Future closeTerminal({ terminalId: terminalId, ); +/// Focus a terminal in a project. +Future focusTerminal({ + required String connId, + required String projectId, + required String terminalId, +}) => RustLib.instance.api.crateApiStateFocusTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, +); + +/// FFI-friendly folder info. +class FolderInfo { + final String id; + final String name; + final List projectIds; + final String folderColor; + + const FolderInfo({ + required this.id, + required this.name, + required this.projectIds, + required this.folderColor, + }); + + @override + int get hashCode => + id.hashCode ^ name.hashCode ^ projectIds.hashCode ^ folderColor.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FolderInfo && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + projectIds == other.projectIds && + folderColor == other.folderColor; +} + /// Flat FFI-friendly project info. class ProjectInfo { final String id; final String name; final String path; - final bool showInOverview; + final bool isVisible; final List terminalIds; - final Map terminalNames; + final String folderColor; const ProjectInfo({ required this.id, required this.name, required this.path, - required this.showInOverview, + required this.isVisible, required this.terminalIds, - required this.terminalNames, + required this.folderColor, }); @override @@ -83,9 +153,9 @@ class ProjectInfo { id.hashCode ^ name.hashCode ^ path.hashCode ^ - showInOverview.hashCode ^ + isVisible.hashCode ^ terminalIds.hashCode ^ - terminalNames.hashCode; + folderColor.hashCode; @override bool operator ==(Object other) => @@ -95,7 +165,26 @@ class ProjectInfo { id == other.id && name == other.name && path == other.path && - showInOverview == other.showInOverview && + isVisible == other.isVisible && terminalIds == other.terminalIds && - terminalNames == other.terminalNames; + folderColor == other.folderColor; +} + +/// Server terminal size returned via FFI. +class ServerTerminalSize { + final int cols; + final int rows; + + const ServerTerminalSize({required this.cols, required this.rows}); + + @override + int get hashCode => cols.hashCode ^ rows.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ServerTerminalSize && + runtimeType == other.runtimeType && + cols == other.cols && + rows == other.rows; } diff --git a/mobile/lib/src/rust/api/terminal.dart b/mobile/lib/src/rust/api/terminal.dart index 38e37931..b818b99c 100644 --- a/mobile/lib/src/rust/api/terminal.dart +++ b/mobile/lib/src/rust/api/terminal.dart @@ -6,7 +6,7 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt` /// Get the visible terminal cells for rendering. List getVisibleCells({ @@ -24,88 +24,24 @@ CursorState getCursor({required String connId, required String terminalId}) => terminalId: terminalId, ); -/// Scroll the terminal display (positive = up, negative = down). -void scroll({ +/// Scroll the terminal display by delta lines (positive = up into history, negative = down). +void scrollTerminal({ required String connId, required String terminalId, required int delta, -}) => RustLib.instance.api.crateApiTerminalScroll( +}) => RustLib.instance.api.crateApiTerminalScrollTerminal( connId: connId, terminalId: terminalId, delta: delta, ); -/// Get scroll info: total lines, visible lines, display offset. -ScrollInfo getScrollInfo({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetScrollInfo( - connId: connId, - terminalId: terminalId, -); - -/// Start a character-level selection at col/row. -void startSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalStartSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Start a word (semantic) selection at col/row. -void startWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalStartWordSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Extend the current selection to col/row. -void updateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalUpdateSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Clear the current selection. -void clearSelection({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalClearSelection( +/// Get the current scroll display offset (0 = at bottom). +int getDisplayOffset({required String connId, required String terminalId}) => + RustLib.instance.api.crateApiTerminalGetDisplayOffset( connId: connId, terminalId: terminalId, ); -/// Get the selected text, if any. -String? getSelectedText({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalGetSelectedText( - connId: connId, - terminalId: terminalId, - ); - -/// Get selection bounds for rendering. -SelectionBounds? getSelectionBounds({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetSelectionBounds( - connId: connId, - terminalId: terminalId, -); - /// Send text input to a terminal. Future sendText({ required String connId, @@ -117,8 +53,8 @@ Future sendText({ text: text, ); -/// Resize a terminal. -void resizeTerminal({ +/// Resize a terminal (local + send WS message to server). +Future resizeTerminal({ required String connId, required String terminalId, required int cols, @@ -130,6 +66,20 @@ void resizeTerminal({ rows: rows, ); +/// Resize only the local alacritty terminal — does NOT send a WS resize message to the server. +/// Used when mobile adapts to the server's terminal size. +void resizeLocal({ + required String connId, + required String terminalId, + required int cols, + required int rows, +}) => RustLib.instance.api.crateApiTerminalResizeLocal( + connId: connId, + terminalId: terminalId, + cols: cols, + rows: rows, +); + /// Cell data for FFI transfer (flat, no pointers). class CellData { /// The character in this cell. @@ -197,58 +147,3 @@ class CursorState { shape == other.shape && visible == other.visible; } - -/// Scroll info for FFI transfer. -class ScrollInfo { - final int totalLines; - final int visibleLines; - final int displayOffset; - - const ScrollInfo({ - required this.totalLines, - required this.visibleLines, - required this.displayOffset, - }); - - @override - int get hashCode => - totalLines.hashCode ^ visibleLines.hashCode ^ displayOffset.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ScrollInfo && - runtimeType == other.runtimeType && - totalLines == other.totalLines && - visibleLines == other.visibleLines && - displayOffset == other.displayOffset; -} - -/// Selection bounds for FFI transfer. -class SelectionBounds { - final int startCol; - final int startRow; - final int endCol; - final int endRow; - - const SelectionBounds({ - required this.startCol, - required this.startRow, - required this.endCol, - required this.endRow, - }); - - @override - int get hashCode => - startCol.hashCode ^ startRow.hashCode ^ endCol.hashCode ^ endRow.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SelectionBounds && - runtimeType == other.runtimeType && - startCol == other.startCol && - startRow == other.startRow && - endCol == other.endCol && - endRow == other.endRow; -} diff --git a/mobile/lib/src/rust/frb_generated.dart b/mobile/lib/src/rust/frb_generated.dart index bf15aa8d..04e4842d 100644 --- a/mobile/lib/src/rust/frb_generated.dart +++ b/mobile/lib/src/rust/frb_generated.dart @@ -68,7 +68,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => -1973712882; + int get rustContentHash => 2126369707; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -79,22 +79,13 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { - void crateApiTerminalClearSelection({ - required String connId, - required String terminalId, - }); - Future crateApiStateCloseTerminal({ required String connId, required String projectId, required String terminalId, }); - String crateApiConnectionConnect({ - required String host, - required int port, - String? savedToken, - }); + String crateApiConnectionConnect({required String host, required int port}); ConnectionStatus crateApiConnectionConnectionStatus({required String connId}); @@ -105,6 +96,12 @@ abstract class RustLibApi extends BaseApi { void crateApiConnectionDisconnect({required String connId}); + Future crateApiStateFocusTerminal({ + required String connId, + required String projectId, + required String terminalId, + }); + List crateApiStateGetAllTerminalIds({required String connId}); CursorState crateApiTerminalGetCursor({ @@ -112,27 +109,29 @@ abstract class RustLibApi extends BaseApi { required String terminalId, }); - String? crateApiStateGetFocusedProjectId({required String connId}); - - List crateApiStateGetProjects({required String connId}); - - ScrollInfo crateApiTerminalGetScrollInfo({ + int crateApiTerminalGetDisplayOffset({ required String connId, required String terminalId, }); - String? crateApiTerminalGetSelectedText({ + String? crateApiStateGetFocusedProjectId({required String connId}); + + List crateApiStateGetFolders({required String connId}); + + String? crateApiStateGetProjectLayoutJson({ required String connId, - required String terminalId, + required String projectId, }); - SelectionBounds? crateApiTerminalGetSelectionBounds({ + List crateApiStateGetProjectOrder({required String connId}); + + List crateApiStateGetProjects({required String connId}); + + ServerTerminalSize crateApiStateGetServerTerminalSize({ required String connId, required String terminalId, }); - String? crateApiConnectionGetToken({required String connId}); - List crateApiTerminalGetVisibleCells({ required String connId, required String terminalId, @@ -150,20 +149,25 @@ abstract class RustLibApi extends BaseApi { required String code, }); - void crateApiTerminalResizeTerminal({ + void crateApiTerminalResizeLocal({ required String connId, required String terminalId, required int cols, required int rows, }); - void crateApiTerminalScroll({ + Future crateApiTerminalResizeTerminal({ required String connId, required String terminalId, - required int delta, + required int cols, + required int rows, }); - double crateApiConnectionSecondsSinceActivity({required String connId}); + void crateApiTerminalScrollTerminal({ + required String connId, + required String terminalId, + required int delta, + }); Future crateApiStateSendSpecialKey({ required String connId, @@ -176,27 +180,6 @@ abstract class RustLibApi extends BaseApi { required String terminalId, required String text, }); - - void crateApiTerminalStartSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); - - void crateApiTerminalStartWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); - - void crateApiTerminalUpdateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); } class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @@ -207,36 +190,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required super.portManager, }); - @override - void crateApiTerminalClearSelection({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalClearSelectionConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalClearSelectionConstMeta => - const TaskConstMeta( - debugName: "clear_selection", - argNames: ["connId", "terminalId"], - ); - @override Future crateApiStateCloseTerminal({ required String connId, @@ -253,7 +206,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 2, + funcId: 1, port: port_, ); }, @@ -274,35 +227,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - String crateApiConnectionConnect({ - required String host, - required int port, - String? savedToken, - }) { + String crateApiConnectionConnect({required String host, required int port}) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(host, serializer); sse_encode_u_16(port, serializer); - sse_encode_opt_String(savedToken, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, decodeErrorData: null, ), constMeta: kCrateApiConnectionConnectConstMeta, - argValues: [host, port, savedToken], + argValues: [host, port], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiConnectionConnectConstMeta => const TaskConstMeta( - debugName: "connect", - argNames: ["host", "port", "savedToken"], - ); + TaskConstMeta get kCrateApiConnectionConnectConstMeta => + const TaskConstMeta(debugName: "connect", argNames: ["host", "port"]); @override ConnectionStatus crateApiConnectionConnectionStatus({ @@ -313,7 +259,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; }, codec: SseCodec( decodeSuccessData: sse_decode_connection_status, @@ -343,7 +289,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 5, + funcId: 4, port: port_, ); }, @@ -371,7 +317,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -387,6 +333,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiConnectionDisconnectConstMeta => const TaskConstMeta(debugName: "disconnect", argNames: ["connId"]); + @override + Future crateApiStateFocusTerminal({ + required String connId, + required String projectId, + required String terminalId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(terminalId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 6, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateFocusTerminalConstMeta, + argValues: [connId, projectId, terminalId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateFocusTerminalConstMeta => const TaskConstMeta( + debugName: "focus_terminal", + argNames: ["connId", "projectId", "terminalId"], + ); + @override List crateApiStateGetAllTerminalIds({required String connId}) { return handler.executeSync( @@ -443,33 +425,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - String? crateApiStateGetFocusedProjectId({required String connId}) { + int crateApiTerminalGetDisplayOffset({ + required String connId, + required String terminalId, + }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, + decodeSuccessData: sse_decode_i_32, decodeErrorData: null, ), - constMeta: kCrateApiStateGetFocusedProjectIdConstMeta, - argValues: [connId], + constMeta: kCrateApiTerminalGetDisplayOffsetConstMeta, + argValues: [connId, terminalId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateGetFocusedProjectIdConstMeta => + TaskConstMeta get kCrateApiTerminalGetDisplayOffsetConstMeta => const TaskConstMeta( - debugName: "get_focused_project_id", - argNames: ["connId"], + debugName: "get_display_offset", + argNames: ["connId", "terminalId"], ); @override - List crateApiStateGetProjects({required String connId}) { + String? crateApiStateGetFocusedProjectId({required String connId}) { return handler.executeSync( SyncTask( callFfi: () { @@ -478,111 +464,100 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_list_project_info, + decodeSuccessData: sse_decode_opt_String, decodeErrorData: null, ), - constMeta: kCrateApiStateGetProjectsConstMeta, + constMeta: kCrateApiStateGetFocusedProjectIdConstMeta, argValues: [connId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateGetProjectsConstMeta => - const TaskConstMeta(debugName: "get_projects", argNames: ["connId"]); + TaskConstMeta get kCrateApiStateGetFocusedProjectIdConstMeta => + const TaskConstMeta( + debugName: "get_focused_project_id", + argNames: ["connId"], + ); @override - ScrollInfo crateApiTerminalGetScrollInfo({ - required String connId, - required String terminalId, - }) { + List crateApiStateGetFolders({required String connId}) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_scroll_info, + decodeSuccessData: sse_decode_list_folder_info, decodeErrorData: null, ), - constMeta: kCrateApiTerminalGetScrollInfoConstMeta, - argValues: [connId, terminalId], + constMeta: kCrateApiStateGetFoldersConstMeta, + argValues: [connId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalGetScrollInfoConstMeta => - const TaskConstMeta( - debugName: "get_scroll_info", - argNames: ["connId", "terminalId"], - ); + TaskConstMeta get kCrateApiStateGetFoldersConstMeta => + const TaskConstMeta(debugName: "get_folders", argNames: ["connId"]); @override - String? crateApiTerminalGetSelectedText({ + String? crateApiStateGetProjectLayoutJson({ required String connId, - required String terminalId, + required String projectId, }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); + sse_encode_String(projectId, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, decodeErrorData: null, ), - constMeta: kCrateApiTerminalGetSelectedTextConstMeta, - argValues: [connId, terminalId], + constMeta: kCrateApiStateGetProjectLayoutJsonConstMeta, + argValues: [connId, projectId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalGetSelectedTextConstMeta => + TaskConstMeta get kCrateApiStateGetProjectLayoutJsonConstMeta => const TaskConstMeta( - debugName: "get_selected_text", - argNames: ["connId", "terminalId"], + debugName: "get_project_layout_json", + argNames: ["connId", "projectId"], ); @override - SelectionBounds? crateApiTerminalGetSelectionBounds({ - required String connId, - required String terminalId, - }) { + List crateApiStateGetProjectOrder({required String connId}) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_opt_box_autoadd_selection_bounds, + decodeSuccessData: sse_decode_list_String, decodeErrorData: null, ), - constMeta: kCrateApiTerminalGetSelectionBoundsConstMeta, - argValues: [connId, terminalId], + constMeta: kCrateApiStateGetProjectOrderConstMeta, + argValues: [connId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalGetSelectionBoundsConstMeta => - const TaskConstMeta( - debugName: "get_selection_bounds", - argNames: ["connId", "terminalId"], - ); + TaskConstMeta get kCrateApiStateGetProjectOrderConstMeta => + const TaskConstMeta(debugName: "get_project_order", argNames: ["connId"]); @override - String? crateApiConnectionGetToken({required String connId}) { + List crateApiStateGetProjects({required String connId}) { return handler.executeSync( SyncTask( callFfi: () { @@ -591,21 +566,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, + decodeSuccessData: sse_decode_list_project_info, decodeErrorData: null, ), - constMeta: kCrateApiConnectionGetTokenConstMeta, + constMeta: kCrateApiStateGetProjectsConstMeta, argValues: [connId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiConnectionGetTokenConstMeta => - const TaskConstMeta(debugName: "get_token", argNames: ["connId"]); + TaskConstMeta get kCrateApiStateGetProjectsConstMeta => + const TaskConstMeta(debugName: "get_projects", argNames: ["connId"]); @override - List crateApiTerminalGetVisibleCells({ + ServerTerminalSize crateApiStateGetServerTerminalSize({ required String connId, required String terminalId, }) { @@ -617,6 +592,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(terminalId, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; }, + codec: SseCodec( + decodeSuccessData: sse_decode_server_terminal_size, + decodeErrorData: null, + ), + constMeta: kCrateApiStateGetServerTerminalSizeConstMeta, + argValues: [connId, terminalId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateGetServerTerminalSizeConstMeta => + const TaskConstMeta( + debugName: "get_server_terminal_size", + argNames: ["connId", "terminalId"], + ); + + @override + List crateApiTerminalGetVisibleCells({ + required String connId, + required String terminalId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + }, codec: SseCodec( decodeSuccessData: sse_decode_list_cell_data, decodeErrorData: null, @@ -643,7 +648,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 17, port: port_, ); }, @@ -672,7 +677,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -704,7 +709,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 19, port: port_, ); }, @@ -723,7 +728,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { const TaskConstMeta(debugName: "pair", argNames: ["connId", "code"]); @override - void crateApiTerminalResizeTerminal({ + void crateApiTerminalResizeLocal({ required String connId, required String terminalId, required int cols, @@ -737,80 +742,94 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(terminalId, serializer); sse_encode_u_16(cols, serializer); sse_encode_u_16(rows, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, decodeErrorData: null, ), - constMeta: kCrateApiTerminalResizeTerminalConstMeta, + constMeta: kCrateApiTerminalResizeLocalConstMeta, argValues: [connId, terminalId, cols, rows], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalResizeTerminalConstMeta => + TaskConstMeta get kCrateApiTerminalResizeLocalConstMeta => const TaskConstMeta( - debugName: "resize_terminal", + debugName: "resize_local", argNames: ["connId", "terminalId", "cols", "rows"], ); @override - void crateApiTerminalScroll({ + Future crateApiTerminalResizeTerminal({ required String connId, required String terminalId, - required int delta, + required int cols, + required int rows, }) { - return handler.executeSync( - SyncTask( - callFfi: () { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - sse_encode_i_32(delta, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + sse_encode_u_16(cols, serializer); + sse_encode_u_16(rows, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 21, + port: port_, + ); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, - decodeErrorData: null, + decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiTerminalScrollConstMeta, - argValues: [connId, terminalId, delta], + constMeta: kCrateApiTerminalResizeTerminalConstMeta, + argValues: [connId, terminalId, cols, rows], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalScrollConstMeta => const TaskConstMeta( - debugName: "scroll", - argNames: ["connId", "terminalId", "delta"], - ); + TaskConstMeta get kCrateApiTerminalResizeTerminalConstMeta => + const TaskConstMeta( + debugName: "resize_terminal", + argNames: ["connId", "terminalId", "cols", "rows"], + ); @override - double crateApiConnectionSecondsSinceActivity({required String connId}) { + void crateApiTerminalScrollTerminal({ + required String connId, + required String terminalId, + required int delta, + }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + sse_encode_String(terminalId, serializer); + sse_encode_i_32(delta, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_f_64, + decodeSuccessData: sse_decode_unit, decodeErrorData: null, ), - constMeta: kCrateApiConnectionSecondsSinceActivityConstMeta, - argValues: [connId], + constMeta: kCrateApiTerminalScrollTerminalConstMeta, + argValues: [connId, terminalId, delta], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiConnectionSecondsSinceActivityConstMeta => + TaskConstMeta get kCrateApiTerminalScrollTerminalConstMeta => const TaskConstMeta( - debugName: "seconds_since_activity", - argNames: ["connId"], + debugName: "scroll_terminal", + argNames: ["connId", "terminalId", "delta"], ); @override @@ -829,7 +848,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 23, port: port_, ); }, @@ -866,7 +885,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 24, port: port_, ); }, @@ -886,124 +905,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["connId", "terminalId", "text"], ); - @override - void crateApiTerminalStartSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalStartSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalStartSelectionConstMeta => - const TaskConstMeta( - debugName: "start_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @override - void crateApiTerminalStartWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalStartWordSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalStartWordSelectionConstMeta => - const TaskConstMeta( - debugName: "start_word_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @override - void crateApiTerminalUpdateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalUpdateSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalUpdateSelectionConstMeta => - const TaskConstMeta( - debugName: "update_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - @protected AnyhowException dco_decode_AnyhowException(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return AnyhowException(raw as String); } - @protected - Map dco_decode_Map_String_String_None(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return Map.fromEntries( - dco_decode_list_record_string_string( - raw, - ).map((e) => MapEntry(e.$1, e.$2)), - ); - } - @protected String dco_decode_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1016,12 +923,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as bool; } - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_selection_bounds(raw); - } - @protected CellData dco_decode_cell_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1076,9 +977,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - double dco_decode_f_64(dynamic raw) { + FolderInfo dco_decode_folder_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as double; + final arr = raw as List; + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + return FolderInfo( + id: dco_decode_String(arr[0]), + name: dco_decode_String(arr[1]), + projectIds: dco_decode_list_String(arr[2]), + folderColor: dco_decode_String(arr[3]), + ); } @protected @@ -1100,21 +1009,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + List dco_decode_list_folder_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Uint8List; + return (raw as List).map(dco_decode_folder_info).toList(); } @protected - List dco_decode_list_project_info(dynamic raw) { + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_project_info).toList(); + return raw as Uint8List; } @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw) { + List dco_decode_list_project_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_record_string_string).toList(); + return (raw as List).map(dco_decode_project_info).toList(); } @protected @@ -1123,12 +1032,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_String(raw); } - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_box_autoadd_selection_bounds(raw); - } - @protected ProjectInfo dco_decode_project_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1139,46 +1042,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { id: dco_decode_String(arr[0]), name: dco_decode_String(arr[1]), path: dco_decode_String(arr[2]), - showInOverview: dco_decode_bool(arr[3]), + isVisible: dco_decode_bool(arr[3]), terminalIds: dco_decode_list_String(arr[4]), - terminalNames: dco_decode_Map_String_String_None(arr[5]), + folderColor: dco_decode_String(arr[5]), ); } @protected - (String, String) dco_decode_record_string_string(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 2) { - throw Exception('Expected 2 elements, got ${arr.length}'); - } - return (dco_decode_String(arr[0]), dco_decode_String(arr[1])); - } - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw) { + ServerTerminalSize dco_decode_server_terminal_size(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 3) - throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); - return ScrollInfo( - totalLines: dco_decode_u_32(arr[0]), - visibleLines: dco_decode_u_32(arr[1]), - displayOffset: dco_decode_u_32(arr[2]), - ); - } - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return SelectionBounds( - startCol: dco_decode_u_16(arr[0]), - startRow: dco_decode_i_32(arr[1]), - endCol: dco_decode_u_16(arr[2]), - endRow: dco_decode_i_32(arr[3]), + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return ServerTerminalSize( + cols: dco_decode_u_16(arr[0]), + rows: dco_decode_u_16(arr[1]), ); } @@ -1213,15 +1091,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return AnyhowException(inner); } - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_list_record_string_string(deserializer); - return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); - } - @protected String sse_decode_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1235,14 +1104,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8() != 0; } - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_selection_bounds(deserializer)); - } - @protected CellData sse_decode_cell_data(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1303,9 +1164,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - double sse_decode_f_64(SseDeserializer deserializer) { + FolderInfo sse_decode_folder_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getFloat64(); + var var_id = sse_decode_String(deserializer); + var var_name = sse_decode_String(deserializer); + var var_projectIds = sse_decode_list_String(deserializer); + var var_folderColor = sse_decode_String(deserializer); + return FolderInfo( + id: var_id, + name: var_name, + projectIds: var_projectIds, + folderColor: var_folderColor, + ); } @protected @@ -1339,34 +1209,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { + List sse_decode_list_folder_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getUint8List(len_); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_folder_info(deserializer)); + } + return ans_; } @protected - List sse_decode_list_project_info(SseDeserializer deserializer) { + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_project_info(deserializer)); - } - return ans_; + return deserializer.buffer.getUint8List(len_); } @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ) { + List sse_decode_list_project_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var len_ = sse_decode_i_32(deserializer); - var ans_ = <(String, String)>[]; + var ans_ = []; for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_record_string_string(deserializer)); + ans_.add(sse_decode_project_info(deserializer)); } return ans_; } @@ -1382,74 +1250,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_selection_bounds(deserializer)); - } else { - return null; - } - } - @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var var_id = sse_decode_String(deserializer); var var_name = sse_decode_String(deserializer); var var_path = sse_decode_String(deserializer); - var var_showInOverview = sse_decode_bool(deserializer); + var var_isVisible = sse_decode_bool(deserializer); var var_terminalIds = sse_decode_list_String(deserializer); - var var_terminalNames = sse_decode_Map_String_String_None(deserializer); + var var_folderColor = sse_decode_String(deserializer); return ProjectInfo( id: var_id, name: var_name, path: var_path, - showInOverview: var_showInOverview, + isVisible: var_isVisible, terminalIds: var_terminalIds, - terminalNames: var_terminalNames, + folderColor: var_folderColor, ); } @protected - (String, String) sse_decode_record_string_string( + ServerTerminalSize sse_decode_server_terminal_size( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - var var_field0 = sse_decode_String(deserializer); - var var_field1 = sse_decode_String(deserializer); - return (var_field0, var_field1); - } - - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_totalLines = sse_decode_u_32(deserializer); - var var_visibleLines = sse_decode_u_32(deserializer); - var var_displayOffset = sse_decode_u_32(deserializer); - return ScrollInfo( - totalLines: var_totalLines, - visibleLines: var_visibleLines, - displayOffset: var_displayOffset, - ); - } - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_startCol = sse_decode_u_16(deserializer); - var var_startRow = sse_decode_i_32(deserializer); - var var_endCol = sse_decode_u_16(deserializer); - var var_endRow = sse_decode_i_32(deserializer); - return SelectionBounds( - startCol: var_startCol, - startRow: var_startRow, - endCol: var_endCol, - endRow: var_endRow, - ); + var var_cols = sse_decode_u_16(deserializer); + var var_rows = sse_decode_u_16(deserializer); + return ServerTerminalSize(cols: var_cols, rows: var_rows); } @protected @@ -1484,18 +1311,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.message, serializer); } - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_record_string_string( - self.entries.map((e) => (e.key, e.value)).toList(), - serializer, - ); - } - @protected void sse_encode_String(String self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1508,15 +1323,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8(self ? 1 : 0); } - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_selection_bounds(self, serializer); - } - @protected void sse_encode_cell_data(CellData self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1563,9 +1369,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - void sse_encode_f_64(double self, SseSerializer serializer) { + void sse_encode_folder_info(FolderInfo self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putFloat64(self); + sse_encode_String(self.id, serializer); + sse_encode_String(self.name, serializer); + sse_encode_list_String(self.projectIds, serializer); + sse_encode_String(self.folderColor, serializer); } @protected @@ -1596,36 +1405,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, + void sse_encode_list_folder_info( + List self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_i_32(self.length, serializer); - serializer.buffer.putUint8List(self); + for (final item in self) { + sse_encode_folder_info(item, serializer); + } } @protected - void sse_encode_list_project_info( - List self, + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_project_info(item, serializer); - } + serializer.buffer.putUint8List(self); } @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, + void sse_encode_list_project_info( + List self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_i_32(self.length, serializer); for (final item in self) { - sse_encode_record_string_string(item, serializer); + sse_encode_project_info(item, serializer); } } @@ -1639,58 +1448,25 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_selection_bounds(self, serializer); - } - } - @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_String(self.id, serializer); sse_encode_String(self.name, serializer); sse_encode_String(self.path, serializer); - sse_encode_bool(self.showInOverview, serializer); + sse_encode_bool(self.isVisible, serializer); sse_encode_list_String(self.terminalIds, serializer); - sse_encode_Map_String_String_None(self.terminalNames, serializer); - } - - @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.$1, serializer); - sse_encode_String(self.$2, serializer); - } - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_32(self.totalLines, serializer); - sse_encode_u_32(self.visibleLines, serializer); - sse_encode_u_32(self.displayOffset, serializer); + sse_encode_String(self.folderColor, serializer); } @protected - void sse_encode_selection_bounds( - SelectionBounds self, + void sse_encode_server_terminal_size( + ServerTerminalSize self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_16(self.startCol, serializer); - sse_encode_i_32(self.startRow, serializer); - sse_encode_u_16(self.endCol, serializer); - sse_encode_i_32(self.endRow, serializer); + sse_encode_u_16(self.cols, serializer); + sse_encode_u_16(self.rows, serializer); } @protected diff --git a/mobile/lib/src/rust/frb_generated.io.dart b/mobile/lib/src/rust/frb_generated.io.dart index f6b9363a..29e6c80a 100644 --- a/mobile/lib/src/rust/frb_generated.io.dart +++ b/mobile/lib/src/rust/frb_generated.io.dart @@ -23,18 +23,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); - @protected - Map dco_decode_Map_String_String_None(dynamic raw); - @protected String dco_decode_String(dynamic raw); @protected bool dco_decode_bool(dynamic raw); - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); - @protected CellData dco_decode_cell_data(dynamic raw); @@ -48,7 +42,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { CursorState dco_decode_cursor_state(dynamic raw); @protected - double dco_decode_f_64(dynamic raw); + FolderInfo dco_decode_folder_info(dynamic raw); @protected int dco_decode_i_32(dynamic raw); @@ -60,31 +54,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List dco_decode_list_cell_data(dynamic raw); @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + List dco_decode_list_folder_info(dynamic raw); @protected - List dco_decode_list_project_info(dynamic raw); + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw); + List dco_decode_list_project_info(dynamic raw); @protected String? dco_decode_opt_String(dynamic raw); - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); - @protected ProjectInfo dco_decode_project_info(dynamic raw); @protected - (String, String) dco_decode_record_string_string(dynamic raw); - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw); - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw); + ServerTerminalSize dco_decode_server_terminal_size(dynamic raw); @protected int dco_decode_u_16(dynamic raw); @@ -101,22 +86,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ); - @protected String sse_decode_String(SseDeserializer deserializer); @protected bool sse_decode_bool(SseDeserializer deserializer); - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - @protected CellData sse_decode_cell_data(SseDeserializer deserializer); @@ -130,7 +105,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { CursorState sse_decode_cursor_state(SseDeserializer deserializer); @protected - double sse_decode_f_64(SseDeserializer deserializer); + FolderInfo sse_decode_folder_info(SseDeserializer deserializer); @protected int sse_decode_i_32(SseDeserializer deserializer); @@ -142,38 +117,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List sse_decode_list_cell_data(SseDeserializer deserializer); @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + List sse_decode_list_folder_info(SseDeserializer deserializer); @protected - List sse_decode_list_project_info(SseDeserializer deserializer); + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ); + List sse_decode_list_project_info(SseDeserializer deserializer); @protected String? sse_decode_opt_String(SseDeserializer deserializer); - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer); @protected - (String, String) sse_decode_record_string_string( + ServerTerminalSize sse_decode_server_terminal_size( SseDeserializer deserializer, ); - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); - @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -192,24 +154,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ); - @protected void sse_encode_String(String self, SseSerializer serializer); @protected void sse_encode_bool(bool self, SseSerializer serializer); - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - @protected void sse_encode_cell_data(CellData self, SseSerializer serializer); @@ -226,7 +176,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_cursor_state(CursorState self, SseSerializer serializer); @protected - void sse_encode_f_64(double self, SseSerializer serializer); + void sse_encode_folder_info(FolderInfo self, SseSerializer serializer); @protected void sse_encode_i_32(int self, SseSerializer serializer); @@ -238,47 +188,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_list_cell_data(List self, SseSerializer serializer); @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, + void sse_encode_list_folder_info( + List self, SseSerializer serializer, ); @protected - void sse_encode_list_project_info( - List self, + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer, ); @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, + void sse_encode_list_project_info( + List self, SseSerializer serializer, ); @protected void sse_encode_opt_String(String? self, SseSerializer serializer); - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ); - @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ); - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, + void sse_encode_server_terminal_size( + ServerTerminalSize self, SseSerializer serializer, ); diff --git a/mobile/lib/src/rust/frb_generated.web.dart b/mobile/lib/src/rust/frb_generated.web.dart index b0f66d8d..88bb0ee5 100644 --- a/mobile/lib/src/rust/frb_generated.web.dart +++ b/mobile/lib/src/rust/frb_generated.web.dart @@ -25,18 +25,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); - @protected - Map dco_decode_Map_String_String_None(dynamic raw); - @protected String dco_decode_String(dynamic raw); @protected bool dco_decode_bool(dynamic raw); - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); - @protected CellData dco_decode_cell_data(dynamic raw); @@ -50,7 +44,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { CursorState dco_decode_cursor_state(dynamic raw); @protected - double dco_decode_f_64(dynamic raw); + FolderInfo dco_decode_folder_info(dynamic raw); @protected int dco_decode_i_32(dynamic raw); @@ -62,31 +56,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List dco_decode_list_cell_data(dynamic raw); @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + List dco_decode_list_folder_info(dynamic raw); @protected - List dco_decode_list_project_info(dynamic raw); + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw); + List dco_decode_list_project_info(dynamic raw); @protected String? dco_decode_opt_String(dynamic raw); - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); - @protected ProjectInfo dco_decode_project_info(dynamic raw); @protected - (String, String) dco_decode_record_string_string(dynamic raw); - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw); - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw); + ServerTerminalSize dco_decode_server_terminal_size(dynamic raw); @protected int dco_decode_u_16(dynamic raw); @@ -103,22 +88,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ); - @protected String sse_decode_String(SseDeserializer deserializer); @protected bool sse_decode_bool(SseDeserializer deserializer); - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - @protected CellData sse_decode_cell_data(SseDeserializer deserializer); @@ -132,7 +107,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { CursorState sse_decode_cursor_state(SseDeserializer deserializer); @protected - double sse_decode_f_64(SseDeserializer deserializer); + FolderInfo sse_decode_folder_info(SseDeserializer deserializer); @protected int sse_decode_i_32(SseDeserializer deserializer); @@ -144,38 +119,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List sse_decode_list_cell_data(SseDeserializer deserializer); @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + List sse_decode_list_folder_info(SseDeserializer deserializer); @protected - List sse_decode_list_project_info(SseDeserializer deserializer); + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ); + List sse_decode_list_project_info(SseDeserializer deserializer); @protected String? sse_decode_opt_String(SseDeserializer deserializer); - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer); @protected - (String, String) sse_decode_record_string_string( + ServerTerminalSize sse_decode_server_terminal_size( SseDeserializer deserializer, ); - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); - @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -194,24 +156,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ); - @protected void sse_encode_String(String self, SseSerializer serializer); @protected void sse_encode_bool(bool self, SseSerializer serializer); - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - @protected void sse_encode_cell_data(CellData self, SseSerializer serializer); @@ -228,7 +178,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_cursor_state(CursorState self, SseSerializer serializer); @protected - void sse_encode_f_64(double self, SseSerializer serializer); + void sse_encode_folder_info(FolderInfo self, SseSerializer serializer); @protected void sse_encode_i_32(int self, SseSerializer serializer); @@ -240,47 +190,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_list_cell_data(List self, SseSerializer serializer); @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, + void sse_encode_list_folder_info( + List self, SseSerializer serializer, ); @protected - void sse_encode_list_project_info( - List self, + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer, ); @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, + void sse_encode_list_project_info( + List self, SseSerializer serializer, ); @protected void sse_encode_opt_String(String? self, SseSerializer serializer); - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ); - @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ); - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, + void sse_encode_server_terminal_size( + ServerTerminalSize self, SseSerializer serializer, ); From b7bff897baf6cee10e03e5ce566f58b403d45e76 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Mon, 16 Feb 2026 09:17:39 +0100 Subject: [PATCH 12/37] chore(mobile): remove google_fonts dependency and add devtools config Co-Authored-By: Claude Opus 4.6 --- mobile/devtools_options.yaml | 3 +++ mobile/pubspec.yaml | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 mobile/devtools_options.yaml diff --git a/mobile/devtools_options.yaml b/mobile/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/mobile/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 3868c624..a2bdd99d 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: freezed_annotation: ^3.1.0 provider: ^6.1.0 shared_preferences: ^2.2.0 - google_fonts: ^6.1.0 dev_dependencies: flutter_test: From e94daab72ef99289d664ebc5be63578223e9167d Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 11:10:06 +0200 Subject: [PATCH 13/37] feat(mobile): add LayoutNode sealed class model and update tests Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/models/layout_node.dart | 92 ++++++++++++++++++++++++ mobile/test/models/layout_node_test.dart | 39 +++++----- 2 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 mobile/lib/src/models/layout_node.dart diff --git a/mobile/lib/src/models/layout_node.dart b/mobile/lib/src/models/layout_node.dart new file mode 100644 index 00000000..2bfb449f --- /dev/null +++ b/mobile/lib/src/models/layout_node.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; + +/// Sealed layout node hierarchy matching ApiLayoutNode from the server. +sealed class LayoutNode { + const LayoutNode(); + + static LayoutNode? fromJson(String jsonStr) { + try { + final map = json.decode(jsonStr) as Map; + return _parse(map); + } catch (_) { + return null; + } + } + + static LayoutNode? _parse(Map map) { + final type = map['type'] as String?; + switch (type) { + case 'terminal': + return TerminalNode( + terminalId: map['terminal_id'] as String?, + minimized: map['minimized'] as bool? ?? false, + detached: map['detached'] as bool? ?? false, + ); + case 'split': + final children = (map['children'] as List?) + ?.map((c) => _parse(c as Map)) + .whereType() + .toList() ?? + []; + final sizes = (map['sizes'] as List?) + ?.map((s) => (s as num).toDouble()) + .toList() ?? + []; + return SplitNode( + direction: map['direction'] == 'vertical' + ? SplitDirection.vertical + : SplitDirection.horizontal, + sizes: sizes, + children: children, + ); + case 'tabs': + final children = (map['children'] as List?) + ?.map((c) => _parse(c as Map)) + .whereType() + .toList() ?? + []; + return TabsNode( + activeTab: map['active_tab'] as int? ?? 0, + children: children, + ); + default: + return null; + } + } +} + +class TerminalNode extends LayoutNode { + final String? terminalId; + final bool minimized; + final bool detached; + + const TerminalNode({ + this.terminalId, + this.minimized = false, + this.detached = false, + }); +} + +class SplitNode extends LayoutNode { + final SplitDirection direction; + final List sizes; + final List children; + + const SplitNode({ + required this.direction, + required this.sizes, + required this.children, + }); +} + +class TabsNode extends LayoutNode { + final int activeTab; + final List children; + + const TabsNode({ + required this.activeTab, + required this.children, + }); +} + +enum SplitDirection { horizontal, vertical } diff --git a/mobile/test/models/layout_node_test.dart b/mobile/test/models/layout_node_test.dart index 56a6fe53..71953ea6 100644 --- a/mobile/test/models/layout_node_test.dart +++ b/mobile/test/models/layout_node_test.dart @@ -1,15 +1,17 @@ +import 'dart:convert'; + import 'package:flutter_test/flutter_test.dart'; import 'package:mobile/src/models/layout_node.dart'; void main() { group('LayoutNode.fromJson', () { test('parses terminal node', () { - final node = LayoutNode.fromJson({ + final node = LayoutNode.fromJson(jsonEncode({ 'type': 'terminal', 'terminal_id': 't1', 'minimized': false, 'detached': false, - }); + })); expect(node, isA()); final t = node as TerminalNode; @@ -19,19 +21,19 @@ void main() { }); test('parses terminal node with null id', () { - final node = LayoutNode.fromJson({ + final node = LayoutNode.fromJson(jsonEncode({ 'type': 'terminal', 'terminal_id': null, 'minimized': true, 'detached': true, - }); + })); expect(node, isA()); expect((node as TerminalNode).terminalId, isNull); }); test('parses split node', () { - final node = LayoutNode.fromJson({ + final node = LayoutNode.fromJson(jsonEncode({ 'type': 'split', 'direction': 'horizontal', 'sizes': [50.0, 50.0], @@ -39,11 +41,11 @@ void main() { {'type': 'terminal', 'terminal_id': 't1', 'minimized': false, 'detached': false}, {'type': 'terminal', 'terminal_id': 't2', 'minimized': false, 'detached': false}, ], - }); + })); expect(node, isA()); final s = node as SplitNode; - expect(s.direction, 'horizontal'); + expect(s.direction, SplitDirection.horizontal); expect(s.sizes, [50.0, 50.0]); expect(s.children.length, 2); expect((s.children[0] as TerminalNode).terminalId, 't1'); @@ -51,14 +53,14 @@ void main() { }); test('parses tabs node', () { - final node = LayoutNode.fromJson({ + final node = LayoutNode.fromJson(jsonEncode({ 'type': 'tabs', 'active_tab': 1, 'children': [ {'type': 'terminal', 'terminal_id': 'a', 'minimized': false, 'detached': false}, {'type': 'terminal', 'terminal_id': 'b', 'minimized': false, 'detached': false}, ], - }); + })); expect(node, isA()); final t = node as TabsNode; @@ -67,7 +69,7 @@ void main() { }); test('parses nested split with tabs', () { - final node = LayoutNode.fromJson({ + final node = LayoutNode.fromJson(jsonEncode({ 'type': 'split', 'direction': 'vertical', 'sizes': [30.0, 70.0], @@ -82,7 +84,7 @@ void main() { ], }, ], - }); + })); expect(node, isA()); final s = node as SplitNode; @@ -92,23 +94,22 @@ void main() { expect(tabs.children.length, 2); }); - test('unknown type falls back to empty terminal', () { - final node = LayoutNode.fromJson({ + test('unknown type returns null', () { + final node = LayoutNode.fromJson(jsonEncode({ 'type': 'unknown_future_type', - }); + })); - expect(node, isA()); - expect((node as TerminalNode).terminalId, isNull); + expect(node, isNull); }); test('handles missing optional fields with defaults', () { - final node = LayoutNode.fromJson({ + final node = LayoutNode.fromJson(jsonEncode({ 'type': 'split', - }); + })); expect(node, isA()); final s = node as SplitNode; - expect(s.direction, 'horizontal'); + expect(s.direction, SplitDirection.horizontal); expect(s.sizes, isEmpty); expect(s.children, isEmpty); }); From 01674bed9ef563c41bca039b890b5903c7fc1973 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 11:10:09 +0200 Subject: [PATCH 14/37] refactor(mobile): extract send_action_with_response in ConnectionManager Co-Authored-By: Claude Opus 4.6 --- mobile/native/src/client/manager.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mobile/native/src/client/manager.rs b/mobile/native/src/client/manager.rs index 5b598e1b..23484e18 100644 --- a/mobile/native/src/client/manager.rs +++ b/mobile/native/src/client/manager.rs @@ -337,6 +337,16 @@ impl ConnectionManager { conn_id: &str, action: ActionRequest, ) -> anyhow::Result<()> { + self.send_action_with_response(conn_id, action).await?; + Ok(()) + } + + /// Send an action to the remote server and return the response body. + pub async fn send_action_with_response( + &self, + conn_id: &str, + action: ActionRequest, + ) -> anyhow::Result { let (host, port, token) = { let connections = self.connections.read(); let conn = connections @@ -365,7 +375,8 @@ impl ConnectionManager { anyhow::bail!("Action failed ({}): {}", status, body); } - Ok(()) + let body = resp.text().await.unwrap_or_default(); + Ok(body) } /// Background task that drains the event channel and updates connection state. From 70aab6970a2ad7f679131107bcd27539c7028ea1 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 11:10:13 +0200 Subject: [PATCH 15/37] feat(mobile): add fullscreen, git, services, and terminal management FFI Co-Authored-By: Claude Opus 4.6 --- mobile/native/src/api/state.rs | 455 ++++++++++++++++++++++++++++----- 1 file changed, 395 insertions(+), 60 deletions(-) diff --git a/mobile/native/src/api/state.rs b/mobile/native/src/api/state.rs index fec00000..82c57a34 100644 --- a/mobile/native/src/api/state.rs +++ b/mobile/native/src/api/state.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use crate::client::manager::ConnectionManager; -use okena_core::api::ActionRequest; +use okena_core::api::{ActionRequest, ApiLayoutNode}; use okena_core::client::{collect_state_terminal_ids, WsClientMessage}; use okena_core::keys::SpecialKey; +use okena_core::types::SplitDirection; /// Flat FFI-friendly project info. #[derive(Debug, Clone)] @@ -14,9 +15,25 @@ pub struct ProjectInfo { pub show_in_overview: bool, pub terminal_ids: Vec, pub terminal_names: HashMap, + pub git_branch: Option, + pub git_lines_added: u32, + pub git_lines_removed: u32, + pub services: Vec, pub folder_color: String, } +/// FFI-friendly service info. +#[derive(Debug, Clone)] +pub struct ServiceInfo { + pub name: String, + pub status: String, + pub terminal_id: Option, + pub ports: Vec, + pub exit_code: Option, + pub kind: String, + pub is_extra: bool, +} + /// FFI-friendly folder info. #[derive(Debug, Clone)] pub struct FolderInfo { @@ -26,11 +43,11 @@ pub struct FolderInfo { pub folder_color: String, } -/// Server terminal size returned via FFI. +/// FFI-friendly fullscreen info. #[derive(Debug, Clone)] -pub struct ServerTerminalSize { - pub cols: u16, - pub rows: u16, +pub struct FullscreenInfo { + pub project_id: String, + pub terminal_id: String, } /// Get all projects from the cached remote state. @@ -53,6 +70,25 @@ pub fn get_projects(conn_id: String) -> Vec { } else { Vec::new() }; + let (git_branch, git_lines_added, git_lines_removed) = + if let Some(ref gs) = p.git_status { + (gs.branch.clone(), gs.lines_added as u32, gs.lines_removed as u32) + } else { + (None, 0, 0) + }; + let services = p + .services + .iter() + .map(|s| ServiceInfo { + name: s.name.clone(), + status: s.status.clone(), + terminal_id: s.terminal_id.clone(), + ports: s.ports.clone(), + exit_code: s.exit_code, + kind: s.kind.clone(), + is_extra: s.is_extra, + }) + .collect(); ProjectInfo { id: p.id.clone(), name: p.name.clone(), @@ -60,7 +96,11 @@ pub fn get_projects(conn_id: String) -> Vec { show_in_overview: p.show_in_overview, terminal_ids, terminal_names: p.terminal_names.clone(), - folder_color: format!("{:?}", p.folder_color), + git_branch, + git_lines_added, + git_lines_removed, + services, + folder_color: format!("{:?}", p.folder_color).to_lowercase(), } }) .collect() @@ -74,6 +114,58 @@ pub fn get_focused_project_id(conn_id: String) -> Option { .and_then(|s| s.focused_project_id.clone()) } +/// Get folders from the cached remote state. +#[flutter_rust_bridge::frb(sync)] +pub fn get_folders(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + let state = match mgr.get_state(&conn_id) { + Some(s) => s, + None => return Vec::new(), + }; + state + .folders + .iter() + .map(|f| FolderInfo { + id: f.id.clone(), + name: f.name.clone(), + project_ids: f.project_ids.clone(), + folder_color: format!("{:?}", f.folder_color).to_lowercase(), + }) + .collect() +} + +/// Get the project order from the cached remote state. +#[flutter_rust_bridge::frb(sync)] +pub fn get_project_order(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id) + .map(|s| s.project_order.clone()) + .unwrap_or_default() +} + +/// Get fullscreen terminal info. +#[flutter_rust_bridge::frb(sync)] +pub fn get_fullscreen_terminal(conn_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id) + .and_then(|s| { + s.fullscreen_terminal.as_ref().map(|f| FullscreenInfo { + project_id: f.project_id.clone(), + terminal_id: f.terminal_id.clone(), + }) + }) +} + +/// Get layout JSON for a project. +#[flutter_rust_bridge::frb(sync)] +pub fn get_project_layout_json(conn_id: String, project_id: String) -> Option { + let mgr = ConnectionManager::get(); + let state = mgr.get_state(&conn_id)?; + let project = state.projects.iter().find(|p| p.id == project_id)?; + let layout = project.layout.as_ref()?; + serde_json::to_string(layout).ok() +} + /// Check if a terminal has unprocessed output (dirty flag). #[flutter_rust_bridge::frb(sync)] pub fn is_dirty(conn_id: String, terminal_id: String) -> bool { @@ -104,15 +196,15 @@ pub async fn send_special_key( Ok(()) } -fn collect_layout_ids_vec(node: &okena_core::api::ApiLayoutNode, ids: &mut Vec) { +fn collect_layout_ids_vec(node: &ApiLayoutNode, ids: &mut Vec) { match node { - okena_core::api::ApiLayoutNode::Terminal { terminal_id, .. } => { + ApiLayoutNode::Terminal { terminal_id, .. } => { if let Some(id) = terminal_id { ids.push(id.clone()); } } - okena_core::api::ApiLayoutNode::Split { children, .. } - | okena_core::api::ApiLayoutNode::Tabs { children, .. } => { + ApiLayoutNode::Split { children, .. } + | ApiLayoutNode::Tabs { children, .. } => { for child in children { collect_layout_ids_vec(child, ids); } @@ -130,34 +222,69 @@ pub fn get_all_terminal_ids(conn_id: String) -> Vec { } } -/// Create a new terminal in the given project via POST /v1/actions. +// ── Terminal actions ──────────────────────────────────────────────── + +/// Create a new terminal in the given project. pub async fn create_terminal(conn_id: String, project_id: String) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action(&conn_id, ActionRequest::CreateTerminal { project_id }) + .await +} + +/// Close a terminal in the given project. +pub async fn close_terminal( + conn_id: String, + project_id: String, + terminal_id: String, +) -> anyhow::Result<()> { let mgr = ConnectionManager::get(); mgr.send_action( &conn_id, - ActionRequest::CreateTerminal { project_id }, + ActionRequest::CloseTerminal { + project_id, + terminal_id, + }, ) .await } -/// Close a terminal in the given project via POST /v1/actions. -pub async fn close_terminal( +/// Close multiple terminals in a project. +pub async fn close_terminals( + conn_id: String, + project_id: String, + terminal_ids: Vec, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::CloseTerminals { + project_id, + terminal_ids, + }, + ) + .await +} + +/// Rename a terminal. +pub async fn rename_terminal( conn_id: String, project_id: String, terminal_id: String, + name: String, ) -> anyhow::Result<()> { let mgr = ConnectionManager::get(); mgr.send_action( &conn_id, - ActionRequest::CloseTerminal { + ActionRequest::RenameTerminal { project_id, terminal_id, + name, }, ) .await } -/// Focus a terminal in a project. +/// Focus a terminal. pub async fn focus_terminal( conn_id: String, project_id: String, @@ -174,55 +301,263 @@ pub async fn focus_terminal( .await } -/// Get the server-side terminal size from the cached state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_server_terminal_size(conn_id: String, terminal_id: String) -> ServerTerminalSize { +/// Toggle minimized state of a terminal. +pub async fn toggle_minimized( + conn_id: String, + project_id: String, + terminal_id: String, +) -> anyhow::Result<()> { let mgr = ConnectionManager::get(); - let state = match mgr.get_state(&conn_id) { - Some(s) => s, - None => return ServerTerminalSize { cols: 0, rows: 0 }, - }; + mgr.send_action( + &conn_id, + ActionRequest::ToggleMinimized { + project_id, + terminal_id, + }, + ) + .await +} - for project in &state.projects { - if let Some(ref layout) = project.layout { - if let Some(size) = find_terminal_size(layout, &terminal_id) { - return size; - } - } - } +/// Set/clear fullscreen terminal. +pub async fn set_fullscreen( + conn_id: String, + project_id: String, + terminal_id: Option, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::SetFullscreen { + project_id, + terminal_id, + }, + ) + .await +} - ServerTerminalSize { cols: 0, rows: 0 } +/// Split a terminal pane. +pub async fn split_terminal( + conn_id: String, + project_id: String, + path: Vec, + direction: String, +) -> anyhow::Result<()> { + let dir = match direction.as_str() { + "vertical" => SplitDirection::Vertical, + _ => SplitDirection::Horizontal, + }; + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::SplitTerminal { + project_id, + path, + direction: dir, + }, + ) + .await } -fn find_terminal_size( - node: &okena_core::api::ApiLayoutNode, - target_id: &str, -) -> Option { - match node { - okena_core::api::ApiLayoutNode::Terminal { +/// Run a command in a terminal (presses Enter automatically). +pub async fn run_command( + conn_id: String, + terminal_id: String, + command: String, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::RunCommand { terminal_id, - cols, - rows, - .. - } => { - if terminal_id.as_deref() == Some(target_id) { - match (cols, rows) { - (Some(c), Some(r)) => Some(ServerTerminalSize { cols: *c, rows: *r }), - _ => None, - } - } else { - None - } - } - okena_core::api::ApiLayoutNode::Split { children, .. } - | okena_core::api::ApiLayoutNode::Tabs { children, .. } => { - for child in children { - if let Some(size) = find_terminal_size(child, target_id) { - return Some(size); - } - } - None - } - } + command, + }, + ) + .await +} + +/// Read terminal content as text. +pub async fn read_content(conn_id: String, terminal_id: String) -> anyhow::Result { + let mgr = ConnectionManager::get(); + mgr.send_action_with_response( + &conn_id, + ActionRequest::ReadContent { terminal_id }, + ) + .await +} + +// ── Git actions ───────────────────────────────────────────────────── + +/// Get detailed git status for a project. +pub async fn git_status(conn_id: String, project_id: String) -> anyhow::Result { + let mgr = ConnectionManager::get(); + mgr.send_action_with_response( + &conn_id, + ActionRequest::GitStatus { project_id }, + ) + .await +} + +/// Get git diff summary for a project. +pub async fn git_diff_summary(conn_id: String, project_id: String) -> anyhow::Result { + let mgr = ConnectionManager::get(); + mgr.send_action_with_response( + &conn_id, + ActionRequest::GitDiffSummary { project_id }, + ) + .await +} + +/// Get git diff for a project. Mode: "working_tree", "staged". +pub async fn git_diff( + conn_id: String, + project_id: String, + mode: String, +) -> anyhow::Result { + let diff_mode = match mode.as_str() { + "staged" => okena_core::types::DiffMode::Staged, + _ => okena_core::types::DiffMode::WorkingTree, + }; + let mgr = ConnectionManager::get(); + mgr.send_action_with_response( + &conn_id, + ActionRequest::GitDiff { + project_id, + mode: diff_mode, + ignore_whitespace: false, + }, + ) + .await +} + +/// Get git branches for a project. +pub async fn git_branches(conn_id: String, project_id: String) -> anyhow::Result { + let mgr = ConnectionManager::get(); + mgr.send_action_with_response( + &conn_id, + ActionRequest::GitBranches { project_id }, + ) + .await +} + +// ── Service actions ───────────────────────────────────────────────── + +/// Start a service. +pub async fn start_service( + conn_id: String, + project_id: String, + service_name: String, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::StartService { + project_id, + service_name, + }, + ) + .await +} + +/// Stop a service. +pub async fn stop_service( + conn_id: String, + project_id: String, + service_name: String, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::StopService { + project_id, + service_name, + }, + ) + .await +} + +/// Restart a service. +pub async fn restart_service( + conn_id: String, + project_id: String, + service_name: String, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::RestartService { + project_id, + service_name, + }, + ) + .await +} + +/// Start all services in a project. +pub async fn start_all_services(conn_id: String, project_id: String) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action(&conn_id, ActionRequest::StartAllServices { project_id }) + .await +} + +/// Stop all services in a project. +pub async fn stop_all_services(conn_id: String, project_id: String) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action(&conn_id, ActionRequest::StopAllServices { project_id }) + .await } +/// Reload services config for a project. +pub async fn reload_services(conn_id: String, project_id: String) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action(&conn_id, ActionRequest::ReloadServices { project_id }) + .await +} + +// ── Project management ────────────────────────────────────────────── + +/// Add a new project. +pub async fn add_project(conn_id: String, name: String, path: String) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action(&conn_id, ActionRequest::AddProject { name, path }) + .await +} + +/// Set project color. +pub async fn set_project_color( + conn_id: String, + project_id: String, + color: String, +) -> anyhow::Result<()> { + let folder_color: okena_core::theme::FolderColor = + serde_json::from_value(serde_json::Value::String(color.clone())) + .unwrap_or_default(); + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::SetProjectColor { + project_id, + color: folder_color, + }, + ) + .await +} + +/// Set folder color. +pub async fn set_folder_color( + conn_id: String, + folder_id: String, + color: String, +) -> anyhow::Result<()> { + let folder_color: okena_core::theme::FolderColor = + serde_json::from_value(serde_json::Value::String(color.clone())) + .unwrap_or_default(); + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::SetFolderColor { + folder_id, + color: folder_color, + }, + ) + .await +} From d0b3a9e181d6b5b03dc6fbf4606966a9148f8c05 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 11:10:17 +0200 Subject: [PATCH 16/37] chore(mobile): regenerate flutter_rust_bridge bindings Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/rust/api/state.dart | 337 +++- mobile/lib/src/rust/frb_generated.dart | 1934 ++++++++++++++++++-- mobile/lib/src/rust/frb_generated.io.dart | 206 ++- mobile/lib/src/rust/frb_generated.web.dart | 206 ++- mobile/native/src/frb_generated.rs | 1658 +++++++++++++++-- 5 files changed, 3954 insertions(+), 387 deletions(-) diff --git a/mobile/lib/src/rust/api/state.dart b/mobile/lib/src/rust/api/state.dart index cf5af845..6855f94f 100644 --- a/mobile/lib/src/rust/api/state.dart +++ b/mobile/lib/src/rust/api/state.dart @@ -6,8 +6,8 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `collect_layout_ids`, `find_terminal_size`, `folder_color_to_string` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt` +// These functions are ignored because they are not marked as `pub`: `collect_layout_ids_vec` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt` /// Get all projects from the cached remote state. List getProjects({required String connId}) => @@ -17,7 +17,7 @@ List getProjects({required String connId}) => String? getFocusedProjectId({required String connId}) => RustLib.instance.api.crateApiStateGetFocusedProjectId(connId: connId); -/// Get all folders from the cached remote state. +/// Get folders from the cached remote state. List getFolders({required String connId}) => RustLib.instance.api.crateApiStateGetFolders(connId: connId); @@ -25,6 +25,19 @@ List getFolders({required String connId}) => List getProjectOrder({required String connId}) => RustLib.instance.api.crateApiStateGetProjectOrder(connId: connId); +/// Get fullscreen terminal info. +FullscreenInfo? getFullscreenTerminal({required String connId}) => + RustLib.instance.api.crateApiStateGetFullscreenTerminal(connId: connId); + +/// Get layout JSON for a project. +String? getProjectLayoutJson({ + required String connId, + required String projectId, +}) => RustLib.instance.api.crateApiStateGetProjectLayoutJson( + connId: connId, + projectId: projectId, +); + /// Check if a terminal has unprocessed output (dirty flag). bool isDirty({required String connId, required String terminalId}) => RustLib .instance @@ -44,53 +57,55 @@ Future sendSpecialKey({ key: key, ); -/// Get a project's layout tree as JSON. -/// -/// Returns the `ApiLayoutNode` serialized as JSON, or `None` if the project -/// has no layout. Using JSON avoids complex recursive enum FRB codegen. -String? getProjectLayoutJson({ +/// Get all terminal IDs from the cached remote state (flat list). +List getAllTerminalIds({required String connId}) => + RustLib.instance.api.crateApiStateGetAllTerminalIds(connId: connId); + +/// Create a new terminal in the given project. +Future createTerminal({ required String connId, required String projectId, -}) => RustLib.instance.api.crateApiStateGetProjectLayoutJson( +}) => RustLib.instance.api.crateApiStateCreateTerminal( connId: connId, projectId: projectId, ); -/// Get all terminal IDs from the cached remote state (flat list). -List getAllTerminalIds({required String connId}) => - RustLib.instance.api.crateApiStateGetAllTerminalIds(connId: connId); - -/// Get the server-side terminal size from the cached state. -/// Returns (0, 0) if the terminal is not found or size is not available. -ServerTerminalSize getServerTerminalSize({ +/// Close a terminal in the given project. +Future closeTerminal({ required String connId, + required String projectId, required String terminalId, -}) => RustLib.instance.api.crateApiStateGetServerTerminalSize( +}) => RustLib.instance.api.crateApiStateCloseTerminal( connId: connId, + projectId: projectId, terminalId: terminalId, ); -/// Create a new terminal in a project. -Future createTerminal({ +/// Close multiple terminals in a project. +Future closeTerminals({ required String connId, required String projectId, -}) => RustLib.instance.api.crateApiStateCreateTerminal( + required List terminalIds, +}) => RustLib.instance.api.crateApiStateCloseTerminals( connId: connId, projectId: projectId, + terminalIds: terminalIds, ); -/// Close a terminal in a project. -Future closeTerminal({ +/// Rename a terminal. +Future renameTerminal({ required String connId, required String projectId, required String terminalId, -}) => RustLib.instance.api.crateApiStateCloseTerminal( + required String name, +}) => RustLib.instance.api.crateApiStateRenameTerminal( connId: connId, projectId: projectId, terminalId: terminalId, + name: name, ); -/// Focus a terminal in a project. +/// Focus a terminal. Future focusTerminal({ required String connId, required String projectId, @@ -101,6 +116,190 @@ Future focusTerminal({ terminalId: terminalId, ); +/// Toggle minimized state of a terminal. +Future toggleMinimized({ + required String connId, + required String projectId, + required String terminalId, +}) => RustLib.instance.api.crateApiStateToggleMinimized( + connId: connId, + projectId: projectId, + terminalId: terminalId, +); + +/// Set/clear fullscreen terminal. +Future setFullscreen({ + required String connId, + required String projectId, + String? terminalId, +}) => RustLib.instance.api.crateApiStateSetFullscreen( + connId: connId, + projectId: projectId, + terminalId: terminalId, +); + +/// Split a terminal pane. +Future splitTerminal({ + required String connId, + required String projectId, + required Uint64List path, + required String direction, +}) => RustLib.instance.api.crateApiStateSplitTerminal( + connId: connId, + projectId: projectId, + path: path, + direction: direction, +); + +/// Run a command in a terminal (presses Enter automatically). +Future runCommand({ + required String connId, + required String terminalId, + required String command, +}) => RustLib.instance.api.crateApiStateRunCommand( + connId: connId, + terminalId: terminalId, + command: command, +); + +/// Read terminal content as text. +Future readContent({ + required String connId, + required String terminalId, +}) => RustLib.instance.api.crateApiStateReadContent( + connId: connId, + terminalId: terminalId, +); + +/// Get detailed git status for a project. +Future gitStatus({required String connId, required String projectId}) => + RustLib.instance.api.crateApiStateGitStatus( + connId: connId, + projectId: projectId, + ); + +/// Get git diff summary for a project. +Future gitDiffSummary({ + required String connId, + required String projectId, +}) => RustLib.instance.api.crateApiStateGitDiffSummary( + connId: connId, + projectId: projectId, +); + +/// Get git diff for a project. Mode: "working_tree", "staged". +Future gitDiff({ + required String connId, + required String projectId, + required String mode, +}) => RustLib.instance.api.crateApiStateGitDiff( + connId: connId, + projectId: projectId, + mode: mode, +); + +/// Get git branches for a project. +Future gitBranches({ + required String connId, + required String projectId, +}) => RustLib.instance.api.crateApiStateGitBranches( + connId: connId, + projectId: projectId, +); + +/// Start a service. +Future startService({ + required String connId, + required String projectId, + required String serviceName, +}) => RustLib.instance.api.crateApiStateStartService( + connId: connId, + projectId: projectId, + serviceName: serviceName, +); + +/// Stop a service. +Future stopService({ + required String connId, + required String projectId, + required String serviceName, +}) => RustLib.instance.api.crateApiStateStopService( + connId: connId, + projectId: projectId, + serviceName: serviceName, +); + +/// Restart a service. +Future restartService({ + required String connId, + required String projectId, + required String serviceName, +}) => RustLib.instance.api.crateApiStateRestartService( + connId: connId, + projectId: projectId, + serviceName: serviceName, +); + +/// Start all services in a project. +Future startAllServices({ + required String connId, + required String projectId, +}) => RustLib.instance.api.crateApiStateStartAllServices( + connId: connId, + projectId: projectId, +); + +/// Stop all services in a project. +Future stopAllServices({ + required String connId, + required String projectId, +}) => RustLib.instance.api.crateApiStateStopAllServices( + connId: connId, + projectId: projectId, +); + +/// Reload services config for a project. +Future reloadServices({ + required String connId, + required String projectId, +}) => RustLib.instance.api.crateApiStateReloadServices( + connId: connId, + projectId: projectId, +); + +/// Add a new project. +Future addProject({ + required String connId, + required String name, + required String path, +}) => RustLib.instance.api.crateApiStateAddProject( + connId: connId, + name: name, + path: path, +); + +/// Set project color. +Future setProjectColor({ + required String connId, + required String projectId, + required String color, +}) => RustLib.instance.api.crateApiStateSetProjectColor( + connId: connId, + projectId: projectId, + color: color, +); + +/// Set folder color. +Future setFolderColor({ + required String connId, + required String folderId, + required String color, +}) => RustLib.instance.api.crateApiStateSetFolderColor( + connId: connId, + folderId: folderId, + color: color, +); + /// FFI-friendly folder info. class FolderInfo { final String id; @@ -130,21 +329,50 @@ class FolderInfo { folderColor == other.folderColor; } +/// FFI-friendly fullscreen info. +class FullscreenInfo { + final String projectId; + final String terminalId; + + const FullscreenInfo({required this.projectId, required this.terminalId}); + + @override + int get hashCode => projectId.hashCode ^ terminalId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FullscreenInfo && + runtimeType == other.runtimeType && + projectId == other.projectId && + terminalId == other.terminalId; +} + /// Flat FFI-friendly project info. class ProjectInfo { final String id; final String name; final String path; - final bool isVisible; + final bool showInOverview; final List terminalIds; + final Map terminalNames; + final String? gitBranch; + final int gitLinesAdded; + final int gitLinesRemoved; + final List services; final String folderColor; const ProjectInfo({ required this.id, required this.name, required this.path, - required this.isVisible, + required this.showInOverview, required this.terminalIds, + required this.terminalNames, + this.gitBranch, + required this.gitLinesAdded, + required this.gitLinesRemoved, + required this.services, required this.folderColor, }); @@ -153,8 +381,13 @@ class ProjectInfo { id.hashCode ^ name.hashCode ^ path.hashCode ^ - isVisible.hashCode ^ + showInOverview.hashCode ^ terminalIds.hashCode ^ + terminalNames.hashCode ^ + gitBranch.hashCode ^ + gitLinesAdded.hashCode ^ + gitLinesRemoved.hashCode ^ + services.hashCode ^ folderColor.hashCode; @override @@ -165,26 +398,56 @@ class ProjectInfo { id == other.id && name == other.name && path == other.path && - isVisible == other.isVisible && + showInOverview == other.showInOverview && terminalIds == other.terminalIds && + terminalNames == other.terminalNames && + gitBranch == other.gitBranch && + gitLinesAdded == other.gitLinesAdded && + gitLinesRemoved == other.gitLinesRemoved && + services == other.services && folderColor == other.folderColor; } -/// Server terminal size returned via FFI. -class ServerTerminalSize { - final int cols; - final int rows; +/// FFI-friendly service info. +class ServiceInfo { + final String name; + final String status; + final String? terminalId; + final Uint16List ports; + final int? exitCode; + final String kind; + final bool isExtra; - const ServerTerminalSize({required this.cols, required this.rows}); + const ServiceInfo({ + required this.name, + required this.status, + this.terminalId, + required this.ports, + this.exitCode, + required this.kind, + required this.isExtra, + }); @override - int get hashCode => cols.hashCode ^ rows.hashCode; + int get hashCode => + name.hashCode ^ + status.hashCode ^ + terminalId.hashCode ^ + ports.hashCode ^ + exitCode.hashCode ^ + kind.hashCode ^ + isExtra.hashCode; @override bool operator ==(Object other) => identical(this, other) || - other is ServerTerminalSize && + other is ServiceInfo && runtimeType == other.runtimeType && - cols == other.cols && - rows == other.rows; + name == other.name && + status == other.status && + terminalId == other.terminalId && + ports == other.ports && + exitCode == other.exitCode && + kind == other.kind && + isExtra == other.isExtra; } diff --git a/mobile/lib/src/rust/frb_generated.dart b/mobile/lib/src/rust/frb_generated.dart index 04e4842d..c033ab1c 100644 --- a/mobile/lib/src/rust/frb_generated.dart +++ b/mobile/lib/src/rust/frb_generated.dart @@ -68,7 +68,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 2126369707; + int get rustContentHash => -142339940; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -79,13 +79,34 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { + Future crateApiStateAddProject({ + required String connId, + required String name, + required String path, + }); + + void crateApiTerminalClearSelection({ + required String connId, + required String terminalId, + }); + Future crateApiStateCloseTerminal({ required String connId, required String projectId, required String terminalId, }); - String crateApiConnectionConnect({required String host, required int port}); + Future crateApiStateCloseTerminals({ + required String connId, + required String projectId, + required List terminalIds, + }); + + String crateApiConnectionConnect({ + required String host, + required int port, + String? savedToken, + }); ConnectionStatus crateApiConnectionConnectionStatus({required String connId}); @@ -109,15 +130,12 @@ abstract class RustLibApi extends BaseApi { required String terminalId, }); - int crateApiTerminalGetDisplayOffset({ - required String connId, - required String terminalId, - }); - String? crateApiStateGetFocusedProjectId({required String connId}); List crateApiStateGetFolders({required String connId}); + FullscreenInfo? crateApiStateGetFullscreenTerminal({required String connId}); + String? crateApiStateGetProjectLayoutJson({ required String connId, required String projectId, @@ -127,16 +145,49 @@ abstract class RustLibApi extends BaseApi { List crateApiStateGetProjects({required String connId}); - ServerTerminalSize crateApiStateGetServerTerminalSize({ + ScrollInfo crateApiTerminalGetScrollInfo({ + required String connId, + required String terminalId, + }); + + String? crateApiTerminalGetSelectedText({ + required String connId, + required String terminalId, + }); + + SelectionBounds? crateApiTerminalGetSelectionBounds({ required String connId, required String terminalId, }); + String? crateApiConnectionGetToken({required String connId}); + List crateApiTerminalGetVisibleCells({ required String connId, required String terminalId, }); + Future crateApiStateGitBranches({ + required String connId, + required String projectId, + }); + + Future crateApiStateGitDiff({ + required String connId, + required String projectId, + required String mode, + }); + + Future crateApiStateGitDiffSummary({ + required String connId, + required String projectId, + }); + + Future crateApiStateGitStatus({ + required String connId, + required String projectId, + }); + Future crateApiConnectionInitApp(); bool crateApiStateIsDirty({ @@ -149,26 +200,50 @@ abstract class RustLibApi extends BaseApi { required String code, }); - void crateApiTerminalResizeLocal({ + Future crateApiStateReadContent({ required String connId, required String terminalId, - required int cols, - required int rows, }); - Future crateApiTerminalResizeTerminal({ + Future crateApiStateReloadServices({ + required String connId, + required String projectId, + }); + + Future crateApiStateRenameTerminal({ + required String connId, + required String projectId, + required String terminalId, + required String name, + }); + + void crateApiTerminalResizeTerminal({ required String connId, required String terminalId, required int cols, required int rows, }); - void crateApiTerminalScrollTerminal({ + Future crateApiStateRestartService({ + required String connId, + required String projectId, + required String serviceName, + }); + + Future crateApiStateRunCommand({ + required String connId, + required String terminalId, + required String command, + }); + + void crateApiTerminalScroll({ required String connId, required String terminalId, required int delta, }); + double crateApiConnectionSecondsSinceActivity({required String connId}); + Future crateApiStateSendSpecialKey({ required String connId, required String terminalId, @@ -180,6 +255,80 @@ abstract class RustLibApi extends BaseApi { required String terminalId, required String text, }); + + Future crateApiStateSetFolderColor({ + required String connId, + required String folderId, + required String color, + }); + + Future crateApiStateSetFullscreen({ + required String connId, + required String projectId, + String? terminalId, + }); + + Future crateApiStateSetProjectColor({ + required String connId, + required String projectId, + required String color, + }); + + Future crateApiStateSplitTerminal({ + required String connId, + required String projectId, + required Uint64List path, + required String direction, + }); + + Future crateApiStateStartAllServices({ + required String connId, + required String projectId, + }); + + void crateApiTerminalStartSelection({ + required String connId, + required String terminalId, + required int col, + required int row, + }); + + Future crateApiStateStartService({ + required String connId, + required String projectId, + required String serviceName, + }); + + void crateApiTerminalStartWordSelection({ + required String connId, + required String terminalId, + required int col, + required int row, + }); + + Future crateApiStateStopAllServices({ + required String connId, + required String projectId, + }); + + Future crateApiStateStopService({ + required String connId, + required String projectId, + required String serviceName, + }); + + Future crateApiStateToggleMinimized({ + required String connId, + required String projectId, + required String terminalId, + }); + + void crateApiTerminalUpdateSelection({ + required String connId, + required String terminalId, + required int col, + required int row, + }); } class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @@ -190,6 +339,72 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required super.portManager, }); + @override + Future crateApiStateAddProject({ + required String connId, + required String name, + required String path, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(name, serializer); + sse_encode_String(path, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 1, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateAddProjectConstMeta, + argValues: [connId, name, path], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateAddProjectConstMeta => const TaskConstMeta( + debugName: "add_project", + argNames: ["connId", "name", "path"], + ); + + @override + void crateApiTerminalClearSelection({ + required String connId, + required String terminalId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiTerminalClearSelectionConstMeta, + argValues: [connId, terminalId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalClearSelectionConstMeta => + const TaskConstMeta( + debugName: "clear_selection", + argNames: ["connId", "terminalId"], + ); + @override Future crateApiStateCloseTerminal({ required String connId, @@ -206,7 +421,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 1, + funcId: 3, port: port_, ); }, @@ -227,28 +442,72 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - String crateApiConnectionConnect({required String host, required int port}) { + Future crateApiStateCloseTerminals({ + required String connId, + required String projectId, + required List terminalIds, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_list_String(terminalIds, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 4, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateCloseTerminalsConstMeta, + argValues: [connId, projectId, terminalIds], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateCloseTerminalsConstMeta => + const TaskConstMeta( + debugName: "close_terminals", + argNames: ["connId", "projectId", "terminalIds"], + ); + + @override + String crateApiConnectionConnect({ + required String host, + required int port, + String? savedToken, + }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(host, serializer); sse_encode_u_16(port, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + sse_encode_opt_String(savedToken, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, decodeErrorData: null, ), constMeta: kCrateApiConnectionConnectConstMeta, - argValues: [host, port], + argValues: [host, port, savedToken], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiConnectionConnectConstMeta => - const TaskConstMeta(debugName: "connect", argNames: ["host", "port"]); + TaskConstMeta get kCrateApiConnectionConnectConstMeta => const TaskConstMeta( + debugName: "connect", + argNames: ["host", "port", "savedToken"], + ); @override ConnectionStatus crateApiConnectionConnectionStatus({ @@ -259,7 +518,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; }, codec: SseCodec( decodeSuccessData: sse_decode_connection_status, @@ -289,7 +548,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 4, + funcId: 7, port: port_, ); }, @@ -317,7 +576,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -349,7 +608,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 6, + funcId: 9, port: port_, ); }, @@ -376,7 +635,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_String, @@ -406,7 +665,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; }, codec: SseCodec( decodeSuccessData: sse_decode_cursor_state, @@ -425,83 +684,79 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - int crateApiTerminalGetDisplayOffset({ - required String connId, - required String terminalId, - }) { + String? crateApiStateGetFocusedProjectId({required String connId}) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_i_32, + decodeSuccessData: sse_decode_opt_String, decodeErrorData: null, ), - constMeta: kCrateApiTerminalGetDisplayOffsetConstMeta, - argValues: [connId, terminalId], + constMeta: kCrateApiStateGetFocusedProjectIdConstMeta, + argValues: [connId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalGetDisplayOffsetConstMeta => + TaskConstMeta get kCrateApiStateGetFocusedProjectIdConstMeta => const TaskConstMeta( - debugName: "get_display_offset", - argNames: ["connId", "terminalId"], + debugName: "get_focused_project_id", + argNames: ["connId"], ); @override - String? crateApiStateGetFocusedProjectId({required String connId}) { + List crateApiStateGetFolders({required String connId}) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, + decodeSuccessData: sse_decode_list_folder_info, decodeErrorData: null, ), - constMeta: kCrateApiStateGetFocusedProjectIdConstMeta, + constMeta: kCrateApiStateGetFoldersConstMeta, argValues: [connId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateGetFocusedProjectIdConstMeta => - const TaskConstMeta( - debugName: "get_focused_project_id", - argNames: ["connId"], - ); + TaskConstMeta get kCrateApiStateGetFoldersConstMeta => + const TaskConstMeta(debugName: "get_folders", argNames: ["connId"]); @override - List crateApiStateGetFolders({required String connId}) { + FullscreenInfo? crateApiStateGetFullscreenTerminal({required String connId}) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_list_folder_info, + decodeSuccessData: sse_decode_opt_box_autoadd_fullscreen_info, decodeErrorData: null, ), - constMeta: kCrateApiStateGetFoldersConstMeta, + constMeta: kCrateApiStateGetFullscreenTerminalConstMeta, argValues: [connId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateGetFoldersConstMeta => - const TaskConstMeta(debugName: "get_folders", argNames: ["connId"]); + TaskConstMeta get kCrateApiStateGetFullscreenTerminalConstMeta => + const TaskConstMeta( + debugName: "get_fullscreen_terminal", + argNames: ["connId"], + ); @override String? crateApiStateGetProjectLayoutJson({ @@ -514,7 +769,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(projectId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -540,7 +795,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_String, @@ -563,7 +818,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_project_info, @@ -580,7 +835,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { const TaskConstMeta(debugName: "get_projects", argNames: ["connId"]); @override - ServerTerminalSize crateApiStateGetServerTerminalSize({ + ScrollInfo crateApiTerminalGetScrollInfo({ required String connId, required String terminalId, }) { @@ -590,27 +845,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_server_terminal_size, + decodeSuccessData: sse_decode_scroll_info, decodeErrorData: null, ), - constMeta: kCrateApiStateGetServerTerminalSizeConstMeta, + constMeta: kCrateApiTerminalGetScrollInfoConstMeta, argValues: [connId, terminalId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateGetServerTerminalSizeConstMeta => + TaskConstMeta get kCrateApiTerminalGetScrollInfoConstMeta => const TaskConstMeta( - debugName: "get_server_terminal_size", + debugName: "get_scroll_info", argNames: ["connId", "terminalId"], ); @override - List crateApiTerminalGetVisibleCells({ + String? crateApiTerminalGetSelectedText({ required String connId, required String terminalId, }) { @@ -620,96 +875,843 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_list_cell_data, + decodeSuccessData: sse_decode_opt_String, decodeErrorData: null, ), - constMeta: kCrateApiTerminalGetVisibleCellsConstMeta, + constMeta: kCrateApiTerminalGetSelectedTextConstMeta, argValues: [connId, terminalId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalGetVisibleCellsConstMeta => + TaskConstMeta get kCrateApiTerminalGetSelectedTextConstMeta => const TaskConstMeta( - debugName: "get_visible_cells", + debugName: "get_selected_text", argNames: ["connId", "terminalId"], ); @override - Future crateApiConnectionInitApp() { + SelectionBounds? crateApiTerminalGetSelectionBounds({ + required String connId, + required String terminalId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_box_autoadd_selection_bounds, + decodeErrorData: null, + ), + constMeta: kCrateApiTerminalGetSelectionBoundsConstMeta, + argValues: [connId, terminalId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalGetSelectionBoundsConstMeta => + const TaskConstMeta( + debugName: "get_selection_bounds", + argNames: ["connId", "terminalId"], + ); + + @override + String? crateApiConnectionGetToken({required String connId}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_String, + decodeErrorData: null, + ), + constMeta: kCrateApiConnectionGetTokenConstMeta, + argValues: [connId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiConnectionGetTokenConstMeta => + const TaskConstMeta(debugName: "get_token", argNames: ["connId"]); + + @override + List crateApiTerminalGetVisibleCells({ + required String connId, + required String terminalId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_cell_data, + decodeErrorData: null, + ), + constMeta: kCrateApiTerminalGetVisibleCellsConstMeta, + argValues: [connId, terminalId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalGetVisibleCellsConstMeta => + const TaskConstMeta( + debugName: "get_visible_cells", + argNames: ["connId", "terminalId"], + ); + + @override + Future crateApiStateGitBranches({ + required String connId, + required String projectId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 23, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateGitBranchesConstMeta, + argValues: [connId, projectId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateGitBranchesConstMeta => const TaskConstMeta( + debugName: "git_branches", + argNames: ["connId", "projectId"], + ); + + @override + Future crateApiStateGitDiff({ + required String connId, + required String projectId, + required String mode, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(mode, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 24, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateGitDiffConstMeta, + argValues: [connId, projectId, mode], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateGitDiffConstMeta => const TaskConstMeta( + debugName: "git_diff", + argNames: ["connId", "projectId", "mode"], + ); + + @override + Future crateApiStateGitDiffSummary({ + required String connId, + required String projectId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 25, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateGitDiffSummaryConstMeta, + argValues: [connId, projectId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateGitDiffSummaryConstMeta => + const TaskConstMeta( + debugName: "git_diff_summary", + argNames: ["connId", "projectId"], + ); + + @override + Future crateApiStateGitStatus({ + required String connId, + required String projectId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 26, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateGitStatusConstMeta, + argValues: [connId, projectId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateGitStatusConstMeta => const TaskConstMeta( + debugName: "git_status", + argNames: ["connId", "projectId"], + ); + + @override + Future crateApiConnectionInitApp() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 27, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiConnectionInitAppConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiConnectionInitAppConstMeta => + const TaskConstMeta(debugName: "init_app", argNames: []); + + @override + bool crateApiStateIsDirty({ + required String connId, + required String terminalId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: null, + ), + constMeta: kCrateApiStateIsDirtyConstMeta, + argValues: [connId, terminalId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateIsDirtyConstMeta => const TaskConstMeta( + debugName: "is_dirty", + argNames: ["connId", "terminalId"], + ); + + @override + Future crateApiConnectionPair({ + required String connId, + required String code, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(code, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 29, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiConnectionPairConstMeta, + argValues: [connId, code], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiConnectionPairConstMeta => + const TaskConstMeta(debugName: "pair", argNames: ["connId", "code"]); + + @override + Future crateApiStateReadContent({ + required String connId, + required String terminalId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 30, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateReadContentConstMeta, + argValues: [connId, terminalId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateReadContentConstMeta => const TaskConstMeta( + debugName: "read_content", + argNames: ["connId", "terminalId"], + ); + + @override + Future crateApiStateReloadServices({ + required String connId, + required String projectId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 31, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateReloadServicesConstMeta, + argValues: [connId, projectId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateReloadServicesConstMeta => + const TaskConstMeta( + debugName: "reload_services", + argNames: ["connId", "projectId"], + ); + + @override + Future crateApiStateRenameTerminal({ + required String connId, + required String projectId, + required String terminalId, + required String name, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_String(name, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 32, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateRenameTerminalConstMeta, + argValues: [connId, projectId, terminalId, name], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateRenameTerminalConstMeta => + const TaskConstMeta( + debugName: "rename_terminal", + argNames: ["connId", "projectId", "terminalId", "name"], + ); + + @override + void crateApiTerminalResizeTerminal({ + required String connId, + required String terminalId, + required int cols, + required int rows, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_u_16(cols, serializer); + sse_encode_u_16(rows, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiTerminalResizeTerminalConstMeta, + argValues: [connId, terminalId, cols, rows], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalResizeTerminalConstMeta => + const TaskConstMeta( + debugName: "resize_terminal", + argNames: ["connId", "terminalId", "cols", "rows"], + ); + + @override + Future crateApiStateRestartService({ + required String connId, + required String projectId, + required String serviceName, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(serviceName, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 34, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateRestartServiceConstMeta, + argValues: [connId, projectId, serviceName], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateRestartServiceConstMeta => + const TaskConstMeta( + debugName: "restart_service", + argNames: ["connId", "projectId", "serviceName"], + ); + + @override + Future crateApiStateRunCommand({ + required String connId, + required String terminalId, + required String command, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_String(command, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 35, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateRunCommandConstMeta, + argValues: [connId, terminalId, command], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateRunCommandConstMeta => const TaskConstMeta( + debugName: "run_command", + argNames: ["connId", "terminalId", "command"], + ); + + @override + void crateApiTerminalScroll({ + required String connId, + required String terminalId, + required int delta, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_i_32(delta, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiTerminalScrollConstMeta, + argValues: [connId, terminalId, delta], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalScrollConstMeta => const TaskConstMeta( + debugName: "scroll", + argNames: ["connId", "terminalId", "delta"], + ); + + @override + double crateApiConnectionSecondsSinceActivity({required String connId}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_f_64, + decodeErrorData: null, + ), + constMeta: kCrateApiConnectionSecondsSinceActivityConstMeta, + argValues: [connId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiConnectionSecondsSinceActivityConstMeta => + const TaskConstMeta( + debugName: "seconds_since_activity", + argNames: ["connId"], + ); + + @override + Future crateApiStateSendSpecialKey({ + required String connId, + required String terminalId, + required String key, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_String(key, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 38, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateSendSpecialKeyConstMeta, + argValues: [connId, terminalId, key], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateSendSpecialKeyConstMeta => + const TaskConstMeta( + debugName: "send_special_key", + argNames: ["connId", "terminalId", "key"], + ); + + @override + Future crateApiTerminalSendText({ + required String connId, + required String terminalId, + required String text, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_String(text, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 39, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiTerminalSendTextConstMeta, + argValues: [connId, terminalId, text], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalSendTextConstMeta => const TaskConstMeta( + debugName: "send_text", + argNames: ["connId", "terminalId", "text"], + ); + + @override + Future crateApiStateSetFolderColor({ + required String connId, + required String folderId, + required String color, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(folderId, serializer); + sse_encode_String(color, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 40, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateSetFolderColorConstMeta, + argValues: [connId, folderId, color], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateSetFolderColorConstMeta => + const TaskConstMeta( + debugName: "set_folder_color", + argNames: ["connId", "folderId", "color"], + ); + + @override + Future crateApiStateSetFullscreen({ + required String connId, + required String projectId, + String? terminalId, + }) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_opt_String(terminalId, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 41, port: port_, ); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, - decodeErrorData: null, + decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiConnectionInitAppConstMeta, - argValues: [], + constMeta: kCrateApiStateSetFullscreenConstMeta, + argValues: [connId, projectId, terminalId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiConnectionInitAppConstMeta => - const TaskConstMeta(debugName: "init_app", argNames: []); + TaskConstMeta get kCrateApiStateSetFullscreenConstMeta => const TaskConstMeta( + debugName: "set_fullscreen", + argNames: ["connId", "projectId", "terminalId"], + ); @override - bool crateApiStateIsDirty({ + Future crateApiStateSetProjectColor({ required String connId, - required String terminalId, + required String projectId, + required String color, }) { - return handler.executeSync( - SyncTask( - callFfi: () { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; + sse_encode_String(projectId, serializer); + sse_encode_String(color, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 42, + port: port_, + ); }, codec: SseCodec( - decodeSuccessData: sse_decode_bool, - decodeErrorData: null, + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiStateIsDirtyConstMeta, - argValues: [connId, terminalId], + constMeta: kCrateApiStateSetProjectColorConstMeta, + argValues: [connId, projectId, color], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateIsDirtyConstMeta => const TaskConstMeta( - debugName: "is_dirty", - argNames: ["connId", "terminalId"], + TaskConstMeta get kCrateApiStateSetProjectColorConstMeta => + const TaskConstMeta( + debugName: "set_project_color", + argNames: ["connId", "projectId", "color"], + ); + + @override + Future crateApiStateSplitTerminal({ + required String connId, + required String projectId, + required Uint64List path, + required String direction, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_list_prim_usize_strict(path, serializer); + sse_encode_String(direction, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 43, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateSplitTerminalConstMeta, + argValues: [connId, projectId, path, direction], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateSplitTerminalConstMeta => const TaskConstMeta( + debugName: "split_terminal", + argNames: ["connId", "projectId", "path", "direction"], ); @override - Future crateApiConnectionPair({ + Future crateApiStateStartAllServices({ required String connId, - required String code, + required String projectId, }) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(code, serializer); + sse_encode_String(projectId, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 44, port: port_, ); }, @@ -717,22 +1719,25 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiConnectionPairConstMeta, - argValues: [connId, code], + constMeta: kCrateApiStateStartAllServicesConstMeta, + argValues: [connId, projectId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiConnectionPairConstMeta => - const TaskConstMeta(debugName: "pair", argNames: ["connId", "code"]); + TaskConstMeta get kCrateApiStateStartAllServicesConstMeta => + const TaskConstMeta( + debugName: "start_all_services", + argNames: ["connId", "projectId"], + ); @override - void crateApiTerminalResizeLocal({ + void crateApiTerminalStartSelection({ required String connId, required String terminalId, - required int cols, - required int rows, + required int col, + required int row, }) { return handler.executeSync( SyncTask( @@ -740,46 +1745,44 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - sse_encode_u_16(cols, serializer); - sse_encode_u_16(rows, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + sse_encode_u_16(col, serializer); + sse_encode_u_16(row, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, decodeErrorData: null, ), - constMeta: kCrateApiTerminalResizeLocalConstMeta, - argValues: [connId, terminalId, cols, rows], + constMeta: kCrateApiTerminalStartSelectionConstMeta, + argValues: [connId, terminalId, col, row], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalResizeLocalConstMeta => + TaskConstMeta get kCrateApiTerminalStartSelectionConstMeta => const TaskConstMeta( - debugName: "resize_local", - argNames: ["connId", "terminalId", "cols", "rows"], + debugName: "start_selection", + argNames: ["connId", "terminalId", "col", "row"], ); @override - Future crateApiTerminalResizeTerminal({ + Future crateApiStateStartService({ required String connId, - required String terminalId, - required int cols, - required int rows, + required String projectId, + required String serviceName, }) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(cols, serializer); - sse_encode_u_16(rows, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(serviceName, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 21, + funcId: 46, port: port_, ); }, @@ -787,24 +1790,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiTerminalResizeTerminalConstMeta, - argValues: [connId, terminalId, cols, rows], + constMeta: kCrateApiStateStartServiceConstMeta, + argValues: [connId, projectId, serviceName], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalResizeTerminalConstMeta => - const TaskConstMeta( - debugName: "resize_terminal", - argNames: ["connId", "terminalId", "cols", "rows"], - ); + TaskConstMeta get kCrateApiStateStartServiceConstMeta => const TaskConstMeta( + debugName: "start_service", + argNames: ["connId", "projectId", "serviceName"], + ); @override - void crateApiTerminalScrollTerminal({ + void crateApiTerminalStartWordSelection({ required String connId, required String terminalId, - required int delta, + required int col, + required int row, }) { return handler.executeSync( SyncTask( @@ -812,43 +1815,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - sse_encode_i_32(delta, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; + sse_encode_u_16(col, serializer); + sse_encode_u_16(row, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, decodeErrorData: null, ), - constMeta: kCrateApiTerminalScrollTerminalConstMeta, - argValues: [connId, terminalId, delta], + constMeta: kCrateApiTerminalStartWordSelectionConstMeta, + argValues: [connId, terminalId, col, row], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalScrollTerminalConstMeta => + TaskConstMeta get kCrateApiTerminalStartWordSelectionConstMeta => const TaskConstMeta( - debugName: "scroll_terminal", - argNames: ["connId", "terminalId", "delta"], + debugName: "start_word_selection", + argNames: ["connId", "terminalId", "col", "row"], ); @override - Future crateApiStateSendSpecialKey({ + Future crateApiStateStopAllServices({ required String connId, - required String terminalId, - required String key, + required String projectId, }) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(key, serializer); + sse_encode_String(projectId, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 48, port: port_, ); }, @@ -856,36 +1858,72 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiStateSendSpecialKeyConstMeta, - argValues: [connId, terminalId, key], + constMeta: kCrateApiStateStopAllServicesConstMeta, + argValues: [connId, projectId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateSendSpecialKeyConstMeta => + TaskConstMeta get kCrateApiStateStopAllServicesConstMeta => const TaskConstMeta( - debugName: "send_special_key", - argNames: ["connId", "terminalId", "key"], + debugName: "stop_all_services", + argNames: ["connId", "projectId"], ); @override - Future crateApiTerminalSendText({ + Future crateApiStateStopService({ required String connId, + required String projectId, + required String serviceName, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(serviceName, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 49, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateStopServiceConstMeta, + argValues: [connId, projectId, serviceName], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateStopServiceConstMeta => const TaskConstMeta( + debugName: "stop_service", + argNames: ["connId", "projectId", "serviceName"], + ); + + @override + Future crateApiStateToggleMinimized({ + required String connId, + required String projectId, required String terminalId, - required String text, }) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); sse_encode_String(terminalId, serializer); - sse_encode_String(text, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 50, port: port_, ); }, @@ -893,17 +1931,52 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiTerminalSendTextConstMeta, - argValues: [connId, terminalId, text], + constMeta: kCrateApiStateToggleMinimizedConstMeta, + argValues: [connId, projectId, terminalId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalSendTextConstMeta => const TaskConstMeta( - debugName: "send_text", - argNames: ["connId", "terminalId", "text"], - ); + TaskConstMeta get kCrateApiStateToggleMinimizedConstMeta => + const TaskConstMeta( + debugName: "toggle_minimized", + argNames: ["connId", "projectId", "terminalId"], + ); + + @override + void crateApiTerminalUpdateSelection({ + required String connId, + required String terminalId, + required int col, + required int row, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_u_16(col, serializer); + sse_encode_u_16(row, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 51)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiTerminalUpdateSelectionConstMeta, + argValues: [connId, terminalId, col, row], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalUpdateSelectionConstMeta => + const TaskConstMeta( + debugName: "update_selection", + argNames: ["connId", "terminalId", "col", "row"], + ); @protected AnyhowException dco_decode_AnyhowException(dynamic raw) { @@ -911,6 +1984,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return AnyhowException(raw as String); } + @protected + Map dco_decode_Map_String_String_None(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return Map.fromEntries( + dco_decode_list_record_string_string( + raw, + ).map((e) => MapEntry(e.$1, e.$2)), + ); + } + @protected String dco_decode_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -923,6 +2006,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as bool; } + @protected + FullscreenInfo dco_decode_box_autoadd_fullscreen_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_fullscreen_info(raw); + } + + @protected + SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_selection_bounds(raw); + } + + @protected + int dco_decode_box_autoadd_u_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + @protected CellData dco_decode_cell_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -976,6 +2077,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + double dco_decode_f_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + @protected FolderInfo dco_decode_folder_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -990,6 +2097,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + FullscreenInfo dco_decode_fullscreen_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return FullscreenInfo( + projectId: dco_decode_String(arr[0]), + terminalId: dco_decode_String(arr[1]), + ); + } + @protected int dco_decode_i_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1015,48 +2134,137 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + Uint16List dco_decode_list_prim_u_16_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint16List; + } + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint8List; + } + + @protected + Uint64List dco_decode_list_prim_usize_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint64List; + } + + @protected + List dco_decode_list_project_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_project_info).toList(); + } + + @protected + List<(String, String)> dco_decode_list_record_string_string(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_record_string_string).toList(); + } + + @protected + List dco_decode_list_service_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_service_info).toList(); + } + + @protected + String? dco_decode_opt_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_String(raw); + } + + @protected + FullscreenInfo? dco_decode_opt_box_autoadd_fullscreen_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Uint8List; + return raw == null ? null : dco_decode_box_autoadd_fullscreen_info(raw); } @protected - List dco_decode_list_project_info(dynamic raw) { + SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_project_info).toList(); + return raw == null ? null : dco_decode_box_autoadd_selection_bounds(raw); } @protected - String? dco_decode_opt_String(dynamic raw) { + int? dco_decode_opt_box_autoadd_u_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_String(raw); + return raw == null ? null : dco_decode_box_autoadd_u_32(raw); } @protected ProjectInfo dco_decode_project_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 6) - throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + if (arr.length != 11) + throw Exception('unexpected arr length: expect 11 but see ${arr.length}'); return ProjectInfo( id: dco_decode_String(arr[0]), name: dco_decode_String(arr[1]), path: dco_decode_String(arr[2]), - isVisible: dco_decode_bool(arr[3]), + showInOverview: dco_decode_bool(arr[3]), terminalIds: dco_decode_list_String(arr[4]), - folderColor: dco_decode_String(arr[5]), + terminalNames: dco_decode_Map_String_String_None(arr[5]), + gitBranch: dco_decode_opt_String(arr[6]), + gitLinesAdded: dco_decode_u_32(arr[7]), + gitLinesRemoved: dco_decode_u_32(arr[8]), + services: dco_decode_list_service_info(arr[9]), + folderColor: dco_decode_String(arr[10]), ); } @protected - ServerTerminalSize dco_decode_server_terminal_size(dynamic raw) { + (String, String) dco_decode_record_string_string(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 2) - throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); - return ServerTerminalSize( - cols: dco_decode_u_16(arr[0]), - rows: dco_decode_u_16(arr[1]), + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return (dco_decode_String(arr[0]), dco_decode_String(arr[1])); + } + + @protected + ScrollInfo dco_decode_scroll_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) + throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return ScrollInfo( + totalLines: dco_decode_u_32(arr[0]), + visibleLines: dco_decode_u_32(arr[1]), + displayOffset: dco_decode_u_32(arr[2]), + ); + } + + @protected + SelectionBounds dco_decode_selection_bounds(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + return SelectionBounds( + startCol: dco_decode_u_16(arr[0]), + startRow: dco_decode_i_32(arr[1]), + endCol: dco_decode_u_16(arr[2]), + endRow: dco_decode_i_32(arr[3]), + ); + } + + @protected + ServiceInfo dco_decode_service_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 7) + throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); + return ServiceInfo( + name: dco_decode_String(arr[0]), + status: dco_decode_String(arr[1]), + terminalId: dco_decode_opt_String(arr[2]), + ports: dco_decode_list_prim_u_16_strict(arr[3]), + exitCode: dco_decode_opt_box_autoadd_u_32(arr[4]), + kind: dco_decode_String(arr[5]), + isExtra: dco_decode_bool(arr[6]), ); } @@ -1084,6 +2292,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return; } + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1091,6 +2305,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return AnyhowException(inner); } + @protected + Map sse_decode_Map_String_String_None( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_record_string_string(deserializer); + return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); + } + @protected String sse_decode_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1104,6 +2327,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8() != 0; } + @protected + FullscreenInfo sse_decode_box_autoadd_fullscreen_info( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_fullscreen_info(deserializer)); + } + + @protected + SelectionBounds sse_decode_box_autoadd_selection_bounds( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_selection_bounds(deserializer)); + } + + @protected + int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_u_32(deserializer)); + } + @protected CellData sse_decode_cell_data(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1163,6 +2408,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + double sse_decode_f_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getFloat64(); + } + @protected FolderInfo sse_decode_folder_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1178,6 +2429,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_projectId = sse_decode_String(deserializer); + var var_terminalId = sse_decode_String(deserializer); + return FullscreenInfo(projectId: var_projectId, terminalId: var_terminalId); + } + @protected int sse_decode_i_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1220,6 +2479,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint16List(len_); + } + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1227,6 +2493,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8List(len_); } + @protected + Uint64List sse_decode_list_prim_usize_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint64List(len_); + } + @protected List sse_decode_list_project_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1239,6 +2512,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + List<(String, String)> sse_decode_list_record_string_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = <(String, String)>[]; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_record_string_string(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_service_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_service_info(deserializer)); + } + return ans_; + } + @protected String? sse_decode_opt_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1250,33 +2549,129 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + FullscreenInfo? sse_decode_opt_box_autoadd_fullscreen_info( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_fullscreen_info(deserializer)); + } else { + return null; + } + } + + @protected + SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_selection_bounds(deserializer)); + } else { + return null; + } + } + + @protected + int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_u_32(deserializer)); + } else { + return null; + } + } + @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var var_id = sse_decode_String(deserializer); var var_name = sse_decode_String(deserializer); var var_path = sse_decode_String(deserializer); - var var_isVisible = sse_decode_bool(deserializer); + var var_showInOverview = sse_decode_bool(deserializer); var var_terminalIds = sse_decode_list_String(deserializer); + var var_terminalNames = sse_decode_Map_String_String_None(deserializer); + var var_gitBranch = sse_decode_opt_String(deserializer); + var var_gitLinesAdded = sse_decode_u_32(deserializer); + var var_gitLinesRemoved = sse_decode_u_32(deserializer); + var var_services = sse_decode_list_service_info(deserializer); var var_folderColor = sse_decode_String(deserializer); return ProjectInfo( id: var_id, name: var_name, path: var_path, - isVisible: var_isVisible, + showInOverview: var_showInOverview, terminalIds: var_terminalIds, + terminalNames: var_terminalNames, + gitBranch: var_gitBranch, + gitLinesAdded: var_gitLinesAdded, + gitLinesRemoved: var_gitLinesRemoved, + services: var_services, folderColor: var_folderColor, ); } @protected - ServerTerminalSize sse_decode_server_terminal_size( + (String, String) sse_decode_record_string_string( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - var var_cols = sse_decode_u_16(deserializer); - var var_rows = sse_decode_u_16(deserializer); - return ServerTerminalSize(cols: var_cols, rows: var_rows); + var var_field0 = sse_decode_String(deserializer); + var var_field1 = sse_decode_String(deserializer); + return (var_field0, var_field1); + } + + @protected + ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_totalLines = sse_decode_u_32(deserializer); + var var_visibleLines = sse_decode_u_32(deserializer); + var var_displayOffset = sse_decode_u_32(deserializer); + return ScrollInfo( + totalLines: var_totalLines, + visibleLines: var_visibleLines, + displayOffset: var_displayOffset, + ); + } + + @protected + SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_startCol = sse_decode_u_16(deserializer); + var var_startRow = sse_decode_i_32(deserializer); + var var_endCol = sse_decode_u_16(deserializer); + var var_endRow = sse_decode_i_32(deserializer); + return SelectionBounds( + startCol: var_startCol, + startRow: var_startRow, + endCol: var_endCol, + endRow: var_endRow, + ); + } + + @protected + ServiceInfo sse_decode_service_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_name = sse_decode_String(deserializer); + var var_status = sse_decode_String(deserializer); + var var_terminalId = sse_decode_opt_String(deserializer); + var var_ports = sse_decode_list_prim_u_16_strict(deserializer); + var var_exitCode = sse_decode_opt_box_autoadd_u_32(deserializer); + var var_kind = sse_decode_String(deserializer); + var var_isExtra = sse_decode_bool(deserializer); + return ServiceInfo( + name: var_name, + status: var_status, + terminalId: var_terminalId, + ports: var_ports, + exitCode: var_exitCode, + kind: var_kind, + isExtra: var_isExtra, + ); } @protected @@ -1302,6 +2697,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs } + @protected + BigInt sse_decode_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + @protected void sse_encode_AnyhowException( AnyhowException self, @@ -1311,6 +2712,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.message, serializer); } + @protected + void sse_encode_Map_String_String_None( + Map self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_record_string_string( + self.entries.map((e) => (e.key, e.value)).toList(), + serializer, + ); + } + @protected void sse_encode_String(String self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1323,6 +2736,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8(self ? 1 : 0); } + @protected + void sse_encode_box_autoadd_fullscreen_info( + FullscreenInfo self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_fullscreen_info(self, serializer); + } + + @protected + void sse_encode_box_autoadd_selection_bounds( + SelectionBounds self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_selection_bounds(self, serializer); + } + + @protected + void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_32(self, serializer); + } + @protected void sse_encode_cell_data(CellData self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1368,6 +2805,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_bool(self.visible, serializer); } + @protected + void sse_encode_f_64(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putFloat64(self); + } + @protected void sse_encode_folder_info(FolderInfo self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1377,6 +2820,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.folderColor, serializer); } + @protected + void sse_encode_fullscreen_info( + FullscreenInfo self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.projectId, serializer); + sse_encode_String(self.terminalId, serializer); + } + @protected void sse_encode_i_32(int self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1416,6 +2869,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_prim_u_16_strict( + Uint16List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint16List(self); + } + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, @@ -1426,6 +2889,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8List(self); } + @protected + void sse_encode_list_prim_usize_strict( + Uint64List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint64List(self); + } + @protected void sse_encode_list_project_info( List self, @@ -1438,6 +2911,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_record_string_string( + List<(String, String)> self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_record_string_string(item, serializer); + } + } + + @protected + void sse_encode_list_service_info( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_service_info(item, serializer); + } + } + @protected void sse_encode_opt_String(String? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1448,25 +2945,98 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_fullscreen_info( + FullscreenInfo? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_fullscreen_info(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_selection_bounds( + SelectionBounds? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_selection_bounds(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_u_32(self, serializer); + } + } + @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_String(self.id, serializer); sse_encode_String(self.name, serializer); sse_encode_String(self.path, serializer); - sse_encode_bool(self.isVisible, serializer); + sse_encode_bool(self.showInOverview, serializer); sse_encode_list_String(self.terminalIds, serializer); + sse_encode_Map_String_String_None(self.terminalNames, serializer); + sse_encode_opt_String(self.gitBranch, serializer); + sse_encode_u_32(self.gitLinesAdded, serializer); + sse_encode_u_32(self.gitLinesRemoved, serializer); + sse_encode_list_service_info(self.services, serializer); sse_encode_String(self.folderColor, serializer); } @protected - void sse_encode_server_terminal_size( - ServerTerminalSize self, + void sse_encode_record_string_string( + (String, String) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.$1, serializer); + sse_encode_String(self.$2, serializer); + } + + @protected + void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_32(self.totalLines, serializer); + sse_encode_u_32(self.visibleLines, serializer); + sse_encode_u_32(self.displayOffset, serializer); + } + + @protected + void sse_encode_selection_bounds( + SelectionBounds self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_16(self.cols, serializer); - sse_encode_u_16(self.rows, serializer); + sse_encode_u_16(self.startCol, serializer); + sse_encode_i_32(self.startRow, serializer); + sse_encode_u_16(self.endCol, serializer); + sse_encode_i_32(self.endRow, serializer); + } + + @protected + void sse_encode_service_info(ServiceInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.name, serializer); + sse_encode_String(self.status, serializer); + sse_encode_opt_String(self.terminalId, serializer); + sse_encode_list_prim_u_16_strict(self.ports, serializer); + sse_encode_opt_box_autoadd_u_32(self.exitCode, serializer); + sse_encode_String(self.kind, serializer); + sse_encode_bool(self.isExtra, serializer); } @protected @@ -1491,4 +3061,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { void sse_encode_unit(void self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs } + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } } diff --git a/mobile/lib/src/rust/frb_generated.io.dart b/mobile/lib/src/rust/frb_generated.io.dart index 29e6c80a..4b0482cf 100644 --- a/mobile/lib/src/rust/frb_generated.io.dart +++ b/mobile/lib/src/rust/frb_generated.io.dart @@ -23,12 +23,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); + @protected + Map dco_decode_Map_String_String_None(dynamic raw); + @protected String dco_decode_String(dynamic raw); @protected bool dco_decode_bool(dynamic raw); + @protected + FullscreenInfo dco_decode_box_autoadd_fullscreen_info(dynamic raw); + + @protected + SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); + + @protected + int dco_decode_box_autoadd_u_32(dynamic raw); + @protected CellData dco_decode_cell_data(dynamic raw); @@ -41,9 +53,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState dco_decode_cursor_state(dynamic raw); + @protected + double dco_decode_f_64(dynamic raw); + @protected FolderInfo dco_decode_folder_info(dynamic raw); + @protected + FullscreenInfo dco_decode_fullscreen_info(dynamic raw); + @protected int dco_decode_i_32(dynamic raw); @@ -56,20 +74,50 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_folder_info(dynamic raw); + @protected + Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + Uint64List dco_decode_list_prim_usize_strict(dynamic raw); + @protected List dco_decode_list_project_info(dynamic raw); + @protected + List<(String, String)> dco_decode_list_record_string_string(dynamic raw); + + @protected + List dco_decode_list_service_info(dynamic raw); + @protected String? dco_decode_opt_String(dynamic raw); + @protected + FullscreenInfo? dco_decode_opt_box_autoadd_fullscreen_info(dynamic raw); + + @protected + SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected ProjectInfo dco_decode_project_info(dynamic raw); @protected - ServerTerminalSize dco_decode_server_terminal_size(dynamic raw); + (String, String) dco_decode_record_string_string(dynamic raw); + + @protected + ScrollInfo dco_decode_scroll_info(dynamic raw); + + @protected + SelectionBounds dco_decode_selection_bounds(dynamic raw); + + @protected + ServiceInfo dco_decode_service_info(dynamic raw); @protected int dco_decode_u_16(dynamic raw); @@ -83,15 +131,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void dco_decode_unit(dynamic raw); + @protected + BigInt dco_decode_usize(dynamic raw); + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + @protected + Map sse_decode_Map_String_String_None( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + FullscreenInfo sse_decode_box_autoadd_fullscreen_info( + SseDeserializer deserializer, + ); + + @protected + SelectionBounds sse_decode_box_autoadd_selection_bounds( + SseDeserializer deserializer, + ); + + @protected + int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected CellData sse_decode_cell_data(SseDeserializer deserializer); @@ -104,9 +173,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState sse_decode_cursor_state(SseDeserializer deserializer); + @protected + double sse_decode_f_64(SseDeserializer deserializer); + @protected FolderInfo sse_decode_folder_info(SseDeserializer deserializer); + @protected + FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer); + @protected int sse_decode_i_32(SseDeserializer deserializer); @@ -119,23 +194,59 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_folder_info(SseDeserializer deserializer); + @protected + Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + Uint64List sse_decode_list_prim_usize_strict(SseDeserializer deserializer); + @protected List sse_decode_list_project_info(SseDeserializer deserializer); + @protected + List<(String, String)> sse_decode_list_record_string_string( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_service_info(SseDeserializer deserializer); + @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + FullscreenInfo? sse_decode_opt_box_autoadd_fullscreen_info( + SseDeserializer deserializer, + ); + + @protected + SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( + SseDeserializer deserializer, + ); + + @protected + int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer); @protected - ServerTerminalSize sse_decode_server_terminal_size( + (String, String) sse_decode_record_string_string( SseDeserializer deserializer, ); + @protected + ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); + + @protected + SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); + + @protected + ServiceInfo sse_decode_service_info(SseDeserializer deserializer); + @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -148,18 +259,42 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_decode_unit(SseDeserializer deserializer); + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + @protected void sse_encode_AnyhowException( AnyhowException self, SseSerializer serializer, ); + @protected + void sse_encode_Map_String_String_None( + Map self, + SseSerializer serializer, + ); + @protected void sse_encode_String(String self, SseSerializer serializer); @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_fullscreen_info( + FullscreenInfo self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_selection_bounds( + SelectionBounds self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected void sse_encode_cell_data(CellData self, SseSerializer serializer); @@ -175,9 +310,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_cursor_state(CursorState self, SseSerializer serializer); + @protected + void sse_encode_f_64(double self, SseSerializer serializer); + @protected void sse_encode_folder_info(FolderInfo self, SseSerializer serializer); + @protected + void sse_encode_fullscreen_info( + FullscreenInfo self, + SseSerializer serializer, + ); + @protected void sse_encode_i_32(int self, SseSerializer serializer); @@ -193,30 +337,81 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_prim_u_16_strict( + Uint16List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, SseSerializer serializer, ); + @protected + void sse_encode_list_prim_usize_strict( + Uint64List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_project_info( List self, SseSerializer serializer, ); + @protected + void sse_encode_list_record_string_string( + List<(String, String)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_service_info( + List self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_fullscreen_info( + FullscreenInfo? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_selection_bounds( + SelectionBounds? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); @protected - void sse_encode_server_terminal_size( - ServerTerminalSize self, + void sse_encode_record_string_string( + (String, String) self, SseSerializer serializer, ); + @protected + void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); + + @protected + void sse_encode_selection_bounds( + SelectionBounds self, + SseSerializer serializer, + ); + + @protected + void sse_encode_service_info(ServiceInfo self, SseSerializer serializer); + @protected void sse_encode_u_16(int self, SseSerializer serializer); @@ -228,6 +423,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); } // Section: wire_class diff --git a/mobile/lib/src/rust/frb_generated.web.dart b/mobile/lib/src/rust/frb_generated.web.dart index 88bb0ee5..499c4cba 100644 --- a/mobile/lib/src/rust/frb_generated.web.dart +++ b/mobile/lib/src/rust/frb_generated.web.dart @@ -25,12 +25,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); + @protected + Map dco_decode_Map_String_String_None(dynamic raw); + @protected String dco_decode_String(dynamic raw); @protected bool dco_decode_bool(dynamic raw); + @protected + FullscreenInfo dco_decode_box_autoadd_fullscreen_info(dynamic raw); + + @protected + SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); + + @protected + int dco_decode_box_autoadd_u_32(dynamic raw); + @protected CellData dco_decode_cell_data(dynamic raw); @@ -43,9 +55,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState dco_decode_cursor_state(dynamic raw); + @protected + double dco_decode_f_64(dynamic raw); + @protected FolderInfo dco_decode_folder_info(dynamic raw); + @protected + FullscreenInfo dco_decode_fullscreen_info(dynamic raw); + @protected int dco_decode_i_32(dynamic raw); @@ -58,20 +76,50 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_folder_info(dynamic raw); + @protected + Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + Uint64List dco_decode_list_prim_usize_strict(dynamic raw); + @protected List dco_decode_list_project_info(dynamic raw); + @protected + List<(String, String)> dco_decode_list_record_string_string(dynamic raw); + + @protected + List dco_decode_list_service_info(dynamic raw); + @protected String? dco_decode_opt_String(dynamic raw); + @protected + FullscreenInfo? dco_decode_opt_box_autoadd_fullscreen_info(dynamic raw); + + @protected + SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected ProjectInfo dco_decode_project_info(dynamic raw); @protected - ServerTerminalSize dco_decode_server_terminal_size(dynamic raw); + (String, String) dco_decode_record_string_string(dynamic raw); + + @protected + ScrollInfo dco_decode_scroll_info(dynamic raw); + + @protected + SelectionBounds dco_decode_selection_bounds(dynamic raw); + + @protected + ServiceInfo dco_decode_service_info(dynamic raw); @protected int dco_decode_u_16(dynamic raw); @@ -85,15 +133,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void dco_decode_unit(dynamic raw); + @protected + BigInt dco_decode_usize(dynamic raw); + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + @protected + Map sse_decode_Map_String_String_None( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + FullscreenInfo sse_decode_box_autoadd_fullscreen_info( + SseDeserializer deserializer, + ); + + @protected + SelectionBounds sse_decode_box_autoadd_selection_bounds( + SseDeserializer deserializer, + ); + + @protected + int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected CellData sse_decode_cell_data(SseDeserializer deserializer); @@ -106,9 +175,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState sse_decode_cursor_state(SseDeserializer deserializer); + @protected + double sse_decode_f_64(SseDeserializer deserializer); + @protected FolderInfo sse_decode_folder_info(SseDeserializer deserializer); + @protected + FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer); + @protected int sse_decode_i_32(SseDeserializer deserializer); @@ -121,23 +196,59 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_folder_info(SseDeserializer deserializer); + @protected + Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + Uint64List sse_decode_list_prim_usize_strict(SseDeserializer deserializer); + @protected List sse_decode_list_project_info(SseDeserializer deserializer); + @protected + List<(String, String)> sse_decode_list_record_string_string( + SseDeserializer deserializer, + ); + + @protected + List sse_decode_list_service_info(SseDeserializer deserializer); + @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + FullscreenInfo? sse_decode_opt_box_autoadd_fullscreen_info( + SseDeserializer deserializer, + ); + + @protected + SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( + SseDeserializer deserializer, + ); + + @protected + int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer); @protected - ServerTerminalSize sse_decode_server_terminal_size( + (String, String) sse_decode_record_string_string( SseDeserializer deserializer, ); + @protected + ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); + + @protected + SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); + + @protected + ServiceInfo sse_decode_service_info(SseDeserializer deserializer); + @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -150,18 +261,42 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_decode_unit(SseDeserializer deserializer); + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + @protected void sse_encode_AnyhowException( AnyhowException self, SseSerializer serializer, ); + @protected + void sse_encode_Map_String_String_None( + Map self, + SseSerializer serializer, + ); + @protected void sse_encode_String(String self, SseSerializer serializer); @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_fullscreen_info( + FullscreenInfo self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_selection_bounds( + SelectionBounds self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected void sse_encode_cell_data(CellData self, SseSerializer serializer); @@ -177,9 +312,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_cursor_state(CursorState self, SseSerializer serializer); + @protected + void sse_encode_f_64(double self, SseSerializer serializer); + @protected void sse_encode_folder_info(FolderInfo self, SseSerializer serializer); + @protected + void sse_encode_fullscreen_info( + FullscreenInfo self, + SseSerializer serializer, + ); + @protected void sse_encode_i_32(int self, SseSerializer serializer); @@ -195,30 +339,81 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_prim_u_16_strict( + Uint16List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, SseSerializer serializer, ); + @protected + void sse_encode_list_prim_usize_strict( + Uint64List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_project_info( List self, SseSerializer serializer, ); + @protected + void sse_encode_list_record_string_string( + List<(String, String)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_service_info( + List self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_fullscreen_info( + FullscreenInfo? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_selection_bounds( + SelectionBounds? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); @protected - void sse_encode_server_terminal_size( - ServerTerminalSize self, + void sse_encode_record_string_string( + (String, String) self, SseSerializer serializer, ); + @protected + void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); + + @protected + void sse_encode_selection_bounds( + SelectionBounds self, + SseSerializer serializer, + ); + + @protected + void sse_encode_service_info(ServiceInfo self, SseSerializer serializer); + @protected void sse_encode_u_16(int self, SseSerializer serializer); @@ -230,6 +425,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); } // Section: wire_class diff --git a/mobile/native/src/frb_generated.rs b/mobile/native/src/frb_generated.rs index 3e3f9fb6..64258e7f 100644 --- a/mobile/native/src/frb_generated.rs +++ b/mobile/native/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1973712882; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -142339940; // Section: executor @@ -45,6 +45,45 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs +fn wire__crate__api__state__add_project_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "add_project", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + let api_path = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::add_project(api_conn_id, api_name, api_path).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__terminal__clear_selection_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -121,6 +160,49 @@ fn wire__crate__api__state__close_terminal_impl( }, ) } +fn wire__crate__api__state__close_terminals_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "close_terminals", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_terminal_ids = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::close_terminals( + api_conn_id, + api_project_id, + api_terminal_ids, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__connection__connect_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -258,6 +340,49 @@ fn wire__crate__api__connection__disconnect_impl( }, ) } +fn wire__crate__api__state__focus_terminal_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "focus_terminal", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::focus_terminal( + api_conn_id, + api_project_id, + api_terminal_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__state__get_all_terminal_ids_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -354,14 +479,14 @@ fn wire__crate__api__state__get_focused_project_id_impl( }, ) } -fn wire__crate__api__state__get_projects_impl( +fn wire__crate__api__state__get_folders_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_projects", + debug_name: "get_folders", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -378,20 +503,20 @@ fn wire__crate__api__state__get_projects_impl( let api_conn_id = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::state::get_projects(api_conn_id))?; + let output_ok = Result::<_, ()>::Ok(crate::api::state::get_folders(api_conn_id))?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__terminal__get_scroll_info_impl( +fn wire__crate__api__state__get_fullscreen_terminal_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_scroll_info", + debug_name: "get_fullscreen_terminal", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -406,26 +531,23 @@ fn wire__crate__api__terminal__get_scroll_info_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_scroll_info( - api_conn_id, - api_terminal_id, - ))?; + let output_ok = + Result::<_, ()>::Ok(crate::api::state::get_fullscreen_terminal(api_conn_id))?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__terminal__get_selected_text_impl( +fn wire__crate__api__state__get_project_layout_json_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_selected_text", + debug_name: "get_project_layout_json", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -440,26 +562,26 @@ fn wire__crate__api__terminal__get_selected_text_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selected_text( + let output_ok = Result::<_, ()>::Ok(crate::api::state::get_project_layout_json( api_conn_id, - api_terminal_id, + api_project_id, ))?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__terminal__get_selection_bounds_impl( +fn wire__crate__api__state__get_project_order_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_selection_bounds", + debug_name: "get_project_order", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -474,26 +596,23 @@ fn wire__crate__api__terminal__get_selection_bounds_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selection_bounds( - api_conn_id, - api_terminal_id, - ))?; + let output_ok = + Result::<_, ()>::Ok(crate::api::state::get_project_order(api_conn_id))?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__connection__get_token_impl( +fn wire__crate__api__state__get_projects_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_token", + debug_name: "get_projects", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -510,21 +629,20 @@ fn wire__crate__api__connection__get_token_impl( let api_conn_id = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::connection::get_token(api_conn_id))?; + let output_ok = Result::<_, ()>::Ok(crate::api::state::get_projects(api_conn_id))?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__terminal__get_visible_cells_impl( +fn wire__crate__api__terminal__get_scroll_info_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_visible_cells", + debug_name: "get_scroll_info", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -542,7 +660,7 @@ fn wire__crate__api__terminal__get_visible_cells_impl( let api_terminal_id = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_visible_cells( + let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_scroll_info( api_conn_id, api_terminal_id, ))?; @@ -551,17 +669,16 @@ fn wire__crate__api__terminal__get_visible_cells_impl( }, ) } -fn wire__crate__api__connection__init_app_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, +fn wire__crate__api__terminal__get_selected_text_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "init_app", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + debug_name: "get_selected_text", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, move || { let message = unsafe { @@ -573,26 +690,27 @@ fn wire__crate__api__connection__init_app_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); deserializer.end(); - move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::connection::init_app(); - })?; - Ok(output_ok) - })()) - } + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selected_text( + api_conn_id, + api_terminal_id, + ))?; + Ok(output_ok) + })()) }, ) } -fn wire__crate__api__state__is_dirty_impl( +fn wire__crate__api__terminal__get_selection_bounds_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "is_dirty", + debug_name: "get_selection_bounds", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -610,24 +728,25 @@ fn wire__crate__api__state__is_dirty_impl( let api_terminal_id = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::is_dirty(api_conn_id, api_terminal_id))?; + let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selection_bounds( + api_conn_id, + api_terminal_id, + ))?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__connection__pair_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, +fn wire__crate__api__connection__get_token_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "pair", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + debug_name: "get_token", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, move || { let message = unsafe { @@ -640,28 +759,23 @@ fn wire__crate__api__connection__pair_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); - let api_code = ::sse_decode(&mut deserializer); deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::connection::pair(api_conn_id, api_code).await?; - Ok(output_ok) - })() - .await, - ) - } + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::connection::get_token(api_conn_id))?; + Ok(output_ok) + })()) }, ) } -fn wire__crate__api__terminal__resize_terminal_impl( +fn wire__crate__api__terminal__get_visible_cells_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "resize_terminal", + debug_name: "get_visible_cells", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -677,33 +791,28 @@ fn wire__crate__api__terminal__resize_terminal_impl( flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); let api_terminal_id = ::sse_decode(&mut deserializer); - let api_cols = ::sse_decode(&mut deserializer); - let api_rows = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::resize_terminal( - api_conn_id, - api_terminal_id, - api_cols, - api_rows, - ); - })?; + let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_visible_cells( + api_conn_id, + api_terminal_id, + ))?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__terminal__scroll_impl( +fn wire__crate__api__state__git_branches_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "scroll", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + debug_name: "git_branches", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, move || { let message = unsafe { @@ -716,28 +825,691 @@ fn wire__crate__api__terminal__scroll_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_delta = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::scroll(api_conn_id, api_terminal_id, api_delta); - })?; - Ok(output_ok) - })()) + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::git_branches(api_conn_id, api_project_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__git_diff_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "git_diff", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_mode = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::git_diff(api_conn_id, api_project_id, api_mode) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__git_diff_summary_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "git_diff_summary", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::git_diff_summary(api_conn_id, api_project_id) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__git_status_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "git_status", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::git_status(api_conn_id, api_project_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__connection__init_app_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "init_app", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::connection::init_app(); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__state__is_dirty_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "is_dirty", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::state::is_dirty(api_conn_id, api_terminal_id))?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__connection__pair_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "pair", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_code = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::connection::pair(api_conn_id, api_code).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__read_content_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "read_content", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::read_content(api_conn_id, api_terminal_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__reload_services_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "reload_services", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::reload_services(api_conn_id, api_project_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__rename_terminal_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rename_terminal", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::rename_terminal( + api_conn_id, + api_project_id, + api_terminal_id, + api_name, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__terminal__resize_terminal_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "resize_terminal", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_cols = ::sse_decode(&mut deserializer); + let api_rows = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::terminal::resize_terminal( + api_conn_id, + api_terminal_id, + api_cols, + api_rows, + ); + })?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__state__restart_service_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "restart_service", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_service_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::restart_service( + api_conn_id, + api_project_id, + api_service_name, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__run_command_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "run_command", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_command = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::run_command( + api_conn_id, + api_terminal_id, + api_command, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__terminal__scroll_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "scroll", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_delta = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::terminal::scroll(api_conn_id, api_terminal_id, api_delta); + })?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__connection__seconds_since_activity_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "seconds_since_activity", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::connection::seconds_since_activity(api_conn_id), + )?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__state__send_special_key_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "send_special_key", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_key = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::send_special_key( + api_conn_id, + api_terminal_id, + api_key, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__terminal__send_text_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "send_text", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_text = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::terminal::send_text(api_conn_id, api_terminal_id, api_text) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__set_folder_color_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_folder_color", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_folder_id = ::sse_decode(&mut deserializer); + let api_color = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::set_folder_color( + api_conn_id, + api_folder_id, + api_color, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } }, ) } -fn wire__crate__api__connection__seconds_since_activity_impl( +fn wire__crate__api__state__set_fullscreen_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "seconds_since_activity", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + debug_name: "set_fullscreen", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, move || { let message = unsafe { @@ -750,17 +1522,27 @@ fn wire__crate__api__connection__seconds_since_activity_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_terminal_id = >::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok( - crate::api::connection::seconds_since_activity(api_conn_id), - )?; - Ok(output_ok) - })()) + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::set_fullscreen( + api_conn_id, + api_project_id, + api_terminal_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } }, ) } -fn wire__crate__api__state__send_special_key_impl( +fn wire__crate__api__state__set_project_color_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -768,7 +1550,7 @@ fn wire__crate__api__state__send_special_key_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "send_special_key", + debug_name: "set_project_color", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -783,16 +1565,16 @@ fn wire__crate__api__state__send_special_key_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_key = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_color = ::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( (move || async move { - let output_ok = crate::api::state::send_special_key( + let output_ok = crate::api::state::set_project_color( api_conn_id, - api_terminal_id, - api_key, + api_project_id, + api_color, ) .await?; Ok(output_ok) @@ -803,7 +1585,7 @@ fn wire__crate__api__state__send_special_key_impl( }, ) } -fn wire__crate__api__terminal__send_text_impl( +fn wire__crate__api__state__split_terminal_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -811,7 +1593,7 @@ fn wire__crate__api__terminal__send_text_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "send_text", + debug_name: "split_terminal", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -826,14 +1608,58 @@ fn wire__crate__api__terminal__send_text_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_text = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_path = >::sse_decode(&mut deserializer); + let api_direction = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::split_terminal( + api_conn_id, + api_project_id, + api_path, + api_direction, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__start_all_services_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "start_all_services", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( (move || async move { let output_ok = - crate::api::terminal::send_text(api_conn_id, api_terminal_id, api_text) + crate::api::state::start_all_services(api_conn_id, api_project_id) .await?; Ok(output_ok) })() @@ -883,16 +1709,182 @@ fn wire__crate__api__terminal__start_selection_impl( }, ) } -fn wire__crate__api__terminal__start_word_selection_impl( +fn wire__crate__api__state__start_service_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "start_service", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_service_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::start_service( + api_conn_id, + api_project_id, + api_service_name, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__terminal__start_word_selection_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "start_word_selection", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_col = ::sse_decode(&mut deserializer); + let api_row = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::terminal::start_word_selection( + api_conn_id, + api_terminal_id, + api_col, + api_row, + ); + })?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__state__stop_all_services_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stop_all_services", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::state::stop_all_services(api_conn_id, api_project_id) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__stop_service_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "stop_service", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_service_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::stop_service( + api_conn_id, + api_project_id, + api_service_name, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__toggle_minimized_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "start_word_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + debug_name: "toggle_minimized", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, move || { let message = unsafe { @@ -905,21 +1897,23 @@ fn wire__crate__api__terminal__start_word_selection_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); let api_terminal_id = ::sse_decode(&mut deserializer); - let api_col = ::sse_decode(&mut deserializer); - let api_row = ::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::start_word_selection( - api_conn_id, - api_terminal_id, - api_col, - api_row, - ); - })?; - Ok(output_ok) - })()) + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::toggle_minimized( + api_conn_id, + api_project_id, + api_terminal_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } }, ) } @@ -1079,6 +2073,34 @@ impl SseDecode for f64 { } } +impl SseDecode for crate::api::state::FolderInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); + let mut var_name = ::sse_decode(deserializer); + let mut var_projectIds = >::sse_decode(deserializer); + let mut var_folderColor = ::sse_decode(deserializer); + return crate::api::state::FolderInfo { + id: var_id, + name: var_name, + project_ids: var_projectIds, + folder_color: var_folderColor, + }; + } +} + +impl SseDecode for crate::api::state::FullscreenInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_projectId = ::sse_decode(deserializer); + let mut var_terminalId = ::sse_decode(deserializer); + return crate::api::state::FullscreenInfo { + project_id: var_projectId, + terminal_id: var_terminalId, + }; + } +} + impl SseDecode for i32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1110,6 +2132,30 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1122,6 +2168,18 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1146,6 +2204,18 @@ impl SseDecode for Vec<(String, String)> { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1157,6 +2227,19 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1170,24 +2253,43 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for crate::api::state::ProjectInfo { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut var_id = ::sse_decode(deserializer); let mut var_name = ::sse_decode(deserializer); let mut var_path = ::sse_decode(deserializer); - let mut var_isVisible = ::sse_decode(deserializer); + let mut var_showInOverview = ::sse_decode(deserializer); let mut var_terminalIds = >::sse_decode(deserializer); let mut var_terminalNames = >::sse_decode(deserializer); + let mut var_gitBranch = >::sse_decode(deserializer); + let mut var_gitLinesAdded = ::sse_decode(deserializer); + let mut var_gitLinesRemoved = ::sse_decode(deserializer); + let mut var_services = >::sse_decode(deserializer); let mut var_folderColor = ::sse_decode(deserializer); return crate::api::state::ProjectInfo { id: var_id, name: var_name, path: var_path, - show_in_overview: var_isVisible, + show_in_overview: var_showInOverview, terminal_ids: var_terminalIds, terminal_names: var_terminalNames, + git_branch: var_gitBranch, + git_lines_added: var_gitLinesAdded, + git_lines_removed: var_gitLinesRemoved, + services: var_services, folder_color: var_folderColor, }; } @@ -1232,6 +2334,28 @@ impl SseDecode for crate::api::terminal::SelectionBounds { } } +impl SseDecode for crate::api::state::ServiceInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_name = ::sse_decode(deserializer); + let mut var_status = ::sse_decode(deserializer); + let mut var_terminalId = >::sse_decode(deserializer); + let mut var_ports = >::sse_decode(deserializer); + let mut var_exitCode = >::sse_decode(deserializer); + let mut var_kind = ::sse_decode(deserializer); + let mut var_isExtra = ::sse_decode(deserializer); + return crate::api::state::ServiceInfo { + name: var_name, + status: var_status, + terminal_id: var_terminalId, + ports: var_ports, + exit_code: var_exitCode, + kind: var_kind, + is_extra: var_isExtra, + }; + } +} + impl SseDecode for u16 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1258,6 +2382,13 @@ impl SseDecode for () { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} } +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + fn pde_ffi_dispatcher_primary_impl( func_id: i32, port: flutter_rust_bridge::for_generated::MessagePort, @@ -1267,12 +2398,33 @@ fn pde_ffi_dispatcher_primary_impl( ) { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 2 => wire__crate__api__state__close_terminal_impl(port, ptr, rust_vec_len, data_len), - 5 => wire__crate__api__state__create_terminal_impl(port, ptr, rust_vec_len, data_len), - 16 => wire__crate__api__connection__init_app_impl(port, ptr, rust_vec_len, data_len), - 18 => wire__crate__api__connection__pair_impl(port, ptr, rust_vec_len, data_len), - 22 => wire__crate__api__state__send_special_key_impl(port, ptr, rust_vec_len, data_len), - 23 => wire__crate__api__terminal__send_text_impl(port, ptr, rust_vec_len, data_len), + 1 => wire__crate__api__state__add_project_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__api__state__close_terminal_impl(port, ptr, rust_vec_len, data_len), + 4 => wire__crate__api__state__close_terminals_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__api__state__create_terminal_impl(port, ptr, rust_vec_len, data_len), + 9 => wire__crate__api__state__focus_terminal_impl(port, ptr, rust_vec_len, data_len), + 23 => wire__crate__api__state__git_branches_impl(port, ptr, rust_vec_len, data_len), + 24 => wire__crate__api__state__git_diff_impl(port, ptr, rust_vec_len, data_len), + 25 => wire__crate__api__state__git_diff_summary_impl(port, ptr, rust_vec_len, data_len), + 26 => wire__crate__api__state__git_status_impl(port, ptr, rust_vec_len, data_len), + 27 => wire__crate__api__connection__init_app_impl(port, ptr, rust_vec_len, data_len), + 29 => wire__crate__api__connection__pair_impl(port, ptr, rust_vec_len, data_len), + 30 => wire__crate__api__state__read_content_impl(port, ptr, rust_vec_len, data_len), + 31 => wire__crate__api__state__reload_services_impl(port, ptr, rust_vec_len, data_len), + 32 => wire__crate__api__state__rename_terminal_impl(port, ptr, rust_vec_len, data_len), + 34 => wire__crate__api__state__restart_service_impl(port, ptr, rust_vec_len, data_len), + 35 => wire__crate__api__state__run_command_impl(port, ptr, rust_vec_len, data_len), + 38 => wire__crate__api__state__send_special_key_impl(port, ptr, rust_vec_len, data_len), + 39 => wire__crate__api__terminal__send_text_impl(port, ptr, rust_vec_len, data_len), + 40 => wire__crate__api__state__set_folder_color_impl(port, ptr, rust_vec_len, data_len), + 41 => wire__crate__api__state__set_fullscreen_impl(port, ptr, rust_vec_len, data_len), + 42 => wire__crate__api__state__set_project_color_impl(port, ptr, rust_vec_len, data_len), + 43 => wire__crate__api__state__split_terminal_impl(port, ptr, rust_vec_len, data_len), + 44 => wire__crate__api__state__start_all_services_impl(port, ptr, rust_vec_len, data_len), + 46 => wire__crate__api__state__start_service_impl(port, ptr, rust_vec_len, data_len), + 48 => wire__crate__api__state__stop_all_services_impl(port, ptr, rust_vec_len, data_len), + 49 => wire__crate__api__state__stop_service_impl(port, ptr, rust_vec_len, data_len), + 50 => wire__crate__api__state__toggle_minimized_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1285,28 +2437,32 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 1 => wire__crate__api__terminal__clear_selection_impl(ptr, rust_vec_len, data_len), - 3 => wire__crate__api__connection__connect_impl(ptr, rust_vec_len, data_len), - 4 => wire__crate__api__connection__connection_status_impl(ptr, rust_vec_len, data_len), - 6 => wire__crate__api__connection__disconnect_impl(ptr, rust_vec_len, data_len), - 7 => wire__crate__api__state__get_all_terminal_ids_impl(ptr, rust_vec_len, data_len), - 8 => wire__crate__api__terminal__get_cursor_impl(ptr, rust_vec_len, data_len), - 9 => wire__crate__api__state__get_focused_project_id_impl(ptr, rust_vec_len, data_len), - 10 => wire__crate__api__state__get_projects_impl(ptr, rust_vec_len, data_len), - 11 => wire__crate__api__terminal__get_scroll_info_impl(ptr, rust_vec_len, data_len), - 12 => wire__crate__api__terminal__get_selected_text_impl(ptr, rust_vec_len, data_len), - 13 => wire__crate__api__terminal__get_selection_bounds_impl(ptr, rust_vec_len, data_len), - 14 => wire__crate__api__connection__get_token_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__api__terminal__get_visible_cells_impl(ptr, rust_vec_len, data_len), - 17 => wire__crate__api__state__is_dirty_impl(ptr, rust_vec_len, data_len), - 19 => wire__crate__api__terminal__resize_terminal_impl(ptr, rust_vec_len, data_len), - 20 => wire__crate__api__terminal__scroll_impl(ptr, rust_vec_len, data_len), - 21 => { + 2 => wire__crate__api__terminal__clear_selection_impl(ptr, rust_vec_len, data_len), + 5 => wire__crate__api__connection__connect_impl(ptr, rust_vec_len, data_len), + 6 => wire__crate__api__connection__connection_status_impl(ptr, rust_vec_len, data_len), + 8 => wire__crate__api__connection__disconnect_impl(ptr, rust_vec_len, data_len), + 10 => wire__crate__api__state__get_all_terminal_ids_impl(ptr, rust_vec_len, data_len), + 11 => wire__crate__api__terminal__get_cursor_impl(ptr, rust_vec_len, data_len), + 12 => wire__crate__api__state__get_focused_project_id_impl(ptr, rust_vec_len, data_len), + 13 => wire__crate__api__state__get_folders_impl(ptr, rust_vec_len, data_len), + 14 => wire__crate__api__state__get_fullscreen_terminal_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__api__state__get_project_layout_json_impl(ptr, rust_vec_len, data_len), + 16 => wire__crate__api__state__get_project_order_impl(ptr, rust_vec_len, data_len), + 17 => wire__crate__api__state__get_projects_impl(ptr, rust_vec_len, data_len), + 18 => wire__crate__api__terminal__get_scroll_info_impl(ptr, rust_vec_len, data_len), + 19 => wire__crate__api__terminal__get_selected_text_impl(ptr, rust_vec_len, data_len), + 20 => wire__crate__api__terminal__get_selection_bounds_impl(ptr, rust_vec_len, data_len), + 21 => wire__crate__api__connection__get_token_impl(ptr, rust_vec_len, data_len), + 22 => wire__crate__api__terminal__get_visible_cells_impl(ptr, rust_vec_len, data_len), + 28 => wire__crate__api__state__is_dirty_impl(ptr, rust_vec_len, data_len), + 33 => wire__crate__api__terminal__resize_terminal_impl(ptr, rust_vec_len, data_len), + 36 => wire__crate__api__terminal__scroll_impl(ptr, rust_vec_len, data_len), + 37 => { wire__crate__api__connection__seconds_since_activity_impl(ptr, rust_vec_len, data_len) } - 24 => wire__crate__api__terminal__start_selection_impl(ptr, rust_vec_len, data_len), - 25 => wire__crate__api__terminal__start_word_selection_impl(ptr, rust_vec_len, data_len), - 26 => wire__crate__api__terminal__update_selection_impl(ptr, rust_vec_len, data_len), + 45 => wire__crate__api__terminal__start_selection_impl(ptr, rust_vec_len, data_len), + 47 => wire__crate__api__terminal__start_word_selection_impl(ptr, rust_vec_len, data_len), + 51 => wire__crate__api__terminal__update_selection_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1410,6 +2566,47 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::state::FolderInfo { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.id.into_into_dart().into_dart(), + self.name.into_into_dart().into_dart(), + self.project_ids.into_into_dart().into_dart(), + self.folder_color.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::state::FolderInfo {} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::state::FolderInfo +{ + fn into_into_dart(self) -> crate::api::state::FolderInfo { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::state::FullscreenInfo { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.project_id.into_into_dart().into_dart(), + self.terminal_id.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::state::FullscreenInfo +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::state::FullscreenInfo +{ + fn into_into_dart(self) -> crate::api::state::FullscreenInfo { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::state::ProjectInfo { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -1419,6 +2616,10 @@ impl flutter_rust_bridge::IntoDart for crate::api::state::ProjectInfo { self.show_in_overview.into_into_dart().into_dart(), self.terminal_ids.into_into_dart().into_dart(), self.terminal_names.into_into_dart().into_dart(), + self.git_branch.into_into_dart().into_dart(), + self.git_lines_added.into_into_dart().into_dart(), + self.git_lines_removed.into_into_dart().into_dart(), + self.services.into_into_dart().into_dart(), self.folder_color.into_into_dart().into_dart(), ] .into_dart() @@ -1480,6 +2681,32 @@ impl flutter_rust_bridge::IntoIntoDart self } } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::state::ServiceInfo { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.name.into_into_dart().into_dart(), + self.status.into_into_dart().into_dart(), + self.terminal_id.into_into_dart().into_dart(), + self.ports.into_into_dart().into_dart(), + self.exit_code.into_into_dart().into_dart(), + self.kind.into_into_dart().into_dart(), + self.is_extra.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::state::ServiceInfo +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::state::ServiceInfo +{ + fn into_into_dart(self) -> crate::api::state::ServiceInfo { + self + } +} impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1580,6 +2807,24 @@ impl SseEncode for f64 { } } +impl SseEncode for crate::api::state::FolderInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); + ::sse_encode(self.name, serializer); + >::sse_encode(self.project_ids, serializer); + ::sse_encode(self.folder_color, serializer); + } +} + +impl SseEncode for crate::api::state::FullscreenInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.project_id, serializer); + ::sse_encode(self.terminal_id, serializer); + } +} + impl SseEncode for i32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1607,6 +2852,26 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1617,6 +2882,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1637,6 +2912,16 @@ impl SseEncode for Vec<(String, String)> { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1647,6 +2932,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1657,6 +2952,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for crate::api::state::ProjectInfo { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1666,6 +2971,10 @@ impl SseEncode for crate::api::state::ProjectInfo { ::sse_encode(self.show_in_overview, serializer); >::sse_encode(self.terminal_ids, serializer); >::sse_encode(self.terminal_names, serializer); + >::sse_encode(self.git_branch, serializer); + ::sse_encode(self.git_lines_added, serializer); + ::sse_encode(self.git_lines_removed, serializer); + >::sse_encode(self.services, serializer); ::sse_encode(self.folder_color, serializer); } } @@ -1697,6 +3006,19 @@ impl SseEncode for crate::api::terminal::SelectionBounds { } } +impl SseEncode for crate::api::state::ServiceInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.name, serializer); + ::sse_encode(self.status, serializer); + >::sse_encode(self.terminal_id, serializer); + >::sse_encode(self.ports, serializer); + >::sse_encode(self.exit_code, serializer); + ::sse_encode(self.kind, serializer); + ::sse_encode(self.is_extra, serializer); + } +} + impl SseEncode for u16 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1723,6 +3045,16 @@ impl SseEncode for () { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} } +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + #[cfg(not(target_family = "wasm"))] mod io { // This file is automatically generated, so please do not edit it. From ea55a5728af078629f72b22b7dfbe08d6ce97156 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 11:10:21 +0200 Subject: [PATCH 17/37] feat(mobile): add fullscreen, services, git status, and layout management UI Co-Authored-By: Claude Opus 4.6 --- .../lib/src/providers/workspace_provider.dart | 56 +- mobile/lib/src/screens/workspace_screen.dart | 1126 +++++++++++++---- mobile/lib/src/widgets/project_drawer.dart | 873 ++++++++----- 3 files changed, 1462 insertions(+), 593 deletions(-) diff --git a/mobile/lib/src/providers/workspace_provider.dart b/mobile/lib/src/providers/workspace_provider.dart index 21b95545..99b9f235 100644 --- a/mobile/lib/src/providers/workspace_provider.dart +++ b/mobile/lib/src/providers/workspace_provider.dart @@ -11,6 +11,7 @@ class WorkspaceProvider extends ChangeNotifier { List _projects = []; List _folders = []; List _projectOrder = []; + ffi.FullscreenInfo? _fullscreenTerminal; String? _selectedProjectId; String? _selectedTerminalId; Set? _previousTerminalIds; @@ -20,6 +21,7 @@ class WorkspaceProvider extends ChangeNotifier { List get projects => _projects; List get folders => _folders; List get projectOrder => _projectOrder; + ffi.FullscreenInfo? get fullscreenTerminal => _fullscreenTerminal; String? get selectedProjectId => _selectedProjectId; String? get selectedTerminalId => _selectedTerminalId; double get secondsSinceActivity => _secondsSinceActivity; @@ -50,6 +52,14 @@ class WorkspaceProvider extends ChangeNotifier { notifyListeners(); } + /// Get the layout JSON for the selected project. + String? getProjectLayoutJson() { + final connId = _connection.connId; + final projectId = _selectedProjectId ?? selectedProject?.id; + if (connId == null || projectId == null) return null; + return ffi.getProjectLayoutJson(connId: connId, projectId: projectId); + } + void _onConnectionChanged() { if (_connection.isConnected) { _startPolling(); @@ -58,6 +68,7 @@ class WorkspaceProvider extends ChangeNotifier { _projects = []; _folders = []; _projectOrder = []; + _fullscreenTerminal = null; _selectedProjectId = null; _selectedTerminalId = null; notifyListeners(); @@ -84,9 +95,10 @@ class WorkspaceProvider extends ChangeNotifier { if (connId == null) return; final newProjects = ffi.getProjects(connId: connId); + final focusedId = ffi.getFocusedProjectId(connId: connId); final newFolders = ffi.getFolders(connId: connId); final newProjectOrder = ffi.getProjectOrder(connId: connId); - final focusedId = ffi.getFocusedProjectId(connId: connId); + final newFullscreen = ffi.getFullscreenTerminal(connId: connId); bool changed = false; @@ -95,16 +107,22 @@ class WorkspaceProvider extends ChangeNotifier { changed = true; } - if (!_folderListEquals(newFolders, _folders)) { + if (!listEquals(newFolders.map((f) => f.id).toList(), + _folders.map((f) => f.id).toList())) { _folders = newFolders; changed = true; } - if (!_stringListEquals(newProjectOrder, _projectOrder)) { + if (!listEquals(newProjectOrder, _projectOrder)) { _projectOrder = newProjectOrder; changed = true; } + if (newFullscreen?.terminalId != _fullscreenTerminal?.terminalId) { + _fullscreenTerminal = newFullscreen; + changed = true; + } + // Auto-select the focused project if we don't have a selection if (_selectedProjectId == null && focusedId != null) { _selectedProjectId = focusedId; @@ -153,28 +171,22 @@ class WorkspaceProvider extends ChangeNotifier { bool _projectListEquals( List a, List b) { if (a.length != b.length) return false; - for (int i = 0; i < a.length; i++) { - if (a[i].id != b[i].id || - a[i].name != b[i].name || - a[i].folderColor != b[i].folderColor) return false; - } - return true; - } - - bool _folderListEquals( - List a, List b) { - if (a.length != b.length) return false; for (int i = 0; i < a.length; i++) { if (a[i].id != b[i].id || a[i].name != b[i].name) return false; if (!listEquals(a[i].terminalIds, b[i].terminalIds)) return false; - } - return true; - } - - bool _stringListEquals(List a, List b) { - if (a.length != b.length) return false; - for (int i = 0; i < a.length; i++) { - if (a[i] != b[i]) return false; + // Check git status changes + if (a[i].gitBranch != b[i].gitBranch) return false; + if (a[i].gitLinesAdded != b[i].gitLinesAdded) return false; + if (a[i].gitLinesRemoved != b[i].gitLinesRemoved) return false; + // Check services changes + if (a[i].services.length != b[i].services.length) return false; + for (int j = 0; j < a[i].services.length; j++) { + if (a[i].services[j].name != b[i].services[j].name || + a[i].services[j].status != b[i].services[j].status) { + return false; + } + } + if (a[i].folderColor != b[i].folderColor) return false; } return true; } diff --git a/mobile/lib/src/screens/workspace_screen.dart b/mobile/lib/src/screens/workspace_screen.dart index 06ee5324..775cb841 100644 --- a/mobile/lib/src/screens/workspace_screen.dart +++ b/mobile/lib/src/screens/workspace_screen.dart @@ -1,130 +1,358 @@ -import 'dart:ui'; +import 'dart:convert'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../providers/connection_provider.dart'; import '../providers/workspace_provider.dart'; import '../rust/api/state.dart' as state_ffi; import '../widgets/project_drawer.dart'; -import '../widgets/key_toolbar.dart' show KeyToolbar, KeyModifiers; +import '../widgets/key_toolbar.dart'; import '../widgets/terminal_view.dart'; -import '../theme/app_theme.dart'; -class WorkspaceScreen extends StatefulWidget { +class WorkspaceScreen extends StatelessWidget { const WorkspaceScreen({super.key}); @override - State createState() => _WorkspaceScreenState(); + Widget build(BuildContext context) { + final workspace = context.watch(); + final connection = context.watch(); + final project = workspace.selectedProject; + final connId = connection.connId; + final selectedTerminalId = workspace.selectedTerminalId; + + return Scaffold( + appBar: AppBar( + title: _ProjectSwitcher( + projects: workspace.projects, + selectedProjectId: workspace.selectedProjectId, + onSelect: (id) => workspace.selectProject(id), + ), + leading: Builder( + builder: (ctx) => IconButton( + icon: const Icon(Icons.menu), + onPressed: () => Scaffold.of(ctx).openDrawer(), + ), + ), + actions: [ + // Connection quality indicator + if (connId != null) + Padding( + padding: const EdgeInsets.only(right: 4), + child: _ConnectionDot( + secondsSinceActivity: workspace.secondsSinceActivity, + ), + ), + // Git status button + if (connId != null && project != null && project.gitBranch != null) + _GitButton(connId: connId, project: project), + // Services button + if (connId != null && + project != null && + project.services.isNotEmpty) + _ServicesButton(connId: connId, project: project), + // Fullscreen toggle + if (connId != null && project != null && selectedTerminalId != null) + IconButton( + icon: Icon( + workspace.fullscreenTerminal != null + ? Icons.fullscreen_exit + : Icons.fullscreen, + size: 20, + ), + tooltip: workspace.fullscreenTerminal != null + ? 'Exit Fullscreen' + : 'Fullscreen', + onPressed: () { + if (workspace.fullscreenTerminal != null) { + state_ffi.setFullscreen( + connId: connId, + projectId: project.id, + terminalId: null, + ); + } else { + state_ffi.setFullscreen( + connId: connId, + projectId: project.id, + terminalId: selectedTerminalId, + ); + } + }, + ), + if (connId != null && project != null) + IconButton( + icon: const Icon(Icons.add), + tooltip: 'New Terminal', + onPressed: () { + state_ffi.createTerminal( + connId: connId, + projectId: project.id, + ); + }, + ), + ], + ), + drawer: const ProjectDrawer(), + body: connId == null || project == null + ? const Center(child: Text('No project selected')) + : selectedTerminalId == null + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'No terminals', + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: () { + state_ffi.createTerminal( + connId: connId, + projectId: project.id, + ); + }, + icon: const Icon(Icons.add), + label: const Text('New Terminal'), + ), + ], + ), + ) + : Column( + children: [ + if (project.terminalIds.length > 1) + _TerminalTabBar( + terminalIds: project.terminalIds, + terminalNames: project.terminalNames, + selectedTerminalId: selectedTerminalId, + projectId: project.id, + connId: connId, + onSelect: (id) => workspace.selectTerminal(id), + ), + Expanded( + child: TerminalView( + connId: connId, + terminalId: selectedTerminalId, + onTerminalSwipe: (direction) { + final ids = project.terminalIds; + if (ids.length <= 1) return; + final idx = ids.indexOf(selectedTerminalId); + if (idx < 0) return; + final newIdx = + (idx + direction).clamp(0, ids.length - 1); + if (newIdx != idx) { + workspace.selectTerminal(ids[newIdx]); + } + }, + ), + ), + KeyToolbar( + connId: connId, + terminalId: selectedTerminalId, + ), + ], + ), + ); + } } -class _WorkspaceScreenState extends State { - late PageController _pageController; - final _keyModifiers = KeyModifiers(); - int _currentPage = 0; - String? _lastProjectId; +/// Tappable project name in AppBar that opens a dropdown to switch projects. +class _ProjectSwitcher extends StatelessWidget { + final List projects; + final String? selectedProjectId; + final ValueChanged onSelect; - @override - void initState() { - super.initState(); - _pageController = PageController(); - } + const _ProjectSwitcher({ + required this.projects, + required this.selectedProjectId, + required this.onSelect, + }); @override - void dispose() { - _pageController.dispose(); - _keyModifiers.dispose(); - super.dispose(); - } + Widget build(BuildContext context) { + final selected = projects + .where((p) => p.id == selectedProjectId) + .firstOrNull ?? + projects.firstOrNull; + final name = selected?.name ?? 'No Project'; - void _syncState(String projectId, List terminalIds) { - if (projectId != _lastProjectId) { - _lastProjectId = projectId; - _currentPage = 0; - if (_pageController.hasClients) { - _pageController.jumpToPage(0); - } - } - if (_currentPage >= terminalIds.length && terminalIds.isNotEmpty) { - _currentPage = terminalIds.length - 1; + if (projects.length <= 1) { + return Text(name); } + + return GestureDetector( + onTap: () => _showProjectMenu(context), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + name, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + const Icon(Icons.arrow_drop_down, size: 20), + ], + ), + ); } - @override - Widget build(BuildContext context) { - final workspace = context.watch(); - final connection = context.watch(); - final project = workspace.selectedProject; - final connId = connection.connId; + void _showProjectMenu(BuildContext context) { + final RenderBox button = context.findRenderObject() as RenderBox; + final overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + final position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(Offset(0, button.size.height), ancestor: overlay), + button.localToGlobal(button.size.bottomRight(Offset.zero), + ancestor: overlay), + ), + Offset.zero & overlay.size, + ); - if (connId == null || project == null) { - return Scaffold( - backgroundColor: OkenaColors.background, - body: Center( - child: Text( - 'No project selected', - style: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), + showMenu( + context: context, + position: position, + items: projects.map((p) { + return PopupMenuItem( + value: p.id, + child: Row( + children: [ + Icon( + Icons.folder, + size: 18, + color: p.id == selectedProjectId + ? Theme.of(context).colorScheme.primary + : null, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + p.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: p.id == selectedProjectId + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + if (p.gitBranch != null) ...[ + const SizedBox(width: 8), + Icon(Icons.commit, size: 14, color: Colors.grey[500]), + const SizedBox(width: 2), + Text( + p.gitBranch!, + style: TextStyle(fontSize: 11, color: Colors.grey[500]), + ), + ], + ], ), - ), - ); - } + ); + }).toList(), + ).then((value) { + if (value != null) { + onSelect(value); + } + }); + } +} - final terminalIds = project.terminalIds; - _syncState(project.id, terminalIds); +/// Horizontal tab bar showing terminals in the current project. +class _TerminalTabBar extends StatelessWidget { + final List terminalIds; + final Map terminalNames; + final String selectedTerminalId; + final String projectId; + final String connId; + final ValueChanged onSelect; - final safeCurrentPage = terminalIds.isNotEmpty - ? _currentPage.clamp(0, terminalIds.length - 1) - : 0; - final currentTerminalId = terminalIds.isNotEmpty - ? terminalIds[safeCurrentPage] - : null; + const _TerminalTabBar({ + required this.terminalIds, + required this.terminalNames, + required this.selectedTerminalId, + required this.projectId, + required this.connId, + required this.onSelect, + }); - return Scaffold( - backgroundColor: OkenaColors.background, - body: SafeArea( - bottom: false, + @override + Widget build(BuildContext context) { + return Container( + height: 36, + color: const Color(0xFF252526), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: terminalIds.length, + padding: const EdgeInsets.symmetric(horizontal: 4), + itemBuilder: (context, index) { + final tid = terminalIds[index]; + final isSelected = tid == selectedTerminalId; + final name = terminalNames[tid] ?? 'Terminal ${index + 1}'; + + return GestureDetector( + onTap: () => onSelect(tid), + onLongPress: () => _showTabMenu(context, tid, name), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF3C3C3C) + : Colors.transparent, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Text( + name, + style: TextStyle( + color: isSelected ? Colors.white : Colors.white54, + fontSize: 12, + fontFamily: 'JetBrainsMono', + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ); + }, + ), + ); + } + + void _showTabMenu(BuildContext context, String terminalId, String name) { + showModalBottomSheet( + context: context, + builder: (ctx) => SafeArea( child: Column( + mainAxisSize: MainAxisSize.min, children: [ - _Header( - projectName: project.name, - folderColor: project.folderColor, - terminalCount: terminalIds.length, - currentPage: safeCurrentPage, - onCreateTerminal: () { - HapticFeedback.mediumImpact(); - state_ffi.createTerminal(connId: connId, projectId: project.id); - }, - onCloseTerminal: currentTerminalId != null - ? () { - HapticFeedback.mediumImpact(); - state_ffi.closeTerminal( - connId: connId, - projectId: project.id, - terminalId: currentTerminalId, - ); - } - : null, + Padding( + padding: const EdgeInsets.all(16), + child: Text(name, + style: Theme.of(context).textTheme.titleMedium), ), - Expanded( - child: terminalIds.isEmpty - ? _buildEmptyState(connId, project.id) - : PageView.builder( - controller: _pageController, - itemCount: terminalIds.length, - onPageChanged: (i) => setState(() => _currentPage = i), - itemBuilder: (context, index) => TerminalView( - connId: connId, - terminalId: terminalIds[index], - modifiers: _keyModifiers, - ), - ), + ListTile( + leading: const Icon(Icons.edit), + title: const Text('Rename'), + onTap: () { + Navigator.of(ctx).pop(); + _showRenameDialog(context, terminalId, name); + }, ), - KeyToolbar( - connId: connId, - terminalId: currentTerminalId, - modifiers: _keyModifiers, + ListTile( + leading: const Icon(Icons.close, color: Colors.redAccent), + title: const Text('Close', + style: TextStyle(color: Colors.redAccent)), + onTap: () { + Navigator.of(ctx).pop(); + state_ffi.closeTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + ); + }, ), ], ), @@ -132,35 +360,43 @@ class _WorkspaceScreenState extends State { ); } - Widget _buildEmptyState(String connId, String projectId) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.terminal_rounded, color: OkenaColors.textTertiary.withOpacity(0.3), size: 48), - const SizedBox(height: 12), - Text( - 'No terminals', - style: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), + void _showRenameDialog( + BuildContext context, String terminalId, String currentName) { + final controller = TextEditingController(text: currentName); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Rename Terminal'), + content: TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration(labelText: 'Name'), + onSubmitted: (_) { + Navigator.of(ctx).pop(); + state_ffi.renameTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + name: controller.text, + ); + }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), ), - const SizedBox(height: 16), - GestureDetector( - onTap: () { - HapticFeedback.mediumImpact(); - state_ffi.createTerminal(connId: connId, projectId: projectId); + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + state_ffi.renameTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + name: controller.text, + ); }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: OkenaColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: OkenaColors.accent.withOpacity(0.3), width: 0.5), - ), - child: Text( - 'New Terminal', - style: OkenaTypography.callout.copyWith(color: OkenaColors.accent), - ), - ), + child: const Text('Rename'), ), ], ), @@ -168,174 +404,526 @@ class _WorkspaceScreenState extends State { } } -// ── Header with frosted glass ────────────────────────────────────────── - -Color _folderColorToColor(String colorName) { - return switch (colorName) { - 'red' => const Color(0xFFEF4444), - 'orange' => const Color(0xFFF97316), - 'yellow' => const Color(0xFFEAB308), - 'lime' => const Color(0xFF84CC16), - 'green' => const Color(0xFF22C55E), - 'teal' => const Color(0xFF14B8A6), - 'cyan' => const Color(0xFF06B6D4), - 'blue' => const Color(0xFF3B82F6), - 'indigo' => const Color(0xFF6366F1), - 'purple' => const Color(0xFFA855F7), - 'pink' => const Color(0xFFEC4899), - _ => OkenaColors.textTertiary, - }; +/// Small colored dot indicating connection quality. +class _ConnectionDot extends StatelessWidget { + final double secondsSinceActivity; + + const _ConnectionDot({required this.secondsSinceActivity}); + + @override + Widget build(BuildContext context) { + final Color color; + if (secondsSinceActivity < 3) { + color = Colors.green; + } else if (secondsSinceActivity < 10) { + color = Colors.orange; + } else { + color = Colors.red; + } + + return Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ); + } } -class _Header extends StatelessWidget { - final String projectName; - final String folderColor; - final int terminalCount; - final int currentPage; - final VoidCallback onCreateTerminal; - final VoidCallback? onCloseTerminal; - - const _Header({ - required this.projectName, - required this.folderColor, - required this.terminalCount, - required this.currentPage, - required this.onCreateTerminal, - this.onCloseTerminal, +/// Git status button in the app bar. +class _GitButton extends StatelessWidget { + final String connId; + final state_ffi.ProjectInfo project; + + const _GitButton({required this.connId, required this.project}); + + @override + Widget build(BuildContext context) { + final hasChanges = project.gitLinesAdded > 0 || project.gitLinesRemoved > 0; + + return IconButton( + icon: Badge( + isLabelVisible: hasChanges, + smallSize: 8, + child: const Icon(Icons.commit, size: 20), + ), + tooltip: project.gitBranch ?? 'Git', + onPressed: () => _showGitSheet(context), + ); + } + + void _showGitSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (ctx) => DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 0.9, + expand: false, + builder: (ctx, scrollController) => _GitSheet( + connId: connId, + project: project, + scrollController: scrollController, + ), + ), + ); + } +} + +class _GitSheet extends StatefulWidget { + final String connId; + final state_ffi.ProjectInfo project; + final ScrollController scrollController; + + const _GitSheet({ + required this.connId, + required this.project, + required this.scrollController, }); + @override + State<_GitSheet> createState() => _GitSheetState(); +} + +class _GitSheetState extends State<_GitSheet> { + String? _diffSummary; + String? _branches; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + try { + final results = await Future.wait([ + state_ffi.gitDiffSummary( + connId: widget.connId, projectId: widget.project.id), + state_ffi.gitBranches( + connId: widget.connId, projectId: widget.project.id), + ]); + if (mounted) { + setState(() { + _diffSummary = results[0]; + _branches = results[1]; + _loading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + @override Widget build(BuildContext context) { - final color = _folderColorToColor(folderColor); - - return ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), - child: Container( - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: const BoxDecoration( - color: OkenaColors.glassBg, - border: Border( - bottom: BorderSide(color: OkenaColors.glassStroke, width: 0.5), - ), + return Column( + children: [ + // Handle bar + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 32, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[600], + borderRadius: BorderRadius.circular(2), ), + ), + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ - // Project avatar - Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: color.withOpacity(0.15), - borderRadius: BorderRadius.circular(7), - ), - alignment: Alignment.center, - child: Text( - projectName.isNotEmpty ? projectName[0].toUpperCase() : '?', - style: TextStyle( - color: color, - fontSize: 13, - fontWeight: FontWeight.w700, - ), - ), - ), - const SizedBox(width: 10), - // Project name + chevron (tappable → opens project sheet) + const Icon(Icons.commit, size: 20), + const SizedBox(width: 8), Expanded( - child: GestureDetector( - onTap: () { - HapticFeedback.selectionClick(); - ProjectSheet.show(context); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - projectName, - style: OkenaTypography.callout.copyWith( - color: OkenaColors.textPrimary, - fontWeight: FontWeight.w600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.project.gitBranch ?? 'Unknown branch', + style: Theme.of(context).textTheme.titleMedium, + ), + Row( + children: [ + if (widget.project.gitLinesAdded > 0) + Text( + '+${widget.project.gitLinesAdded}', + style: TextStyle( + fontSize: 12, color: Colors.green[400]), ), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 4), - const Icon( - CupertinoIcons.chevron_down, - color: OkenaColors.textTertiary, - size: 12, - ), - ], - ), + if (widget.project.gitLinesAdded > 0 && + widget.project.gitLinesRemoved > 0) + const SizedBox(width: 8), + if (widget.project.gitLinesRemoved > 0) + Text( + '-${widget.project.gitLinesRemoved}', + style: TextStyle( + fontSize: 12, color: Colors.red[400]), + ), + ], + ), + ], ), ), - // Page indicator dots (only when >1 terminal) - if (terminalCount > 1) ...[ - const SizedBox(width: 8), - Row( - mainAxisSize: MainAxisSize.min, - children: List.generate(terminalCount, (i) { - final isActive = i == currentPage; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.5), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: isActive ? 8 : 6, - height: isActive ? 8 : 6, - decoration: BoxDecoration( - color: isActive ? OkenaColors.accent : OkenaColors.textTertiary, - shape: BoxShape.circle, - ), - ), - ); - }), + ], + ), + ), + const Divider(), + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : ListView( + controller: widget.scrollController, + padding: const EdgeInsets.all(16), + children: [ + if (_diffSummary != null) ...[ + Text('Changes', + style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 8), + _DiffSummaryView(json: _diffSummary!), + const SizedBox(height: 16), + ], + if (_branches != null) ...[ + Text('Branches', + style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 8), + _BranchesView(json: _branches!), + ], + ], ), - ], - const SizedBox(width: 8), - // Close terminal button - if (onCloseTerminal != null) - GestureDetector( - onTap: onCloseTerminal, - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: OkenaColors.surfaceElevated, - borderRadius: BorderRadius.circular(7), + ), + ], + ); + } +} + +class _DiffSummaryView extends StatelessWidget { + final String json; + + const _DiffSummaryView({required this.json}); + + @override + Widget build(BuildContext context) { + try { + final data = jsonDecode(json); + if (data is Map && data.containsKey('files')) { + final files = data['files'] as List? ?? []; + if (files.isEmpty) { + return Text('No changes', + style: TextStyle(color: Colors.grey[500])); + } + return Column( + children: files.map((f) { + final file = f as Map; + final path = file['path'] as String? ?? ''; + final added = file['added'] as int? ?? 0; + final removed = file['removed'] as int? ?? 0; + final status = file['status'] as String? ?? 'modified'; + + IconData icon; + Color iconColor; + switch (status) { + case 'added': + icon = Icons.add_circle_outline; + iconColor = Colors.green; + break; + case 'deleted': + icon = Icons.remove_circle_outline; + iconColor = Colors.red; + break; + default: + icon = Icons.edit; + iconColor = Colors.orange; + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon(icon, size: 16, color: iconColor), + const SizedBox(width: 8), + Expanded( + child: Text( + path, + style: const TextStyle( + fontSize: 12, fontFamily: 'JetBrainsMono'), + overflow: TextOverflow.ellipsis, ), - alignment: Alignment.center, - child: const Icon( - Icons.close_rounded, - color: OkenaColors.textTertiary, - size: 14, + ), + if (added > 0) + Text('+$added', + style: TextStyle( + fontSize: 11, color: Colors.green[400])), + if (added > 0 && removed > 0) const SizedBox(width: 4), + if (removed > 0) + Text('-$removed', + style: + TextStyle(fontSize: 11, color: Colors.red[400])), + ], + ), + ); + }).toList(), + ); + } + // Fallback: show as plain text + return Text(json, + style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); + } catch (_) { + return Text(json, + style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); + } + } +} + +class _BranchesView extends StatelessWidget { + final String json; + + const _BranchesView({required this.json}); + + @override + Widget build(BuildContext context) { + try { + final data = jsonDecode(json); + if (data is Map && data.containsKey('branches')) { + final branches = data['branches'] as List? ?? []; + return Column( + children: branches.map((b) { + final branch = b as Map; + final name = branch['name'] as String? ?? ''; + final isCurrent = branch['current'] as bool? ?? false; + final isRemote = branch['remote'] as bool? ?? false; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon( + isCurrent ? Icons.check_circle : Icons.circle_outlined, + size: 16, + color: isCurrent ? Colors.green : Colors.grey[600], + ), + const SizedBox(width: 8), + if (isRemote) + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon(Icons.cloud, + size: 12, color: Colors.grey[500]), + ), + Expanded( + child: Text( + name, + style: TextStyle( + fontSize: 12, + fontFamily: 'JetBrainsMono', + fontWeight: + isCurrent ? FontWeight.bold : FontWeight.normal, + ), + overflow: TextOverflow.ellipsis, ), ), - ), - const SizedBox(width: 4), - // Create terminal button - GestureDetector( - onTap: onCreateTerminal, - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: OkenaColors.surfaceElevated, - borderRadius: BorderRadius.circular(7), + ], + ), + ); + }).toList(), + ); + } + return Text(json, + style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); + } catch (_) { + return Text(json, + style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); + } + } +} + +/// Services button in the app bar. +class _ServicesButton extends StatelessWidget { + final String connId; + final state_ffi.ProjectInfo project; + + const _ServicesButton({required this.connId, required this.project}); + + @override + Widget build(BuildContext context) { + final running = + project.services.where((s) => s.status == 'running').length; + final total = project.services.length; + + return IconButton( + icon: Badge( + isLabelVisible: running > 0, + label: Text('$running', + style: const TextStyle(fontSize: 9)), + child: const Icon(Icons.dns, size: 20), + ), + tooltip: '$running/$total services running', + onPressed: () => _showServicesSheet(context), + ); + } + + void _showServicesSheet(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 32, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[600], + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + const Icon(Icons.dns, size: 20), + const SizedBox(width: 8), + Text('Services', + style: Theme.of(context).textTheme.titleMedium), + const Spacer(), + TextButton( + onPressed: () { + state_ffi.startAllServices( + connId: connId, + projectId: project.id, + ); + }, + child: const Text('Start All'), ), - alignment: Alignment.center, - child: const Icon( - Icons.add_rounded, - color: OkenaColors.textSecondary, - size: 16, + TextButton( + onPressed: () { + state_ffi.stopAllServices( + connId: connId, + projectId: project.id, + ); + }, + child: const Text('Stop All', + style: TextStyle(color: Colors.redAccent)), ), - ), + ], ), - ], - ), + ), + const Divider(), + ...project.services.map((s) => _ServiceTile( + service: s, + connId: connId, + projectId: project.id, + )), + const SizedBox(height: 16), + ], ), ), ); } } + +class _ServiceTile extends StatelessWidget { + final state_ffi.ServiceInfo service; + final String connId; + final String projectId; + + const _ServiceTile({ + required this.service, + required this.connId, + required this.projectId, + }); + + Color _statusColor() { + switch (service.status) { + case 'running': + return Colors.green; + case 'stopped': + return Colors.grey; + case 'crashed': + return Colors.red; + case 'starting': + case 'restarting': + return Colors.orange; + default: + return Colors.grey; + } + } + + @override + Widget build(BuildContext context) { + final color = _statusColor(); + return ListTile( + leading: Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + title: Row( + children: [ + Expanded(child: Text(service.name)), + Text( + service.status, + style: TextStyle(fontSize: 12, color: color), + ), + ], + ), + subtitle: service.ports.isNotEmpty + ? Text( + service.ports.map((p) => ':$p').join(', '), + style: TextStyle(fontSize: 11, color: Colors.grey[500]), + ) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (service.status == 'running') ...[ + IconButton( + icon: const Icon(Icons.restart_alt, size: 20), + tooltip: 'Restart', + onPressed: () { + state_ffi.restartService( + connId: connId, + projectId: projectId, + serviceName: service.name, + ); + }, + ), + IconButton( + icon: Icon(Icons.stop, size: 20, color: Colors.red[300]), + tooltip: 'Stop', + onPressed: () { + state_ffi.stopService( + connId: connId, + projectId: projectId, + serviceName: service.name, + ); + }, + ), + ] else ...[ + IconButton( + icon: Icon(Icons.play_arrow, size: 20, color: Colors.green[300]), + tooltip: 'Start', + onPressed: () { + state_ffi.startService( + connId: connId, + projectId: projectId, + serviceName: service.name, + ); + }, + ), + ], + ], + ), + ); + } +} diff --git a/mobile/lib/src/widgets/project_drawer.dart b/mobile/lib/src/widgets/project_drawer.dart index 4ac218ba..6f6deb49 100644 --- a/mobile/lib/src/widgets/project_drawer.dart +++ b/mobile/lib/src/widgets/project_drawer.dart @@ -1,162 +1,58 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../providers/connection_provider.dart'; import '../providers/workspace_provider.dart'; import '../rust/api/state.dart' as state_ffi; -import '../theme/app_theme.dart'; import 'status_indicator.dart'; -Color _folderColorToColor(String colorName) { - return switch (colorName) { - 'red' => const Color(0xFFEF4444), - 'orange' => const Color(0xFFF97316), - 'yellow' => const Color(0xFFEAB308), - 'lime' => const Color(0xFF84CC16), - 'green' => const Color(0xFF22C55E), - 'teal' => const Color(0xFF14B8A6), - 'cyan' => const Color(0xFF06B6D4), - 'blue' => const Color(0xFF3B82F6), - 'indigo' => const Color(0xFF6366F1), - 'purple' => const Color(0xFFA855F7), - 'pink' => const Color(0xFFEC4899), - _ => OkenaColors.textTertiary, - }; -} - -/// Bottom sheet for project selection (replaces old Drawer). -class ProjectSheet extends StatelessWidget { - const ProjectSheet({super.key}); - - static void show(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) => DraggableScrollableSheet( - initialChildSize: 0.6, - minChildSize: 0.3, - maxChildSize: 0.9, - snap: true, - snapSizes: const [0.3, 0.6, 0.9], - builder: (context, scrollController) => _SheetContent( - scrollController: scrollController, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return const SizedBox.shrink(); - } -} - -class _SheetContent extends StatelessWidget { - final ScrollController scrollController; - - const _SheetContent({required this.scrollController}); +class ProjectDrawer extends StatelessWidget { + const ProjectDrawer({super.key}); @override Widget build(BuildContext context) { final workspace = context.watch(); final connection = context.watch(); - return Container( - decoration: const BoxDecoration( - color: OkenaColors.surface, - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), + return Drawer( child: Column( children: [ - // Drag handle - Center( - child: Container( - width: 36, - height: 4, - margin: const EdgeInsets.only(top: 10, bottom: 16), - decoration: BoxDecoration( - color: OkenaColors.textTertiary.withOpacity(0.4), - borderRadius: BorderRadius.circular(2), - ), + DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), - ), - // Header - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Projects', style: OkenaTypography.title), - if (connection.activeServer != null) ...[ - const SizedBox(height: 3), - Text( - connection.activeServer!.displayName, - style: OkenaTypography.caption2.copyWith( - color: OkenaColors.textTertiary, - ), - ), - ], - ], - ), + Text( + 'Okena', + style: Theme.of(context).textTheme.headlineSmall, ), + const SizedBox(height: 4), + if (connection.activeServer != null) + Text( + connection.activeServer!.displayName, + style: Theme.of(context).textTheme.bodySmall, + ), + const Spacer(), StatusIndicator(status: connection.status), ], ), ), - // Divider - Container( - height: 0.5, - color: OkenaColors.border, - ), - // Project list Expanded( child: _ProjectList( workspace: workspace, - scrollController: scrollController, + connection: connection, ), ), - // Disconnect footer - Container( - decoration: const BoxDecoration( - border: Border( - top: BorderSide(color: OkenaColors.border, width: 0.5), - ), - ), - child: SafeArea( - top: false, - child: InkWell( - onTap: () { - HapticFeedback.mediumImpact(); - Navigator.of(context).pop(); - connection.disconnect(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), - child: Row( - children: [ - Icon( - Icons.link_off_rounded, - color: OkenaColors.error.withOpacity(0.7), - size: 16, - ), - const SizedBox(width: 10), - Text( - 'Disconnect', - style: OkenaTypography.callout.copyWith( - color: OkenaColors.error.withOpacity(0.8), - ), - ), - ], - ), - ), - ), - ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.link_off), + title: const Text('Disconnect'), + onTap: () { + Navigator.of(context).pop(); + connection.disconnect(); + }, ), ], ), @@ -164,228 +60,601 @@ class _SheetContent extends StatelessWidget { } } -// ── Project list with folder grouping ────────────────────────────── - class _ProjectList extends StatelessWidget { final WorkspaceProvider workspace; - final ScrollController scrollController; + final ConnectionProvider connection; - const _ProjectList({ - required this.workspace, - required this.scrollController, - }); + const _ProjectList({required this.workspace, required this.connection}); @override Widget build(BuildContext context) { - final projectMap = {for (final p in workspace.projects) p.id: p}; - final folderMap = {for (final f in workspace.folders) f.id: f}; + final folders = workspace.folders; final projectOrder = workspace.projectOrder; + final projects = workspace.projects; - // If no project_order, fall back to flat list - if (projectOrder.isEmpty) { - return ListView.builder( - controller: scrollController, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - itemCount: workspace.projects.length, - itemBuilder: (context, index) { - final project = workspace.projects[index]; - return _buildProjectTile(context, project); - }, - ); - } + // Build ordered list: folders and standalone projects + final List items = []; - // Build ordered items: folders expand to header + children, standalone projects inline - final widgets = []; - for (final id in projectOrder) { - final folder = folderMap[id]; - if (folder != null) { - // Folder header - widgets.add(_FolderHeader( - name: folder.name, - folderColor: _folderColorToColor(folder.folderColor), - )); - // Folder's projects - for (final pid in folder.projectIds) { - final project = projectMap[pid]; + if (projectOrder.isNotEmpty || folders.isNotEmpty) { + // Use project_order to display in correct order + final folderMap = {for (final f in folders) f.id: f}; + final projectMap = {for (final p in projects) p.id: p}; + final displayedProjectIds = {}; + + for (final entryId in projectOrder) { + final folder = folderMap[entryId]; + if (folder != null) { + items.add(_FolderTile( + folder: folder, + projects: folder.projectIds + .map((pid) => projectMap[pid]) + .whereType() + .toList(), + workspace: workspace, + connection: connection, + )); + displayedProjectIds.addAll(folder.projectIds); + } else { + final project = projectMap[entryId]; if (project != null) { - widgets.add(Padding( - padding: const EdgeInsets.only(left: 16), - child: _buildProjectTile(context, project), + items.add(_ProjectTile( + project: project, + workspace: workspace, + connection: connection, )); + displayedProjectIds.add(entryId); } } - } else { - final project = projectMap[id]; - if (project != null) { - widgets.add(_buildProjectTile(context, project)); + } + + // Add any projects not in the order + for (final p in projects) { + if (!displayedProjectIds.contains(p.id)) { + items.add(_ProjectTile( + project: p, + workspace: workspace, + connection: connection, + )); } } + } else { + // No ordering info — just list projects + for (final p in projects) { + items.add(_ProjectTile( + project: p, + workspace: workspace, + connection: connection, + )); + } } - return ListView( - controller: scrollController, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - children: widgets, + return ListView(children: items); + } +} + +Color _folderColorToColor(String colorName) { + switch (colorName) { + case 'red': + return Colors.red; + case 'orange': + return Colors.orange; + case 'yellow': + return Colors.yellow; + case 'lime': + return Colors.lime; + case 'green': + return Colors.green; + case 'teal': + return Colors.teal; + case 'cyan': + return Colors.cyan; + case 'blue': + return Colors.blue; + case 'purple': + return Colors.purple; + case 'pink': + return Colors.pink; + default: + return Colors.grey; + } +} + +class _FolderTile extends StatelessWidget { + final state_ffi.FolderInfo folder; + final List projects; + final WorkspaceProvider workspace; + final ConnectionProvider connection; + + const _FolderTile({ + required this.folder, + required this.projects, + required this.workspace, + required this.connection, + }); + + @override + Widget build(BuildContext context) { + final color = _folderColorToColor(folder.folderColor); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 4), + child: Row( + children: [ + Icon(Icons.folder, size: 18, color: color), + const SizedBox(width: 8), + Expanded( + child: Text( + folder.name, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color, + letterSpacing: 0.5, + ), + ), + ), + ], + ), + ), + ...projects.map((p) => _ProjectTile( + project: p, + workspace: workspace, + connection: connection, + indent: true, + )), + ], ); } +} + +class _ProjectTile extends StatelessWidget { + final state_ffi.ProjectInfo project; + final WorkspaceProvider workspace; + final ConnectionProvider connection; + final bool indent; - Widget _buildProjectTile(BuildContext context, state_ffi.ProjectInfo project) { + const _ProjectTile({ + required this.project, + required this.workspace, + required this.connection, + this.indent = false, + }); + + @override + Widget build(BuildContext context) { final isSelected = project.id == workspace.selectedProjectId; - return _ProjectTile( - name: project.name, - path: project.path, - isSelected: isSelected, - folderColor: _folderColorToColor(project.folderColor), - onTap: () { - HapticFeedback.selectionClick(); - workspace.selectProject(project.id); - Navigator.of(context).pop(); - }, + final folderColor = _folderColorToColor(project.folderColor); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + contentPadding: EdgeInsets.only(left: indent ? 32 : 16, right: 16), + leading: Icon( + Icons.folder, + color: isSelected + ? Theme.of(context).colorScheme.primary + : folderColor, + ), + title: Text(project.name), + subtitle: _buildSubtitle(context), + selected: isSelected, + onTap: () { + workspace.selectProject(project.id); + }, + ), + if (isSelected) ...[ + // Git status + if (project.gitBranch != null) + _GitStatusRow(project: project), + // Services + if (project.services.isNotEmpty) + ...project.services.map((s) => _ServiceRow( + service: s, + project: project, + connection: connection, + )), + // Terminals + ...project.terminalIds.asMap().entries.map((entry) { + final idx = entry.key; + final tid = entry.value; + final isTerminalSelected = tid == workspace.selectedTerminalId; + final name = + project.terminalNames[tid] ?? 'Terminal ${idx + 1}'; + return ListTile( + contentPadding: + const EdgeInsets.only(left: 56, right: 8), + leading: Icon( + Icons.terminal, + size: 20, + color: isTerminalSelected + ? Theme.of(context).colorScheme.primary + : null, + ), + title: Text( + name, + style: TextStyle( + fontSize: 14, + color: isTerminalSelected + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + selected: isTerminalSelected, + dense: true, + trailing: _TerminalActions( + connId: connection.connId!, + projectId: project.id, + terminalId: tid, + name: name, + ), + onTap: () { + workspace.selectTerminal(tid); + Navigator.of(context).pop(); + }, + onLongPress: () { + _showTerminalMenu( + context, + connId: connection.connId!, + projectId: project.id, + terminalId: tid, + name: name, + ); + }, + ); + }), + if (connection.connId != null) + ListTile( + contentPadding: + const EdgeInsets.only(left: 56, right: 16), + leading: Icon( + Icons.add, + size: 20, + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + title: Text( + 'New Terminal', + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + dense: true, + onTap: () { + state_ffi.createTerminal( + connId: connection.connId!, + projectId: project.id, + ); + Navigator.of(context).pop(); + }, + ), + ], + ], ); } -} -// ── Folder header ────────────────────────────────────────────────── + Widget? _buildSubtitle(BuildContext context) { + final parts = []; + if (project.gitBranch != null) { + parts.add(Icon(Icons.commit, size: 12, color: Colors.grey[500])); + parts.add(const SizedBox(width: 2)); + parts.add(Text( + project.gitBranch!, + style: TextStyle(fontSize: 11, color: Colors.grey[500]), + )); + } + final runningServices = + project.services.where((s) => s.status == 'running').length; + if (runningServices > 0) { + if (parts.isNotEmpty) parts.add(const SizedBox(width: 8)); + parts.add(Icon(Icons.dns, size: 12, color: Colors.green[400])); + parts.add(const SizedBox(width: 2)); + parts.add(Text( + '$runningServices', + style: TextStyle(fontSize: 11, color: Colors.green[400]), + )); + } + if (parts.isEmpty) return null; + return Row(children: parts); + } -class _FolderHeader extends StatelessWidget { + void _showTerminalMenu( + BuildContext context, { + required String connId, + required String projectId, + required String terminalId, + required String name, + }) { + showModalBottomSheet( + context: context, + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.edit), + title: const Text('Rename'), + onTap: () { + Navigator.of(ctx).pop(); + _showRenameDialog(context, + connId: connId, + projectId: projectId, + terminalId: terminalId, + currentName: name); + }, + ), + ListTile( + leading: const Icon(Icons.close, color: Colors.redAccent), + title: + const Text('Close', style: TextStyle(color: Colors.redAccent)), + onTap: () { + Navigator.of(ctx).pop(); + Navigator.of(context).pop(); + state_ffi.closeTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + ); + }, + ), + ], + ), + ), + ); + } + + void _showRenameDialog( + BuildContext context, { + required String connId, + required String projectId, + required String terminalId, + required String currentName, + }) { + final controller = TextEditingController(text: currentName); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Rename Terminal'), + content: TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration(labelText: 'Name'), + onSubmitted: (_) { + Navigator.of(ctx).pop(); + state_ffi.renameTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + name: controller.text, + ); + }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + state_ffi.renameTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + name: controller.text, + ); + }, + child: const Text('Rename'), + ), + ], + ), + ); + } +} + +class _TerminalActions extends StatelessWidget { + final String connId; + final String projectId; + final String terminalId; final String name; - final Color folderColor; - const _FolderHeader({ + const _TerminalActions({ + required this.connId, + required this.projectId, + required this.terminalId, required this.name, - required this.folderColor, }); + @override + Widget build(BuildContext context) { + return SizedBox( + width: 32, + height: 32, + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 16, + icon: const Icon(Icons.close, size: 16), + onPressed: () { + Navigator.of(context).pop(); + state_ffi.closeTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + ); + }, + ), + ); + } +} + +class _GitStatusRow extends StatelessWidget { + final state_ffi.ProjectInfo project; + + const _GitStatusRow({required this.project}); + @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.fromLTRB(12, 14, 12, 6), + padding: const EdgeInsets.only(left: 56, right: 16, bottom: 4), child: Row( children: [ - Container( - width: 7, - height: 7, - decoration: BoxDecoration( - color: folderColor == OkenaColors.textTertiary - ? OkenaColors.textTertiary - : folderColor.withOpacity(0.8), - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 8), + Icon(Icons.commit, size: 16, color: Colors.grey[400]), + const SizedBox(width: 6), Text( - name.toUpperCase(), - style: TextStyle( - color: folderColor == OkenaColors.textTertiary - ? OkenaColors.textTertiary - : folderColor.withOpacity(0.7), - fontSize: 10, - fontWeight: FontWeight.w700, - letterSpacing: 1.0, - ), + project.gitBranch ?? '', + style: TextStyle(fontSize: 12, color: Colors.grey[400]), ), + const Spacer(), + if (project.gitLinesAdded > 0) ...[ + Text( + '+${project.gitLinesAdded}', + style: TextStyle(fontSize: 11, color: Colors.green[400]), + ), + const SizedBox(width: 4), + ], + if (project.gitLinesRemoved > 0) + Text( + '-${project.gitLinesRemoved}', + style: TextStyle(fontSize: 11, color: Colors.red[400]), + ), ], ), ); } } -// ── Project tile ────────────────────────────────────────────────────── - -class _ProjectTile extends StatelessWidget { - final String name; - final String path; - final bool isSelected; - final Color folderColor; - final VoidCallback onTap; +class _ServiceRow extends StatelessWidget { + final state_ffi.ServiceInfo service; + final state_ffi.ProjectInfo project; + final ConnectionProvider connection; - const _ProjectTile({ - required this.name, - required this.path, - required this.isSelected, - required this.folderColor, - required this.onTap, + const _ServiceRow({ + required this.service, + required this.project, + required this.connection, }); + Color _statusColor() { + switch (service.status) { + case 'running': + return Colors.green; + case 'stopped': + return Colors.grey; + case 'crashed': + return Colors.red; + case 'starting': + case 'restarting': + return Colors.orange; + default: + return Colors.grey; + } + } + + IconData _statusIcon() { + switch (service.status) { + case 'running': + return Icons.check_circle; + case 'stopped': + return Icons.stop_circle; + case 'crashed': + return Icons.error; + case 'starting': + case 'restarting': + return Icons.hourglass_top; + default: + return Icons.help; + } + } + @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 1), - child: Material( - color: isSelected ? OkenaColors.surfaceOverlay : Colors.transparent, - borderRadius: BorderRadius.circular(10), - child: InkWell( - borderRadius: BorderRadius.circular(10), - splashColor: OkenaColors.accent.withOpacity(0.08), - highlightColor: OkenaColors.accent.withOpacity(0.04), - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: isSelected - ? BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border( - left: BorderSide( - color: OkenaColors.accent, - width: 3, - ), - ), - ) - : null, - child: Row( - children: [ - // Letter avatar - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: (isSelected ? OkenaColors.accent : folderColor) - .withOpacity(0.15), - borderRadius: BorderRadius.circular(6), - ), - alignment: Alignment.center, - child: Text( - name.isNotEmpty ? name[0].toUpperCase() : '?', - style: TextStyle( - color: isSelected ? OkenaColors.accent : folderColor, - fontSize: 11, - fontWeight: FontWeight.w700, - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: OkenaTypography.callout.copyWith( - color: isSelected - ? OkenaColors.textPrimary - : OkenaColors.textSecondary, - fontWeight: - isSelected ? FontWeight.w600 : FontWeight.w400, - ), - ), - const SizedBox(height: 1), - Text( - path, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: OkenaTypography.caption2.copyWith( - fontFamily: 'JetBrainsMono', - fontSize: 10, - ), - ), - ], - ), - ), - ], + final connId = connection.connId; + final color = _statusColor(); + + return ListTile( + contentPadding: const EdgeInsets.only(left: 56, right: 8), + leading: Icon(_statusIcon(), size: 18, color: color), + title: Row( + children: [ + Expanded( + child: Text( + service.name, + style: const TextStyle(fontSize: 13), ), ), - ), + if (service.ports.isNotEmpty) + Text( + service.ports.map((p) => ':$p').join(' '), + style: TextStyle(fontSize: 11, color: Colors.grey[500]), + ), + ], + ), + dense: true, + trailing: _ServiceActionButton( + service: service, + connId: connId, + projectId: project.id, ), ); } } + +class _ServiceActionButton extends StatelessWidget { + final state_ffi.ServiceInfo service; + final String? connId; + final String projectId; + + const _ServiceActionButton({ + required this.service, + required this.connId, + required this.projectId, + }); + + @override + Widget build(BuildContext context) { + if (connId == null) return const SizedBox.shrink(); + + return PopupMenuButton( + padding: EdgeInsets.zero, + iconSize: 20, + itemBuilder: (ctx) => [ + if (service.status == 'stopped' || service.status == 'crashed') + const PopupMenuItem(value: 'start', child: Text('Start')), + if (service.status == 'running') + const PopupMenuItem(value: 'stop', child: Text('Stop')), + if (service.status == 'running') + const PopupMenuItem(value: 'restart', child: Text('Restart')), + ], + onSelected: (action) { + switch (action) { + case 'start': + state_ffi.startService( + connId: connId!, + projectId: projectId, + serviceName: service.name, + ); + break; + case 'stop': + state_ffi.stopService( + connId: connId!, + projectId: projectId, + serviceName: service.name, + ); + break; + case 'restart': + state_ffi.restartService( + connId: connId!, + projectId: projectId, + serviceName: service.name, + ); + break; + } + }, + ); + } +} From 40481b48b289c5b067f6b3dadc5ab4072c7868ed Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 19:26:55 +0200 Subject: [PATCH 18/37] feat(mobile): add terminal selection and scroll info APIs Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/rust/api/terminal.dart | 137 ++++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 9 deletions(-) diff --git a/mobile/lib/src/rust/api/terminal.dart b/mobile/lib/src/rust/api/terminal.dart index b818b99c..54fb41e2 100644 --- a/mobile/lib/src/rust/api/terminal.dart +++ b/mobile/lib/src/rust/api/terminal.dart @@ -6,7 +6,7 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt` /// Get the visible terminal cells for rendering. List getVisibleCells({ @@ -24,24 +24,88 @@ CursorState getCursor({required String connId, required String terminalId}) => terminalId: terminalId, ); -/// Scroll the terminal display by delta lines (positive = up into history, negative = down). -void scrollTerminal({ +/// Scroll the terminal display (positive = up, negative = down). +void scroll({ required String connId, required String terminalId, required int delta, -}) => RustLib.instance.api.crateApiTerminalScrollTerminal( +}) => RustLib.instance.api.crateApiTerminalScroll( connId: connId, terminalId: terminalId, delta: delta, ); -/// Get the current scroll display offset (0 = at bottom). -int getDisplayOffset({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalGetDisplayOffset( +/// Get scroll info: total lines, visible lines, display offset. +ScrollInfo getScrollInfo({ + required String connId, + required String terminalId, +}) => RustLib.instance.api.crateApiTerminalGetScrollInfo( + connId: connId, + terminalId: terminalId, +); + +/// Start a character-level selection at col/row. +void startSelection({ + required String connId, + required String terminalId, + required int col, + required int row, +}) => RustLib.instance.api.crateApiTerminalStartSelection( + connId: connId, + terminalId: terminalId, + col: col, + row: row, +); + +/// Start a word (semantic) selection at col/row. +void startWordSelection({ + required String connId, + required String terminalId, + required int col, + required int row, +}) => RustLib.instance.api.crateApiTerminalStartWordSelection( + connId: connId, + terminalId: terminalId, + col: col, + row: row, +); + +/// Extend the current selection to col/row. +void updateSelection({ + required String connId, + required String terminalId, + required int col, + required int row, +}) => RustLib.instance.api.crateApiTerminalUpdateSelection( + connId: connId, + terminalId: terminalId, + col: col, + row: row, +); + +/// Clear the current selection. +void clearSelection({required String connId, required String terminalId}) => + RustLib.instance.api.crateApiTerminalClearSelection( connId: connId, terminalId: terminalId, ); +/// Get the selected text, if any. +String? getSelectedText({required String connId, required String terminalId}) => + RustLib.instance.api.crateApiTerminalGetSelectedText( + connId: connId, + terminalId: terminalId, + ); + +/// Get selection bounds for rendering. +SelectionBounds? getSelectionBounds({ + required String connId, + required String terminalId, +}) => RustLib.instance.api.crateApiTerminalGetSelectionBounds( + connId: connId, + terminalId: terminalId, +); + /// Send text input to a terminal. Future sendText({ required String connId, @@ -53,8 +117,8 @@ Future sendText({ text: text, ); -/// Resize a terminal (local + send WS message to server). -Future resizeTerminal({ +/// Resize a terminal. +void resizeTerminal({ required String connId, required String terminalId, required int cols, @@ -147,3 +211,58 @@ class CursorState { shape == other.shape && visible == other.visible; } + +/// Scroll info for FFI transfer. +class ScrollInfo { + final int totalLines; + final int visibleLines; + final int displayOffset; + + const ScrollInfo({ + required this.totalLines, + required this.visibleLines, + required this.displayOffset, + }); + + @override + int get hashCode => + totalLines.hashCode ^ visibleLines.hashCode ^ displayOffset.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScrollInfo && + runtimeType == other.runtimeType && + totalLines == other.totalLines && + visibleLines == other.visibleLines && + displayOffset == other.displayOffset; +} + +/// Selection bounds for FFI transfer. +class SelectionBounds { + final int startCol; + final int startRow; + final int endCol; + final int endRow; + + const SelectionBounds({ + required this.startCol, + required this.startRow, + required this.endCol, + required this.endRow, + }); + + @override + int get hashCode => + startCol.hashCode ^ startRow.hashCode ^ endCol.hashCode ^ endRow.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SelectionBounds && + runtimeType == other.runtimeType && + startCol == other.startCol && + startRow == other.startRow && + endCol == other.endCol && + endRow == other.endRow; +} From ca211471942f98103ecf13d1c4bc0fec122817cd Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 19:27:00 +0200 Subject: [PATCH 19/37] feat(mobile): add layout management, project reorder, and git file contents FFI Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/rust/api/state.dart | 114 +++++++++++++++++++ mobile/native/src/api/state.rs | 170 +++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) diff --git a/mobile/lib/src/rust/api/state.dart b/mobile/lib/src/rust/api/state.dart index 6855f94f..2c55dec2 100644 --- a/mobile/lib/src/rust/api/state.dart +++ b/mobile/lib/src/rust/api/state.dart @@ -300,6 +300,120 @@ Future setFolderColor({ color: color, ); +/// Reorder a project within a folder. +Future reorderProjectInFolder({ + required String connId, + required String folderId, + required String projectId, + required BigInt newIndex, +}) => RustLib.instance.api.crateApiStateReorderProjectInFolder( + connId: connId, + folderId: folderId, + projectId: projectId, + newIndex: newIndex, +); + +/// Update split sizes for a split pane. +Future updateSplitSizes({ + required String connId, + required String projectId, + required Uint64List path, + required List sizes, +}) => RustLib.instance.api.crateApiStateUpdateSplitSizes( + connId: connId, + projectId: projectId, + path: path, + sizes: sizes, +); + +/// Add a new tab to a tab group. +Future addTab({ + required String connId, + required String projectId, + required Uint64List path, + required bool inGroup, +}) => RustLib.instance.api.crateApiStateAddTab( + connId: connId, + projectId: projectId, + path: path, + inGroup: inGroup, +); + +/// Set the active tab in a tab group. +Future setActiveTab({ + required String connId, + required String projectId, + required Uint64List path, + required BigInt index, +}) => RustLib.instance.api.crateApiStateSetActiveTab( + connId: connId, + projectId: projectId, + path: path, + index: index, +); + +/// Move a tab within a tab group. +Future moveTab({ + required String connId, + required String projectId, + required Uint64List path, + required BigInt fromIndex, + required BigInt toIndex, +}) => RustLib.instance.api.crateApiStateMoveTab( + connId: connId, + projectId: projectId, + path: path, + fromIndex: fromIndex, + toIndex: toIndex, +); + +/// Move a terminal into a tab group. +Future moveTerminalToTabGroup({ + required String connId, + required String projectId, + required String terminalId, + required Uint64List targetPath, + BigInt? position, + String? targetProjectId, +}) => RustLib.instance.api.crateApiStateMoveTerminalToTabGroup( + connId: connId, + projectId: projectId, + terminalId: terminalId, + targetPath: targetPath, + position: position, + targetProjectId: targetProjectId, +); + +/// Move a pane to a drop zone relative to another terminal. +Future movePaneTo({ + required String connId, + required String projectId, + required String terminalId, + required String targetProjectId, + required String targetTerminalId, + required String zone, +}) => RustLib.instance.api.crateApiStateMovePaneTo( + connId: connId, + projectId: projectId, + terminalId: terminalId, + targetProjectId: targetProjectId, + targetTerminalId: targetTerminalId, + zone: zone, +); + +/// Get file contents from git (working tree or staged). +Future gitFileContents({ + required String connId, + required String projectId, + required String filePath, + required String mode, +}) => RustLib.instance.api.crateApiStateGitFileContents( + connId: connId, + projectId: projectId, + filePath: filePath, + mode: mode, +); + /// FFI-friendly folder info. class FolderInfo { final String id; diff --git a/mobile/native/src/api/state.rs b/mobile/native/src/api/state.rs index 82c57a34..6bfe6e8c 100644 --- a/mobile/native/src/api/state.rs +++ b/mobile/native/src/api/state.rs @@ -561,3 +561,173 @@ pub async fn set_folder_color( ) .await } + +/// Reorder a project within a folder. +pub async fn reorder_project_in_folder( + conn_id: String, + folder_id: String, + project_id: String, + new_index: usize, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::ReorderProjectInFolder { + folder_id, + project_id, + new_index, + }, + ) + .await +} + +// ── Layout actions ───────────────────────────────────────────────── + +/// Update split sizes for a split pane. +pub async fn update_split_sizes( + conn_id: String, + project_id: String, + path: Vec, + sizes: Vec, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::UpdateSplitSizes { + project_id, + path, + sizes, + }, + ) + .await +} + +/// Add a new tab to a tab group. +pub async fn add_tab( + conn_id: String, + project_id: String, + path: Vec, + in_group: bool, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::AddTab { + project_id, + path, + in_group, + }, + ) + .await +} + +/// Set the active tab in a tab group. +pub async fn set_active_tab( + conn_id: String, + project_id: String, + path: Vec, + index: usize, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::SetActiveTab { + project_id, + path, + index, + }, + ) + .await +} + +/// Move a tab within a tab group. +pub async fn move_tab( + conn_id: String, + project_id: String, + path: Vec, + from_index: usize, + to_index: usize, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::MoveTab { + project_id, + path, + from_index, + to_index, + }, + ) + .await +} + +/// Move a terminal into a tab group. +pub async fn move_terminal_to_tab_group( + conn_id: String, + project_id: String, + terminal_id: String, + target_path: Vec, + position: Option, + target_project_id: Option, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::MoveTerminalToTabGroup { + project_id, + terminal_id, + target_path, + position, + target_project_id, + }, + ) + .await +} + +/// Move a pane to a drop zone relative to another terminal. +pub async fn move_pane_to( + conn_id: String, + project_id: String, + terminal_id: String, + target_project_id: String, + target_terminal_id: String, + zone: String, +) -> anyhow::Result<()> { + let mgr = ConnectionManager::get(); + mgr.send_action( + &conn_id, + ActionRequest::MovePaneTo { + project_id, + terminal_id, + target_project_id, + target_terminal_id, + zone, + }, + ) + .await +} + +// ── Additional git actions ───────────────────────────────────────── + +/// Get file contents from git (working tree or staged). +pub async fn git_file_contents( + conn_id: String, + project_id: String, + file_path: String, + mode: String, +) -> anyhow::Result { + let diff_mode = match mode.as_str() { + "staged" => okena_core::types::DiffMode::Staged, + _ => okena_core::types::DiffMode::WorkingTree, + }; + let mgr = ConnectionManager::get(); + mgr.send_action_with_response( + &conn_id, + ActionRequest::GitFileContents { + project_id, + file_path, + mode: diff_mode, + }, + ) + .await +} From 024ba82292bdd90db9c642196b71ece9f81be4ba Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 19:27:04 +0200 Subject: [PATCH 20/37] chore(mobile): regenerate flutter_rust_bridge bindings Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/rust/frb_generated.dart | 664 +++++++++++++++++++-- mobile/lib/src/rust/frb_generated.io.dart | 51 ++ mobile/lib/src/rust/frb_generated.web.dart | 51 ++ mobile/native/src/frb_generated.rs | 588 ++++++++++++++++-- 4 files changed, 1252 insertions(+), 102 deletions(-) diff --git a/mobile/lib/src/rust/frb_generated.dart b/mobile/lib/src/rust/frb_generated.dart index c033ab1c..76a052b6 100644 --- a/mobile/lib/src/rust/frb_generated.dart +++ b/mobile/lib/src/rust/frb_generated.dart @@ -68,7 +68,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => -142339940; + int get rustContentHash => 632182563; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -85,6 +85,13 @@ abstract class RustLibApi extends BaseApi { required String path, }); + Future crateApiStateAddTab({ + required String connId, + required String projectId, + required Uint64List path, + required bool inGroup, + }); + void crateApiTerminalClearSelection({ required String connId, required String terminalId, @@ -183,6 +190,13 @@ abstract class RustLibApi extends BaseApi { required String projectId, }); + Future crateApiStateGitFileContents({ + required String connId, + required String projectId, + required String filePath, + required String mode, + }); + Future crateApiStateGitStatus({ required String connId, required String projectId, @@ -195,6 +209,32 @@ abstract class RustLibApi extends BaseApi { required String terminalId, }); + Future crateApiStateMovePaneTo({ + required String connId, + required String projectId, + required String terminalId, + required String targetProjectId, + required String targetTerminalId, + required String zone, + }); + + Future crateApiStateMoveTab({ + required String connId, + required String projectId, + required Uint64List path, + required BigInt fromIndex, + required BigInt toIndex, + }); + + Future crateApiStateMoveTerminalToTabGroup({ + required String connId, + required String projectId, + required String terminalId, + required Uint64List targetPath, + BigInt? position, + String? targetProjectId, + }); + Future crateApiConnectionPair({ required String connId, required String code, @@ -217,6 +257,20 @@ abstract class RustLibApi extends BaseApi { required String name, }); + Future crateApiStateReorderProjectInFolder({ + required String connId, + required String folderId, + required String projectId, + required BigInt newIndex, + }); + + void crateApiTerminalResizeLocal({ + required String connId, + required String terminalId, + required int cols, + required int rows, + }); + void crateApiTerminalResizeTerminal({ required String connId, required String terminalId, @@ -256,6 +310,13 @@ abstract class RustLibApi extends BaseApi { required String text, }); + Future crateApiStateSetActiveTab({ + required String connId, + required String projectId, + required Uint64List path, + required BigInt index, + }); + Future crateApiStateSetFolderColor({ required String connId, required String folderId, @@ -329,6 +390,13 @@ abstract class RustLibApi extends BaseApi { required int col, required int row, }); + + Future crateApiStateUpdateSplitSizes({ + required String connId, + required String projectId, + required Uint64List path, + required List sizes, + }); } class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @@ -375,6 +443,44 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["connId", "name", "path"], ); + @override + Future crateApiStateAddTab({ + required String connId, + required String projectId, + required Uint64List path, + required bool inGroup, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_list_prim_usize_strict(path, serializer); + sse_encode_bool(inGroup, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 2, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateAddTabConstMeta, + argValues: [connId, projectId, path, inGroup], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateAddTabConstMeta => const TaskConstMeta( + debugName: "add_tab", + argNames: ["connId", "projectId", "path", "inGroup"], + ); + @override void crateApiTerminalClearSelection({ required String connId, @@ -386,7 +492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -421,7 +527,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 3, + funcId: 4, port: port_, ); }, @@ -457,7 +563,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 4, + funcId: 5, port: port_, ); }, @@ -491,7 +597,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(host, serializer); sse_encode_u_16(port, serializer); sse_encode_opt_String(savedToken, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -518,7 +624,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; }, codec: SseCodec( decodeSuccessData: sse_decode_connection_status, @@ -548,7 +654,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 8, port: port_, ); }, @@ -576,7 +682,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -608,7 +714,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 9, + funcId: 10, port: port_, ); }, @@ -635,7 +741,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_String, @@ -665,7 +771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; }, codec: SseCodec( decodeSuccessData: sse_decode_cursor_state, @@ -690,7 +796,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -716,7 +822,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_folder_info, @@ -739,7 +845,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_box_autoadd_fullscreen_info, @@ -769,7 +875,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(projectId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -795,7 +901,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_String, @@ -818,7 +924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_project_info, @@ -845,7 +951,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; }, codec: SseCodec( decodeSuccessData: sse_decode_scroll_info, @@ -875,7 +981,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -905,7 +1011,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_box_autoadd_selection_bounds, @@ -931,7 +1037,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -958,7 +1064,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_cell_data, @@ -991,7 +1097,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 24, port: port_, ); }, @@ -1027,7 +1133,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 25, port: port_, ); }, @@ -1061,7 +1167,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 25, + funcId: 26, port: port_, ); }, @@ -1082,6 +1188,45 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["connId", "projectId"], ); + @override + Future crateApiStateGitFileContents({ + required String connId, + required String projectId, + required String filePath, + required String mode, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(filePath, serializer); + sse_encode_String(mode, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 27, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateGitFileContentsConstMeta, + argValues: [connId, projectId, filePath, mode], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateGitFileContentsConstMeta => + const TaskConstMeta( + debugName: "git_file_contents", + argNames: ["connId", "projectId", "filePath", "mode"], + ); + @override Future crateApiStateGitStatus({ required String connId, @@ -1096,7 +1241,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 26, + funcId: 28, port: port_, ); }, @@ -1125,7 +1270,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 27, + funcId: 29, port: port_, ); }, @@ -1154,7 +1299,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -1172,6 +1317,159 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["connId", "terminalId"], ); + @override + Future crateApiStateMovePaneTo({ + required String connId, + required String projectId, + required String terminalId, + required String targetProjectId, + required String targetTerminalId, + required String zone, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_String(targetProjectId, serializer); + sse_encode_String(targetTerminalId, serializer); + sse_encode_String(zone, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 31, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateMovePaneToConstMeta, + argValues: [ + connId, + projectId, + terminalId, + targetProjectId, + targetTerminalId, + zone, + ], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateMovePaneToConstMeta => const TaskConstMeta( + debugName: "move_pane_to", + argNames: [ + "connId", + "projectId", + "terminalId", + "targetProjectId", + "targetTerminalId", + "zone", + ], + ); + + @override + Future crateApiStateMoveTab({ + required String connId, + required String projectId, + required Uint64List path, + required BigInt fromIndex, + required BigInt toIndex, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_list_prim_usize_strict(path, serializer); + sse_encode_usize(fromIndex, serializer); + sse_encode_usize(toIndex, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 32, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateMoveTabConstMeta, + argValues: [connId, projectId, path, fromIndex, toIndex], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateMoveTabConstMeta => const TaskConstMeta( + debugName: "move_tab", + argNames: ["connId", "projectId", "path", "fromIndex", "toIndex"], + ); + + @override + Future crateApiStateMoveTerminalToTabGroup({ + required String connId, + required String projectId, + required String terminalId, + required Uint64List targetPath, + BigInt? position, + String? targetProjectId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_list_prim_usize_strict(targetPath, serializer); + sse_encode_opt_box_autoadd_usize(position, serializer); + sse_encode_opt_String(targetProjectId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 33, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateMoveTerminalToTabGroupConstMeta, + argValues: [ + connId, + projectId, + terminalId, + targetPath, + position, + targetProjectId, + ], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateMoveTerminalToTabGroupConstMeta => + const TaskConstMeta( + debugName: "move_terminal_to_tab_group", + argNames: [ + "connId", + "projectId", + "terminalId", + "targetPath", + "position", + "targetProjectId", + ], + ); + @override Future crateApiConnectionPair({ required String connId, @@ -1186,7 +1484,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 34, port: port_, ); }, @@ -1218,7 +1516,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 30, + funcId: 35, port: port_, ); }, @@ -1252,7 +1550,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 36, port: port_, ); }, @@ -1291,7 +1589,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 32, + funcId: 37, port: port_, ); }, @@ -1312,6 +1610,79 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["connId", "projectId", "terminalId", "name"], ); + @override + Future crateApiStateReorderProjectInFolder({ + required String connId, + required String folderId, + required String projectId, + required BigInt newIndex, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(folderId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_usize(newIndex, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 38, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateReorderProjectInFolderConstMeta, + argValues: [connId, folderId, projectId, newIndex], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateReorderProjectInFolderConstMeta => + const TaskConstMeta( + debugName: "reorder_project_in_folder", + argNames: ["connId", "folderId", "projectId", "newIndex"], + ); + + @override + void crateApiTerminalResizeLocal({ + required String connId, + required String terminalId, + required int cols, + required int rows, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(terminalId, serializer); + sse_encode_u_16(cols, serializer); + sse_encode_u_16(rows, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiTerminalResizeLocalConstMeta, + argValues: [connId, terminalId, cols, rows], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiTerminalResizeLocalConstMeta => + const TaskConstMeta( + debugName: "resize_local", + argNames: ["connId", "terminalId", "cols", "rows"], + ); + @override void crateApiTerminalResizeTerminal({ required String connId, @@ -1327,7 +1698,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(terminalId, serializer); sse_encode_u_16(cols, serializer); sse_encode_u_16(rows, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1362,7 +1733,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 34, + funcId: 41, port: port_, ); }, @@ -1399,7 +1770,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 35, + funcId: 42, port: port_, ); }, @@ -1432,7 +1803,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(connId, serializer); sse_encode_String(terminalId, serializer); sse_encode_i_32(delta, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1457,7 +1828,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44)!; }, codec: SseCodec( decodeSuccessData: sse_decode_f_64, @@ -1492,7 +1863,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 45, port: port_, ); }, @@ -1529,7 +1900,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 46, port: port_, ); }, @@ -1549,6 +1920,44 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["connId", "terminalId", "text"], ); + @override + Future crateApiStateSetActiveTab({ + required String connId, + required String projectId, + required Uint64List path, + required BigInt index, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_list_prim_usize_strict(path, serializer); + sse_encode_usize(index, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 47, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateSetActiveTabConstMeta, + argValues: [connId, projectId, path, index], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateSetActiveTabConstMeta => const TaskConstMeta( + debugName: "set_active_tab", + argNames: ["connId", "projectId", "path", "index"], + ); + @override Future crateApiStateSetFolderColor({ required String connId, @@ -1565,7 +1974,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 48, port: port_, ); }, @@ -1602,7 +2011,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 49, port: port_, ); }, @@ -1638,7 +2047,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 50, port: port_, ); }, @@ -1677,7 +2086,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 51, port: port_, ); }, @@ -1711,7 +2120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 52, port: port_, ); }, @@ -1747,7 +2156,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(terminalId, serializer); sse_encode_u_16(col, serializer); sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 53)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1782,7 +2191,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 54, port: port_, ); }, @@ -1817,7 +2226,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(terminalId, serializer); sse_encode_u_16(col, serializer); sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1850,7 +2259,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 56, port: port_, ); }, @@ -1887,7 +2296,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 49, + funcId: 57, port: port_, ); }, @@ -1923,7 +2332,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 50, + funcId: 58, port: port_, ); }, @@ -1959,7 +2368,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(terminalId, serializer); sse_encode_u_16(col, serializer); sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 51)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 59)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1978,6 +2387,45 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["connId", "terminalId", "col", "row"], ); + @override + Future crateApiStateUpdateSplitSizes({ + required String connId, + required String projectId, + required Uint64List path, + required List sizes, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(connId, serializer); + sse_encode_String(projectId, serializer); + sse_encode_list_prim_usize_strict(path, serializer); + sse_encode_list_prim_f_32_loose(sizes, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 60, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiStateUpdateSplitSizesConstMeta, + argValues: [connId, projectId, path, sizes], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiStateUpdateSplitSizesConstMeta => + const TaskConstMeta( + debugName: "update_split_sizes", + argNames: ["connId", "projectId", "path", "sizes"], + ); + @protected AnyhowException dco_decode_AnyhowException(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2024,6 +2472,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + BigInt dco_decode_box_autoadd_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_usize(raw); + } + @protected CellData dco_decode_cell_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2077,6 +2531,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + double dco_decode_f_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + @protected double dco_decode_f_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2133,6 +2593,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (raw as List).map(dco_decode_folder_info).toList(); } + @protected + List dco_decode_list_prim_f_32_loose(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as List; + } + + @protected + Float32List dco_decode_list_prim_f_32_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Float32List; + } + @protected Uint16List dco_decode_list_prim_u_16_strict(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2193,6 +2665,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_u_32(raw); } + @protected + BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_usize(raw); + } + @protected ProjectInfo dco_decode_project_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2349,6 +2827,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_u_32(deserializer)); } + @protected + BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_usize(deserializer)); + } + @protected CellData sse_decode_cell_data(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2408,6 +2892,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + double sse_decode_f_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getFloat32(); + } + @protected double sse_decode_f_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2479,6 +2969,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + List sse_decode_list_prim_f_32_loose(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getFloat32List(len_); + } + + @protected + Float32List sse_decode_list_prim_f_32_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getFloat32List(len_); + } + @protected Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2586,6 +3090,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_usize(deserializer)); + } else { + return null; + } + } + @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2760,6 +3275,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_32(self, serializer); } + @protected + void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize(self, serializer); + } + @protected void sse_encode_cell_data(CellData self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2805,6 +3326,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_bool(self.visible, serializer); } + @protected + void sse_encode_f_32(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putFloat32(self); + } + @protected void sse_encode_f_64(double self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2869,6 +3396,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_prim_f_32_loose( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putFloat32List( + self is Float32List ? self : Float32List.fromList(self), + ); + } + + @protected + void sse_encode_list_prim_f_32_strict( + Float32List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putFloat32List(self); + } + @protected void sse_encode_list_prim_u_16_strict( Uint16List self, @@ -2981,6 +3530,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_usize( + BigInt? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_usize(self, serializer); + } + } + @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/mobile/lib/src/rust/frb_generated.io.dart b/mobile/lib/src/rust/frb_generated.io.dart index 4b0482cf..ac9322e6 100644 --- a/mobile/lib/src/rust/frb_generated.io.dart +++ b/mobile/lib/src/rust/frb_generated.io.dart @@ -41,6 +41,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + BigInt dco_decode_box_autoadd_usize(dynamic raw); + @protected CellData dco_decode_cell_data(dynamic raw); @@ -53,6 +56,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState dco_decode_cursor_state(dynamic raw); + @protected + double dco_decode_f_32(dynamic raw); + @protected double dco_decode_f_64(dynamic raw); @@ -74,6 +80,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_folder_info(dynamic raw); + @protected + List dco_decode_list_prim_f_32_loose(dynamic raw); + + @protected + Float32List dco_decode_list_prim_f_32_strict(dynamic raw); + @protected Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); @@ -104,6 +116,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected + BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); + @protected ProjectInfo dco_decode_project_info(dynamic raw); @@ -161,6 +176,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); + @protected CellData sse_decode_cell_data(SseDeserializer deserializer); @@ -173,6 +191,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState sse_decode_cursor_state(SseDeserializer deserializer); + @protected + double sse_decode_f_32(SseDeserializer deserializer); + @protected double sse_decode_f_64(SseDeserializer deserializer); @@ -194,6 +215,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_folder_info(SseDeserializer deserializer); + @protected + List sse_decode_list_prim_f_32_loose(SseDeserializer deserializer); + + @protected + Float32List sse_decode_list_prim_f_32_strict(SseDeserializer deserializer); + @protected Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); @@ -230,6 +257,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected + BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); + @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer); @@ -295,6 +325,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); + @protected void sse_encode_cell_data(CellData self, SseSerializer serializer); @@ -310,6 +343,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_cursor_state(CursorState self, SseSerializer serializer); + @protected + void sse_encode_f_32(double self, SseSerializer serializer); + @protected void sse_encode_f_64(double self, SseSerializer serializer); @@ -337,6 +373,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_prim_f_32_loose( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_prim_f_32_strict( + Float32List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_prim_u_16_strict( Uint16List self, @@ -391,6 +439,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); + @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); diff --git a/mobile/lib/src/rust/frb_generated.web.dart b/mobile/lib/src/rust/frb_generated.web.dart index 499c4cba..8fd5550d 100644 --- a/mobile/lib/src/rust/frb_generated.web.dart +++ b/mobile/lib/src/rust/frb_generated.web.dart @@ -43,6 +43,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + BigInt dco_decode_box_autoadd_usize(dynamic raw); + @protected CellData dco_decode_cell_data(dynamic raw); @@ -55,6 +58,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState dco_decode_cursor_state(dynamic raw); + @protected + double dco_decode_f_32(dynamic raw); + @protected double dco_decode_f_64(dynamic raw); @@ -76,6 +82,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_folder_info(dynamic raw); + @protected + List dco_decode_list_prim_f_32_loose(dynamic raw); + + @protected + Float32List dco_decode_list_prim_f_32_strict(dynamic raw); + @protected Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); @@ -106,6 +118,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected + BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); + @protected ProjectInfo dco_decode_project_info(dynamic raw); @@ -163,6 +178,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); + @protected CellData sse_decode_cell_data(SseDeserializer deserializer); @@ -175,6 +193,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected CursorState sse_decode_cursor_state(SseDeserializer deserializer); + @protected + double sse_decode_f_32(SseDeserializer deserializer); + @protected double sse_decode_f_64(SseDeserializer deserializer); @@ -196,6 +217,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_folder_info(SseDeserializer deserializer); + @protected + List sse_decode_list_prim_f_32_loose(SseDeserializer deserializer); + + @protected + Float32List sse_decode_list_prim_f_32_strict(SseDeserializer deserializer); + @protected Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); @@ -232,6 +259,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected + BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); + @protected ProjectInfo sse_decode_project_info(SseDeserializer deserializer); @@ -297,6 +327,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); + @protected void sse_encode_cell_data(CellData self, SseSerializer serializer); @@ -312,6 +345,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_cursor_state(CursorState self, SseSerializer serializer); + @protected + void sse_encode_f_32(double self, SseSerializer serializer); + @protected void sse_encode_f_64(double self, SseSerializer serializer); @@ -339,6 +375,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_prim_f_32_loose( + List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_prim_f_32_strict( + Float32List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_prim_u_16_strict( Uint16List self, @@ -393,6 +441,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); + @protected void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); diff --git a/mobile/native/src/frb_generated.rs b/mobile/native/src/frb_generated.rs index 64258e7f..754b738e 100644 --- a/mobile/native/src/frb_generated.rs +++ b/mobile/native/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -142339940; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 632182563; // Section: executor @@ -84,6 +84,51 @@ fn wire__crate__api__state__add_project_impl( }, ) } +fn wire__crate__api__state__add_tab_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "add_tab", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_path = >::sse_decode(&mut deserializer); + let api_in_group = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::add_tab( + api_conn_id, + api_project_id, + api_path, + api_in_group, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__terminal__clear_selection_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -919,6 +964,51 @@ fn wire__crate__api__state__git_diff_summary_impl( }, ) } +fn wire__crate__api__state__git_file_contents_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "git_file_contents", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_file_path = ::sse_decode(&mut deserializer); + let api_mode = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::git_file_contents( + api_conn_id, + api_project_id, + api_file_path, + api_mode, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__state__git_status_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1023,6 +1113,151 @@ fn wire__crate__api__state__is_dirty_impl( }, ) } +fn wire__crate__api__state__move_pane_to_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "move_pane_to", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_target_project_id = ::sse_decode(&mut deserializer); + let api_target_terminal_id = ::sse_decode(&mut deserializer); + let api_zone = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::move_pane_to( + api_conn_id, + api_project_id, + api_terminal_id, + api_target_project_id, + api_target_terminal_id, + api_zone, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__move_tab_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "move_tab", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_path = >::sse_decode(&mut deserializer); + let api_from_index = ::sse_decode(&mut deserializer); + let api_to_index = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::move_tab( + api_conn_id, + api_project_id, + api_path, + api_from_index, + api_to_index, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__state__move_terminal_to_tab_group_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "move_terminal_to_tab_group", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_target_path = >::sse_decode(&mut deserializer); + let api_position = >::sse_decode(&mut deserializer); + let api_target_project_id = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::move_terminal_to_tab_group( + api_conn_id, + api_project_id, + api_terminal_id, + api_target_path, + api_position, + api_target_project_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__connection__pair_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1181,6 +1416,91 @@ fn wire__crate__api__state__rename_terminal_impl( }, ) } +fn wire__crate__api__state__reorder_project_in_folder_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "reorder_project_in_folder", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_folder_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_new_index = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::reorder_project_in_folder( + api_conn_id, + api_folder_id, + api_project_id, + api_new_index, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__terminal__resize_local_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "resize_local", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_terminal_id = ::sse_decode(&mut deserializer); + let api_cols = ::sse_decode(&mut deserializer); + let api_rows = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::terminal::resize_local( + api_conn_id, + api_terminal_id, + api_cols, + api_rows, + ); + })?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__terminal__resize_terminal_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -1456,6 +1776,51 @@ fn wire__crate__api__terminal__send_text_impl( }, ) } +fn wire__crate__api__state__set_active_tab_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_active_tab", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_path = >::sse_decode(&mut deserializer); + let api_index = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::set_active_tab( + api_conn_id, + api_project_id, + api_path, + api_index, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__state__set_folder_color_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1957,6 +2322,51 @@ fn wire__crate__api__terminal__update_selection_impl( }, ) } +fn wire__crate__api__state__update_split_sizes_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "update_split_sizes", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_conn_id = ::sse_decode(&mut deserializer); + let api_project_id = ::sse_decode(&mut deserializer); + let api_path = >::sse_decode(&mut deserializer); + let api_sizes = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::state::update_split_sizes( + api_conn_id, + api_project_id, + api_path, + api_sizes, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} // Section: dart2rust @@ -2066,6 +2476,13 @@ impl SseDecode for crate::api::terminal::CursorState { } } +impl SseDecode for f32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_f32::().unwrap() + } +} + impl SseDecode for f64 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -2144,6 +2561,18 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -2264,6 +2693,17 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for crate::api::state::ProjectInfo { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -2399,32 +2839,50 @@ fn pde_ffi_dispatcher_primary_impl( // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { 1 => wire__crate__api__state__add_project_impl(port, ptr, rust_vec_len, data_len), - 3 => wire__crate__api__state__close_terminal_impl(port, ptr, rust_vec_len, data_len), - 4 => wire__crate__api__state__close_terminals_impl(port, ptr, rust_vec_len, data_len), - 7 => wire__crate__api__state__create_terminal_impl(port, ptr, rust_vec_len, data_len), - 9 => wire__crate__api__state__focus_terminal_impl(port, ptr, rust_vec_len, data_len), - 23 => wire__crate__api__state__git_branches_impl(port, ptr, rust_vec_len, data_len), - 24 => wire__crate__api__state__git_diff_impl(port, ptr, rust_vec_len, data_len), - 25 => wire__crate__api__state__git_diff_summary_impl(port, ptr, rust_vec_len, data_len), - 26 => wire__crate__api__state__git_status_impl(port, ptr, rust_vec_len, data_len), - 27 => wire__crate__api__connection__init_app_impl(port, ptr, rust_vec_len, data_len), - 29 => wire__crate__api__connection__pair_impl(port, ptr, rust_vec_len, data_len), - 30 => wire__crate__api__state__read_content_impl(port, ptr, rust_vec_len, data_len), - 31 => wire__crate__api__state__reload_services_impl(port, ptr, rust_vec_len, data_len), - 32 => wire__crate__api__state__rename_terminal_impl(port, ptr, rust_vec_len, data_len), - 34 => wire__crate__api__state__restart_service_impl(port, ptr, rust_vec_len, data_len), - 35 => wire__crate__api__state__run_command_impl(port, ptr, rust_vec_len, data_len), - 38 => wire__crate__api__state__send_special_key_impl(port, ptr, rust_vec_len, data_len), - 39 => wire__crate__api__terminal__send_text_impl(port, ptr, rust_vec_len, data_len), - 40 => wire__crate__api__state__set_folder_color_impl(port, ptr, rust_vec_len, data_len), - 41 => wire__crate__api__state__set_fullscreen_impl(port, ptr, rust_vec_len, data_len), - 42 => wire__crate__api__state__set_project_color_impl(port, ptr, rust_vec_len, data_len), - 43 => wire__crate__api__state__split_terminal_impl(port, ptr, rust_vec_len, data_len), - 44 => wire__crate__api__state__start_all_services_impl(port, ptr, rust_vec_len, data_len), - 46 => wire__crate__api__state__start_service_impl(port, ptr, rust_vec_len, data_len), - 48 => wire__crate__api__state__stop_all_services_impl(port, ptr, rust_vec_len, data_len), - 49 => wire__crate__api__state__stop_service_impl(port, ptr, rust_vec_len, data_len), - 50 => wire__crate__api__state__toggle_minimized_impl(port, ptr, rust_vec_len, data_len), + 2 => wire__crate__api__state__add_tab_impl(port, ptr, rust_vec_len, data_len), + 4 => wire__crate__api__state__close_terminal_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__api__state__close_terminals_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__state__create_terminal_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__api__state__focus_terminal_impl(port, ptr, rust_vec_len, data_len), + 24 => wire__crate__api__state__git_branches_impl(port, ptr, rust_vec_len, data_len), + 25 => wire__crate__api__state__git_diff_impl(port, ptr, rust_vec_len, data_len), + 26 => wire__crate__api__state__git_diff_summary_impl(port, ptr, rust_vec_len, data_len), + 27 => wire__crate__api__state__git_file_contents_impl(port, ptr, rust_vec_len, data_len), + 28 => wire__crate__api__state__git_status_impl(port, ptr, rust_vec_len, data_len), + 29 => wire__crate__api__connection__init_app_impl(port, ptr, rust_vec_len, data_len), + 31 => wire__crate__api__state__move_pane_to_impl(port, ptr, rust_vec_len, data_len), + 32 => wire__crate__api__state__move_tab_impl(port, ptr, rust_vec_len, data_len), + 33 => wire__crate__api__state__move_terminal_to_tab_group_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 34 => wire__crate__api__connection__pair_impl(port, ptr, rust_vec_len, data_len), + 35 => wire__crate__api__state__read_content_impl(port, ptr, rust_vec_len, data_len), + 36 => wire__crate__api__state__reload_services_impl(port, ptr, rust_vec_len, data_len), + 37 => wire__crate__api__state__rename_terminal_impl(port, ptr, rust_vec_len, data_len), + 38 => wire__crate__api__state__reorder_project_in_folder_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 41 => wire__crate__api__state__restart_service_impl(port, ptr, rust_vec_len, data_len), + 42 => wire__crate__api__state__run_command_impl(port, ptr, rust_vec_len, data_len), + 45 => wire__crate__api__state__send_special_key_impl(port, ptr, rust_vec_len, data_len), + 46 => wire__crate__api__terminal__send_text_impl(port, ptr, rust_vec_len, data_len), + 47 => wire__crate__api__state__set_active_tab_impl(port, ptr, rust_vec_len, data_len), + 48 => wire__crate__api__state__set_folder_color_impl(port, ptr, rust_vec_len, data_len), + 49 => wire__crate__api__state__set_fullscreen_impl(port, ptr, rust_vec_len, data_len), + 50 => wire__crate__api__state__set_project_color_impl(port, ptr, rust_vec_len, data_len), + 51 => wire__crate__api__state__split_terminal_impl(port, ptr, rust_vec_len, data_len), + 52 => wire__crate__api__state__start_all_services_impl(port, ptr, rust_vec_len, data_len), + 54 => wire__crate__api__state__start_service_impl(port, ptr, rust_vec_len, data_len), + 56 => wire__crate__api__state__stop_all_services_impl(port, ptr, rust_vec_len, data_len), + 57 => wire__crate__api__state__stop_service_impl(port, ptr, rust_vec_len, data_len), + 58 => wire__crate__api__state__toggle_minimized_impl(port, ptr, rust_vec_len, data_len), + 60 => wire__crate__api__state__update_split_sizes_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -2437,32 +2895,33 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 2 => wire__crate__api__terminal__clear_selection_impl(ptr, rust_vec_len, data_len), - 5 => wire__crate__api__connection__connect_impl(ptr, rust_vec_len, data_len), - 6 => wire__crate__api__connection__connection_status_impl(ptr, rust_vec_len, data_len), - 8 => wire__crate__api__connection__disconnect_impl(ptr, rust_vec_len, data_len), - 10 => wire__crate__api__state__get_all_terminal_ids_impl(ptr, rust_vec_len, data_len), - 11 => wire__crate__api__terminal__get_cursor_impl(ptr, rust_vec_len, data_len), - 12 => wire__crate__api__state__get_focused_project_id_impl(ptr, rust_vec_len, data_len), - 13 => wire__crate__api__state__get_folders_impl(ptr, rust_vec_len, data_len), - 14 => wire__crate__api__state__get_fullscreen_terminal_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__api__state__get_project_layout_json_impl(ptr, rust_vec_len, data_len), - 16 => wire__crate__api__state__get_project_order_impl(ptr, rust_vec_len, data_len), - 17 => wire__crate__api__state__get_projects_impl(ptr, rust_vec_len, data_len), - 18 => wire__crate__api__terminal__get_scroll_info_impl(ptr, rust_vec_len, data_len), - 19 => wire__crate__api__terminal__get_selected_text_impl(ptr, rust_vec_len, data_len), - 20 => wire__crate__api__terminal__get_selection_bounds_impl(ptr, rust_vec_len, data_len), - 21 => wire__crate__api__connection__get_token_impl(ptr, rust_vec_len, data_len), - 22 => wire__crate__api__terminal__get_visible_cells_impl(ptr, rust_vec_len, data_len), - 28 => wire__crate__api__state__is_dirty_impl(ptr, rust_vec_len, data_len), - 33 => wire__crate__api__terminal__resize_terminal_impl(ptr, rust_vec_len, data_len), - 36 => wire__crate__api__terminal__scroll_impl(ptr, rust_vec_len, data_len), - 37 => { + 3 => wire__crate__api__terminal__clear_selection_impl(ptr, rust_vec_len, data_len), + 6 => wire__crate__api__connection__connect_impl(ptr, rust_vec_len, data_len), + 7 => wire__crate__api__connection__connection_status_impl(ptr, rust_vec_len, data_len), + 9 => wire__crate__api__connection__disconnect_impl(ptr, rust_vec_len, data_len), + 11 => wire__crate__api__state__get_all_terminal_ids_impl(ptr, rust_vec_len, data_len), + 12 => wire__crate__api__terminal__get_cursor_impl(ptr, rust_vec_len, data_len), + 13 => wire__crate__api__state__get_focused_project_id_impl(ptr, rust_vec_len, data_len), + 14 => wire__crate__api__state__get_folders_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__api__state__get_fullscreen_terminal_impl(ptr, rust_vec_len, data_len), + 16 => wire__crate__api__state__get_project_layout_json_impl(ptr, rust_vec_len, data_len), + 17 => wire__crate__api__state__get_project_order_impl(ptr, rust_vec_len, data_len), + 18 => wire__crate__api__state__get_projects_impl(ptr, rust_vec_len, data_len), + 19 => wire__crate__api__terminal__get_scroll_info_impl(ptr, rust_vec_len, data_len), + 20 => wire__crate__api__terminal__get_selected_text_impl(ptr, rust_vec_len, data_len), + 21 => wire__crate__api__terminal__get_selection_bounds_impl(ptr, rust_vec_len, data_len), + 22 => wire__crate__api__connection__get_token_impl(ptr, rust_vec_len, data_len), + 23 => wire__crate__api__terminal__get_visible_cells_impl(ptr, rust_vec_len, data_len), + 30 => wire__crate__api__state__is_dirty_impl(ptr, rust_vec_len, data_len), + 39 => wire__crate__api__terminal__resize_local_impl(ptr, rust_vec_len, data_len), + 40 => wire__crate__api__terminal__resize_terminal_impl(ptr, rust_vec_len, data_len), + 43 => wire__crate__api__terminal__scroll_impl(ptr, rust_vec_len, data_len), + 44 => { wire__crate__api__connection__seconds_since_activity_impl(ptr, rust_vec_len, data_len) } - 45 => wire__crate__api__terminal__start_selection_impl(ptr, rust_vec_len, data_len), - 47 => wire__crate__api__terminal__start_word_selection_impl(ptr, rust_vec_len, data_len), - 51 => wire__crate__api__terminal__update_selection_impl(ptr, rust_vec_len, data_len), + 53 => wire__crate__api__terminal__start_selection_impl(ptr, rust_vec_len, data_len), + 55 => wire__crate__api__terminal__start_word_selection_impl(ptr, rust_vec_len, data_len), + 59 => wire__crate__api__terminal__update_selection_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -2800,6 +3259,13 @@ impl SseEncode for crate::api::terminal::CursorState { } } +impl SseEncode for f32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_f32::(self).unwrap(); + } +} + impl SseEncode for f64 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -2862,6 +3328,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -2962,6 +3438,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for crate::api::state::ProjectInfo { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { From 1613f56dbd99a8b89e88a2c4180394f1f70edcfb Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 19:27:08 +0200 Subject: [PATCH 21/37] feat(mobile): add resizable splits, tab management, and minimized terminal UI Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/widgets/layout_renderer.dart | 338 ++++++++++++++++---- 1 file changed, 269 insertions(+), 69 deletions(-) diff --git a/mobile/lib/src/widgets/layout_renderer.dart b/mobile/lib/src/widgets/layout_renderer.dart index 28fa8664..a9dd58ed 100644 --- a/mobile/lib/src/widgets/layout_renderer.dart +++ b/mobile/lib/src/widgets/layout_renderer.dart @@ -1,6 +1,5 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' show Uint64List; import '../../src/rust/api/state.dart' as state_ffi; import '../models/layout_node.dart'; @@ -30,12 +29,9 @@ class LayoutRenderer extends StatelessWidget { ); if (json != null) { - try { - final node = - LayoutNode.fromJson(jsonDecode(json) as Map); - return _buildNode(context, node); - } catch (_) { - // Fall through to fallback + final node = LayoutNode.fromJson(json); + if (node != null) { + return _buildNode(context, node, const []); } } @@ -51,26 +47,33 @@ class LayoutRenderer extends StatelessWidget { return TerminalView(connId: connId, terminalId: terminalIds.first, modifiers: modifiers); } - Widget _buildNode(BuildContext context, LayoutNode node) { + Widget _buildNode(BuildContext context, LayoutNode node, List path) { return switch (node) { - TerminalNode(:final terminalId) => terminalId != null - ? TerminalView(connId: connId, terminalId: terminalId, modifiers: modifiers) + TerminalNode(:final terminalId, :final minimized) => terminalId != null + ? minimized + ? _MinimizedTerminal( + connId: connId, + projectId: projectId, + terminalId: terminalId, + ) + : TerminalView(connId: connId, terminalId: terminalId, modifiers: modifiers) : const Center( child: Text('Empty terminal', style: TextStyle(color: OkenaColors.textTertiary)), ), SplitNode(:final direction, :final sizes, :final children) => - _buildSplit(context, direction, sizes, children), + _buildSplit(context, direction, sizes, children, path), TabsNode(:final activeTab, :final children) => - _buildTabs(context, activeTab, children), + _buildTabs(context, activeTab, children, path), }; } Widget _buildSplit( BuildContext context, - String direction, + SplitDirection direction, List sizes, List children, + List path, ) { if (children.isEmpty) { return const SizedBox.shrink(); @@ -80,28 +83,16 @@ class LayoutRenderer extends StatelessWidget { final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; final isVertical = - direction == 'vertical' || (direction == 'horizontal' && isPortrait); - - final flexChildren = []; - for (int i = 0; i < children.length; i++) { - final flex = i < sizes.length ? sizes[i].round().clamp(1, 1000) : 1; - if (i > 0) { - flexChildren.add( - isVertical - ? const Divider( - height: 2, thickness: 2, color: OkenaColors.borderLight) - : const VerticalDivider( - width: 2, thickness: 2, color: OkenaColors.borderLight), - ); - } - flexChildren.add( - Expanded(flex: flex, child: _buildNode(context, children[i])), - ); - } + direction == SplitDirection.vertical || (direction == SplitDirection.horizontal && isPortrait); - return Flex( - direction: isVertical ? Axis.vertical : Axis.horizontal, - children: flexChildren, + return _ResizableSplit( + connId: connId, + projectId: projectId, + path: path, + isVertical: isVertical, + sizes: sizes, + children: children, + builder: (node, index) => _buildNode(context, node, [...path, index]), ); } @@ -109,43 +100,219 @@ class LayoutRenderer extends StatelessWidget { BuildContext context, int activeTab, List children, + List path, ) { if (children.isEmpty) { return const SizedBox.shrink(); } return _TabsWidget( - initialTab: activeTab.clamp(0, children.length - 1), + connId: connId, + projectId: projectId, + path: path, + activeTab: activeTab.clamp(0, children.length - 1), children: children, - builder: (node) => _buildNode(context, node), + builder: (node, index) => _buildNode(context, node, [...path, index]), + ); + } +} + +/// A minimized terminal placeholder. +class _MinimizedTerminal extends StatelessWidget { + final String connId; + final String projectId; + final String terminalId; + + const _MinimizedTerminal({ + required this.connId, + required this.projectId, + required this.terminalId, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + state_ffi.toggleMinimized( + connId: connId, + projectId: projectId, + terminalId: terminalId, + ); + }, + child: Container( + height: 36, + color: OkenaColors.surfaceElevated, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + const Icon(Icons.terminal, size: 16, color: OkenaColors.textSecondary), + const SizedBox(width: 8), + Text( + terminalId.length > 8 + ? '...${terminalId.substring(terminalId.length - 8)}' + : terminalId, + style: const TextStyle( + fontSize: 12, + color: OkenaColors.textSecondary, + fontFamily: 'JetBrainsMono', + ), + ), + const Spacer(), + const Icon(Icons.expand_more, size: 16, color: OkenaColors.textTertiary), + ], + ), + ), ); } } -class _TabsWidget extends StatefulWidget { - final int initialTab; +/// Resizable split pane with draggable dividers. +class _ResizableSplit extends StatefulWidget { + final String connId; + final String projectId; + final List path; + final bool isVertical; + final List sizes; final List children; - final Widget Function(LayoutNode) builder; + final Widget Function(LayoutNode, int) builder; - const _TabsWidget({ - required this.initialTab, + const _ResizableSplit({ + required this.connId, + required this.projectId, + required this.path, + required this.isVertical, + required this.sizes, required this.children, required this.builder, }); @override - State<_TabsWidget> createState() => _TabsWidgetState(); + State<_ResizableSplit> createState() => _ResizableSplitState(); } -class _TabsWidgetState extends State<_TabsWidget> { - late int _activeTab; +class _ResizableSplitState extends State<_ResizableSplit> { + late List _sizes; @override void initState() { super.initState(); - _activeTab = widget.initialTab; + _sizes = List.of(widget.sizes); + _ensureSizes(); + } + + @override + void didUpdateWidget(_ResizableSplit oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.sizes != widget.sizes) { + _sizes = List.of(widget.sizes); + _ensureSizes(); + } + } + + void _ensureSizes() { + while (_sizes.length < widget.children.length) { + _sizes.add(1.0); + } } + void _onDividerDrag(int dividerIndex, double delta, double totalSize) { + if (totalSize <= 0) return; + final total = _sizes.reduce((a, b) => a + b); + final fraction = delta / totalSize * total; + + setState(() { + _sizes[dividerIndex] = (_sizes[dividerIndex] + fraction).clamp(0.1, total); + _sizes[dividerIndex + 1] = (_sizes[dividerIndex + 1] - fraction).clamp(0.1, total); + }); + } + + void _onDividerDragEnd() { + state_ffi.updateSplitSizes( + connId: widget.connId, + projectId: widget.projectId, + path: Uint64List.fromList(widget.path), + sizes: _sizes.map((s) => s).toList(), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final totalSize = widget.isVertical + ? constraints.maxHeight + : constraints.maxWidth; + + final flexChildren = []; + for (int i = 0; i < widget.children.length; i++) { + final flex = i < _sizes.length ? _sizes[i].round().clamp(1, 1000) : 1; + if (i > 0) { + final dividerIndex = i - 1; + flexChildren.add( + GestureDetector( + behavior: HitTestBehavior.opaque, + onVerticalDragUpdate: widget.isVertical + ? (details) => _onDividerDrag(dividerIndex, details.delta.dy, totalSize) + : null, + onHorizontalDragUpdate: !widget.isVertical + ? (details) => _onDividerDrag(dividerIndex, details.delta.dx, totalSize) + : null, + onVerticalDragEnd: widget.isVertical ? (_) => _onDividerDragEnd() : null, + onHorizontalDragEnd: !widget.isVertical ? (_) => _onDividerDragEnd() : null, + child: MouseRegion( + cursor: widget.isVertical + ? SystemMouseCursors.resizeRow + : SystemMouseCursors.resizeColumn, + child: Container( + width: widget.isVertical ? double.infinity : 6, + height: widget.isVertical ? 6 : double.infinity, + color: Colors.transparent, + child: Center( + child: Container( + width: widget.isVertical ? 32 : 2, + height: widget.isVertical ? 2 : 32, + decoration: BoxDecoration( + color: OkenaColors.borderLight, + borderRadius: BorderRadius.circular(1), + ), + ), + ), + ), + ), + ), + ); + } + flexChildren.add( + Expanded(flex: flex, child: widget.builder(widget.children[i], i)), + ); + } + + return Flex( + direction: widget.isVertical ? Axis.vertical : Axis.horizontal, + children: flexChildren, + ); + }, + ); + } +} + +class _TabsWidget extends StatelessWidget { + final String connId; + final String projectId; + final List path; + final int activeTab; + final List children; + final Widget Function(LayoutNode, int) builder; + + const _TabsWidget({ + required this.connId, + required this.projectId, + required this.path, + required this.activeTab, + required this.children, + required this.builder, + }); + @override Widget build(BuildContext context) { return Column( @@ -155,36 +322,69 @@ class _TabsWidgetState extends State<_TabsWidget> { color: OkenaColors.surfaceElevated, child: Row( children: [ - for (int i = 0; i < widget.children.length; i++) - GestureDetector( - onTap: () => setState(() => _activeTab = i), - child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: i == _activeTab - ? OkenaColors.accent - : Colors.transparent, - width: 2, + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (int i = 0; i < children.length; i++) + GestureDetector( + onTap: () { + if (i != activeTab) { + state_ffi.setActiveTab( + connId: connId, + projectId: projectId, + path: Uint64List.fromList(path), + index: BigInt.from(i), + ); + } + }, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: i == activeTab + ? OkenaColors.accent + : Colors.transparent, + width: 2, + ), + ), + ), + child: Text( + _tabLabel(children[i], i), + style: TextStyle( + color: i == activeTab ? OkenaColors.textPrimary : OkenaColors.textSecondary, + fontSize: 12, + ), + ), + ), ), - ), - ), - child: Text( - _tabLabel(widget.children[i], i), - style: TextStyle( - color: i == _activeTab ? OkenaColors.textPrimary : OkenaColors.textSecondary, - fontSize: 12, - ), - ), + ], ), ), + ), + // Add tab button + GestureDetector( + onTap: () { + state_ffi.addTab( + connId: connId, + projectId: projectId, + path: Uint64List.fromList(path), + inGroup: true, + ); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Icon(Icons.add, size: 16, color: OkenaColors.textTertiary), + ), + ), ], ), ), Expanded( - child: widget.builder(widget.children[_activeTab]), + child: builder(children[activeTab], activeTab), ), ], ); From 886001b4fb0bb06cec2b684b5976d7b6c3b5476d Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 19:27:13 +0200 Subject: [PATCH 22/37] feat(mobile): add project drawer enhancements (add project, reorder, color picker) Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/widgets/project_drawer.dart | 369 +++++++++++++++++++-- 1 file changed, 350 insertions(+), 19 deletions(-) diff --git a/mobile/lib/src/widgets/project_drawer.dart b/mobile/lib/src/widgets/project_drawer.dart index 6f6deb49..21ad0185 100644 --- a/mobile/lib/src/widgets/project_drawer.dart +++ b/mobile/lib/src/widgets/project_drawer.dart @@ -46,6 +46,13 @@ class ProjectDrawer extends StatelessWidget { ), ), const Divider(height: 1), + // Add Project button + if (connection.connId != null) + ListTile( + leading: const Icon(Icons.create_new_folder), + title: const Text('Add Project'), + onTap: () => _showAddProjectDialog(context, connection.connId!), + ), ListTile( leading: const Icon(Icons.link_off), title: const Text('Disconnect'), @@ -58,6 +65,52 @@ class ProjectDrawer extends StatelessWidget { ), ); } + + void _showAddProjectDialog(BuildContext context, String connId) { + final nameController = TextEditingController(); + final pathController = TextEditingController(); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Add Project'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + autofocus: true, + decoration: const InputDecoration(labelText: 'Name'), + ), + const SizedBox(height: 12), + TextField( + controller: pathController, + decoration: const InputDecoration( + labelText: 'Path', + hintText: '/home/user/project', + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + final name = nameController.text.trim(); + final path = pathController.text.trim(); + if (name.isNotEmpty && path.isNotEmpty) { + Navigator.of(ctx).pop(); + state_ffi.addProject(connId: connId, name: name, path: path); + } + }, + child: const Text('Add'), + ), + ], + ), + ); + } } class _ProjectList extends StatelessWidget { @@ -132,6 +185,13 @@ class _ProjectList extends StatelessWidget { } } +// ── Color constants ──────────────────────────────────────────────────── + +const _colorOptions = [ + 'red', 'orange', 'yellow', 'lime', 'green', + 'teal', 'cyan', 'blue', 'purple', 'pink', +]; + Color _folderColorToColor(String colorName) { switch (colorName) { case 'red': @@ -159,6 +219,60 @@ Color _folderColorToColor(String colorName) { } } +/// Color picker for projects and folders. +void _showColorPicker({ + required BuildContext context, + required String currentColor, + required ValueChanged onSelect, +}) { + showModalBottomSheet( + context: context, + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Choose Color', + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: _colorOptions.map((name) { + final color = _folderColorToColor(name); + final isSelected = name == currentColor; + return GestureDetector( + onTap: () { + Navigator.of(ctx).pop(); + onSelect(name); + }, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: isSelected + ? Border.all(color: Colors.white, width: 3) + : null, + ), + child: isSelected + ? const Icon(Icons.check, size: 20, color: Colors.white) + : null, + ), + ); + }).toList(), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); +} + class _FolderTile extends StatelessWidget { final state_ffi.FolderInfo folder; final List projects; @@ -178,48 +292,117 @@ class _FolderTile extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 4), - child: Row( - children: [ - Icon(Icons.folder, size: 18, color: color), - const SizedBox(width: 8), - Expanded( - child: Text( - folder.name, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: color, - letterSpacing: 0.5, + GestureDetector( + onLongPress: () { + final connId = connection.connId; + if (connId != null) { + _showColorPicker( + context: context, + currentColor: folder.folderColor, + onSelect: (newColor) { + state_ffi.setFolderColor( + connId: connId, + folderId: folder.id, + color: newColor, + ); + }, + ); + } + }, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 4), + child: Row( + children: [ + Icon(Icons.folder, size: 18, color: color), + const SizedBox(width: 8), + Expanded( + child: Text( + folder.name, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color, + letterSpacing: 0.5, + ), ), ), - ), - ], + ], + ), ), ), - ...projects.map((p) => _ProjectTile( - project: p, + ...projects.asMap().entries.map((entry) => _ReorderableProjectTile( + key: ValueKey('folder-${folder.id}-${entry.value.id}'), + project: entry.value, workspace: workspace, connection: connection, - indent: true, + folderId: folder.id, + index: entry.key, + totalCount: projects.length, )), ], ); } } +class _ReorderableProjectTile extends StatelessWidget { + final state_ffi.ProjectInfo project; + final WorkspaceProvider workspace; + final ConnectionProvider connection; + final String folderId; + final int index; + final int totalCount; + + const _ReorderableProjectTile({ + super.key, + required this.project, + required this.workspace, + required this.connection, + required this.folderId, + required this.index, + required this.totalCount, + }); + + @override + Widget build(BuildContext context) { + return _ProjectTile( + project: project, + workspace: workspace, + connection: connection, + indent: true, + onReorder: totalCount > 1 ? (newIndex) { + final connId = connection.connId; + if (connId != null) { + state_ffi.reorderProjectInFolder( + connId: connId, + folderId: folderId, + projectId: project.id, + newIndex: BigInt.from(newIndex), + ); + } + } : null, + reorderIndex: index, + reorderTotal: totalCount, + ); + } +} + class _ProjectTile extends StatelessWidget { final state_ffi.ProjectInfo project; final WorkspaceProvider workspace; final ConnectionProvider connection; final bool indent; + final void Function(int newIndex)? onReorder; + final int? reorderIndex; + final int? reorderTotal; const _ProjectTile({ required this.project, required this.workspace, required this.connection, this.indent = false, + this.onReorder, + this.reorderIndex, + this.reorderTotal, }); @override @@ -244,6 +427,60 @@ class _ProjectTile extends StatelessWidget { onTap: () { workspace.selectProject(project.id); }, + onLongPress: () { + if (onReorder != null) { + _showReorderMenu(context); + } else { + // Show color picker for standalone projects + final connId = connection.connId; + if (connId != null) { + _showColorPicker( + context: context, + currentColor: project.folderColor, + onSelect: (newColor) { + state_ffi.setProjectColor( + connId: connId, + projectId: project.id, + color: newColor, + ); + }, + ); + } + } + }, + trailing: onReorder != null + ? SizedBox( + width: 64, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (reorderIndex != null && reorderIndex! > 0) + SizedBox( + width: 32, + height: 32, + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 16, + icon: const Icon(Icons.arrow_upward, size: 16), + onPressed: () => onReorder!(reorderIndex! - 1), + ), + ), + if (reorderIndex != null && reorderTotal != null && + reorderIndex! < reorderTotal! - 1) + SizedBox( + width: 32, + height: 32, + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 16, + icon: const Icon(Icons.arrow_downward, size: 16), + onPressed: () => onReorder!(reorderIndex! + 1), + ), + ), + ], + ), + ) + : null, ), if (isSelected) ...[ // Git status @@ -292,6 +529,12 @@ class _ProjectTile extends StatelessWidget { ), onTap: () { workspace.selectTerminal(tid); + // Also focus the terminal on the server + state_ffi.focusTerminal( + connId: connection.connId!, + projectId: project.id, + terminalId: tid, + ); Navigator.of(context).pop(); }, onLongPress: () { @@ -338,6 +581,82 @@ class _ProjectTile extends StatelessWidget { ); } + void _showReorderMenu(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text(project.name, + style: Theme.of(context).textTheme.titleMedium), + ), + // Color picker option + if (connection.connId != null) + ListTile( + leading: const Icon(Icons.palette), + title: const Text('Change Color'), + onTap: () { + Navigator.of(ctx).pop(); + _showColorPicker( + context: context, + currentColor: project.folderColor, + onSelect: (newColor) { + state_ffi.setProjectColor( + connId: connection.connId!, + projectId: project.id, + color: newColor, + ); + }, + ); + }, + ), + if (reorderIndex != null && reorderIndex! > 0) + ListTile( + leading: const Icon(Icons.arrow_upward), + title: const Text('Move Up'), + onTap: () { + Navigator.of(ctx).pop(); + onReorder!(reorderIndex! - 1); + }, + ), + if (reorderIndex != null && reorderTotal != null && + reorderIndex! < reorderTotal! - 1) + ListTile( + leading: const Icon(Icons.arrow_downward), + title: const Text('Move Down'), + onTap: () { + Navigator.of(ctx).pop(); + onReorder!(reorderIndex! + 1); + }, + ), + if (reorderIndex != null && reorderIndex! > 0) + ListTile( + leading: const Icon(Icons.vertical_align_top), + title: const Text('Move to Top'), + onTap: () { + Navigator.of(ctx).pop(); + onReorder!(0); + }, + ), + if (reorderIndex != null && reorderTotal != null && + reorderIndex! < reorderTotal! - 1) + ListTile( + leading: const Icon(Icons.vertical_align_bottom), + title: const Text('Move to Bottom'), + onTap: () { + Navigator.of(ctx).pop(); + onReorder!(reorderTotal! - 1); + }, + ), + ], + ), + ), + ); + } + Widget? _buildSubtitle(BuildContext context) { final parts = []; if (project.gitBranch != null) { @@ -388,6 +707,18 @@ class _ProjectTile extends StatelessWidget { currentName: name); }, ), + ListTile( + leading: const Icon(Icons.minimize), + title: const Text('Minimize'), + onTap: () { + Navigator.of(ctx).pop(); + state_ffi.toggleMinimized( + connId: connId, + projectId: projectId, + terminalId: terminalId, + ); + }, + ), ListTile( leading: const Icon(Icons.close, color: Colors.redAccent), title: From 988fb4ecf2659c42a9a0d21f7e5b4845d9cac7db Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Sun, 29 Mar 2026 19:27:18 +0200 Subject: [PATCH 23/37] feat(mobile): add git diff viewer and file contents viewer Co-Authored-By: Claude Opus 4.6 --- mobile/lib/src/screens/workspace_screen.dart | 564 +++++++++++++++++-- 1 file changed, 502 insertions(+), 62 deletions(-) diff --git a/mobile/lib/src/screens/workspace_screen.dart b/mobile/lib/src/screens/workspace_screen.dart index 775cb841..73c7285e 100644 --- a/mobile/lib/src/screens/workspace_screen.dart +++ b/mobile/lib/src/screens/workspace_screen.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' show Uint64List; import 'package:provider/provider.dart'; import '../providers/connection_provider.dart'; @@ -10,9 +11,22 @@ import '../widgets/project_drawer.dart'; import '../widgets/key_toolbar.dart'; import '../widgets/terminal_view.dart'; -class WorkspaceScreen extends StatelessWidget { +class WorkspaceScreen extends StatefulWidget { const WorkspaceScreen({super.key}); + @override + State createState() => _WorkspaceScreenState(); +} + +class _WorkspaceScreenState extends State { + final KeyModifiers _modifiers = KeyModifiers(); + + @override + void dispose() { + _modifiers.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final workspace = context.watch(); @@ -79,15 +93,85 @@ class WorkspaceScreen extends StatelessWidget { } }, ), + // More actions (split, minimize, new terminal) if (connId != null && project != null) - IconButton( - icon: const Icon(Icons.add), - tooltip: 'New Terminal', - onPressed: () { - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); + PopupMenuButton( + icon: const Icon(Icons.add, size: 22), + tooltip: 'Terminal actions', + itemBuilder: (ctx) => [ + const PopupMenuItem( + value: 'new', + child: ListTile( + leading: Icon(Icons.add, size: 20), + title: Text('New Terminal'), + dense: true, + contentPadding: EdgeInsets.zero, + ), + ), + if (selectedTerminalId != null) ...[ + const PopupMenuItem( + value: 'split_vertical', + child: ListTile( + leading: Icon(Icons.vertical_split, size: 20), + title: Text('Split Vertical'), + dense: true, + contentPadding: EdgeInsets.zero, + ), + ), + const PopupMenuItem( + value: 'split_horizontal', + child: ListTile( + leading: Icon(Icons.horizontal_split, size: 20), + title: Text('Split Horizontal'), + dense: true, + contentPadding: EdgeInsets.zero, + ), + ), + const PopupMenuItem( + value: 'minimize', + child: ListTile( + leading: Icon(Icons.minimize, size: 20), + title: Text('Minimize'), + dense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ], + ], + onSelected: (value) { + switch (value) { + case 'new': + state_ffi.createTerminal( + connId: connId, + projectId: project.id, + ); + break; + case 'split_vertical': + state_ffi.splitTerminal( + connId: connId, + projectId: project.id, + path: Uint64List.fromList([]), + direction: 'vertical', + ); + break; + case 'split_horizontal': + state_ffi.splitTerminal( + connId: connId, + projectId: project.id, + path: Uint64List.fromList([]), + direction: 'horizontal', + ); + break; + case 'minimize': + if (selectedTerminalId != null) { + state_ffi.toggleMinimized( + connId: connId, + projectId: project.id, + terminalId: selectedTerminalId, + ); + } + break; + } }, ), ], @@ -133,22 +217,13 @@ class WorkspaceScreen extends StatelessWidget { child: TerminalView( connId: connId, terminalId: selectedTerminalId, - onTerminalSwipe: (direction) { - final ids = project.terminalIds; - if (ids.length <= 1) return; - final idx = ids.indexOf(selectedTerminalId); - if (idx < 0) return; - final newIdx = - (idx + direction).clamp(0, ids.length - 1); - if (newIdx != idx) { - workspace.selectTerminal(ids[newIdx]); - } - }, + modifiers: _modifiers, ), ), KeyToolbar( connId: connId, terminalId: selectedTerminalId, + modifiers: _modifiers, ), ], ), @@ -341,6 +416,44 @@ class _TerminalTabBar extends StatelessWidget { _showRenameDialog(context, terminalId, name); }, ), + ListTile( + leading: const Icon(Icons.vertical_split), + title: const Text('Split Vertical'), + onTap: () { + Navigator.of(ctx).pop(); + state_ffi.splitTerminal( + connId: connId, + projectId: projectId, + path: Uint64List.fromList([]), + direction: 'vertical', + ); + }, + ), + ListTile( + leading: const Icon(Icons.horizontal_split), + title: const Text('Split Horizontal'), + onTap: () { + Navigator.of(ctx).pop(); + state_ffi.splitTerminal( + connId: connId, + projectId: projectId, + path: Uint64List.fromList([]), + direction: 'horizontal', + ); + }, + ), + ListTile( + leading: const Icon(Icons.minimize), + title: const Text('Minimize'), + onTap: () { + Navigator.of(ctx).pop(); + state_ffi.toggleMinimized( + connId: connId, + projectId: projectId, + terminalId: terminalId, + ); + }, + ), ListTile( leading: const Icon(Icons.close, color: Colors.redAccent), title: const Text('Close', @@ -488,17 +601,28 @@ class _GitSheet extends StatefulWidget { State<_GitSheet> createState() => _GitSheetState(); } -class _GitSheetState extends State<_GitSheet> { +class _GitSheetState extends State<_GitSheet> with SingleTickerProviderStateMixin { String? _diffSummary; String? _branches; + String? _gitStatus; + String? _workingTreeDiff; + String? _stagedDiff; bool _loading = true; + late TabController _tabController; @override void initState() { super.initState(); + _tabController = TabController(length: 4, vsync: this); _loadData(); } + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + Future _loadData() async { try { final results = await Future.wait([ @@ -506,11 +630,20 @@ class _GitSheetState extends State<_GitSheet> { connId: widget.connId, projectId: widget.project.id), state_ffi.gitBranches( connId: widget.connId, projectId: widget.project.id), + state_ffi.gitStatus( + connId: widget.connId, projectId: widget.project.id), + state_ffi.gitDiff( + connId: widget.connId, projectId: widget.project.id, mode: 'working_tree'), + state_ffi.gitDiff( + connId: widget.connId, projectId: widget.project.id, mode: 'staged'), ]); if (mounted) { setState(() { _diffSummary = results[0]; _branches = results[1]; + _gitStatus = results[2]; + _workingTreeDiff = results[3]; + _stagedDiff = results[4]; _loading = false; }); } @@ -577,27 +710,63 @@ class _GitSheetState extends State<_GitSheet> { ], ), ), - const Divider(), + // Tab bar + TabBar( + controller: _tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + labelStyle: const TextStyle(fontSize: 13), + tabs: const [ + Tab(text: 'Changes'), + Tab(text: 'Diff'), + Tab(text: 'Staged'), + Tab(text: 'Branches'), + ], + ), Expanded( child: _loading ? const Center(child: CircularProgressIndicator()) - : ListView( - controller: widget.scrollController, - padding: const EdgeInsets.all(16), + : TabBarView( + controller: _tabController, children: [ - if (_diffSummary != null) ...[ - Text('Changes', - style: Theme.of(context).textTheme.titleSmall), - const SizedBox(height: 8), - _DiffSummaryView(json: _diffSummary!), - const SizedBox(height: 16), - ], - if (_branches != null) ...[ - Text('Branches', - style: Theme.of(context).textTheme.titleSmall), - const SizedBox(height: 8), - _BranchesView(json: _branches!), - ], + // Changes tab + ListView( + controller: widget.scrollController, + padding: const EdgeInsets.all(16), + children: [ + if (_diffSummary != null) + _DiffSummaryView( + json: _diffSummary!, + connId: widget.connId, + projectId: widget.project.id, + ), + if (_gitStatus != null) ...[ + const SizedBox(height: 16), + Text('Status', + style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 8), + _GitStatusView(json: _gitStatus!), + ], + ], + ), + // Working tree diff tab + _DiffContentView( + diff: _workingTreeDiff, + scrollController: widget.scrollController, + ), + // Staged diff tab + _DiffContentView( + diff: _stagedDiff, + scrollController: widget.scrollController, + ), + // Branches tab + ListView( + controller: widget.scrollController, + padding: const EdgeInsets.all(16), + children: [ + if (_branches != null) _BranchesView(json: _branches!), + ], + ), ], ), ), @@ -606,10 +775,137 @@ class _GitSheetState extends State<_GitSheet> { } } +/// Renders git status JSON. +class _GitStatusView extends StatelessWidget { + final String json; + + const _GitStatusView({required this.json}); + + @override + Widget build(BuildContext context) { + try { + final data = jsonDecode(json); + if (data is Map) { + final entries = []; + + void addSection(String title, dynamic files) { + if (files is List && files.isNotEmpty) { + entries.add(Padding( + padding: const EdgeInsets.only(top: 8, bottom: 4), + child: Text( + title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.grey[400], + ), + ), + )); + for (final f in files) { + final path = f is String ? f : (f is Map ? f['path'] as String? ?? '' : f.toString()); + entries.add(Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text( + path, + style: const TextStyle( + fontSize: 12, + fontFamily: 'JetBrainsMono', + ), + ), + )); + } + } + } + + addSection('Staged', data['staged']); + addSection('Modified', data['modified'] ?? data['unstaged']); + addSection('Untracked', data['untracked']); + + if (entries.isEmpty) { + return Text('Clean working tree', + style: TextStyle(color: Colors.grey[500])); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: entries, + ); + } + return Text(json, + style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); + } catch (_) { + return Text(json, + style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); + } + } +} + +/// Full diff content viewer with syntax-colored diff lines. +class _DiffContentView extends StatelessWidget { + final String? diff; + final ScrollController scrollController; + + const _DiffContentView({ + required this.diff, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + if (diff == null || diff!.isEmpty) { + return Center( + child: Text('No changes', style: TextStyle(color: Colors.grey[500])), + ); + } + + final lines = diff!.split('\n'); + return ListView.builder( + controller: scrollController, + itemCount: lines.length, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + itemBuilder: (context, index) { + final line = lines[index]; + Color? bgColor; + Color textColor = Colors.grey[300]!; + + if (line.startsWith('+')) { + bgColor = Colors.green.withValues(alpha: 0.1); + textColor = Colors.green[300]!; + } else if (line.startsWith('-')) { + bgColor = Colors.red.withValues(alpha: 0.1); + textColor = Colors.red[300]!; + } else if (line.startsWith('@@')) { + textColor = Colors.cyan[300]!; + } else if (line.startsWith('diff ') || line.startsWith('index ')) { + textColor = Colors.grey[500]!; + } + + return Container( + color: bgColor, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), + child: Text( + line, + style: TextStyle( + fontSize: 11, + fontFamily: 'JetBrainsMono', + color: textColor, + ), + ), + ); + }, + ); + } +} + class _DiffSummaryView extends StatelessWidget { final String json; + final String? connId; + final String? projectId; - const _DiffSummaryView({required this.json}); + const _DiffSummaryView({ + required this.json, + this.connId, + this.projectId, + }); @override Widget build(BuildContext context) { @@ -645,30 +941,35 @@ class _DiffSummaryView extends StatelessWidget { iconColor = Colors.orange; } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Icon(icon, size: 16, color: iconColor), - const SizedBox(width: 8), - Expanded( - child: Text( - path, - style: const TextStyle( - fontSize: 12, fontFamily: 'JetBrainsMono'), - overflow: TextOverflow.ellipsis, + return InkWell( + onTap: connId != null && projectId != null + ? () => _showFileContents(context, path) + : null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Icon(icon, size: 16, color: iconColor), + const SizedBox(width: 8), + Expanded( + child: Text( + path, + style: const TextStyle( + fontSize: 12, fontFamily: 'JetBrainsMono'), + overflow: TextOverflow.ellipsis, + ), ), - ), - if (added > 0) - Text('+$added', - style: TextStyle( - fontSize: 11, color: Colors.green[400])), - if (added > 0 && removed > 0) const SizedBox(width: 4), - if (removed > 0) - Text('-$removed', - style: - TextStyle(fontSize: 11, color: Colors.red[400])), - ], + if (added > 0) + Text('+$added', + style: TextStyle( + fontSize: 11, color: Colors.green[400])), + if (added > 0 && removed > 0) const SizedBox(width: 4), + if (removed > 0) + Text('-$removed', + style: + TextStyle(fontSize: 11, color: Colors.red[400])), + ], + ), ), ); }).toList(), @@ -682,6 +983,145 @@ class _DiffSummaryView extends StatelessWidget { style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); } } + + void _showFileContents(BuildContext context, String filePath) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (ctx) => DraggableScrollableSheet( + initialChildSize: 0.8, + minChildSize: 0.3, + maxChildSize: 0.95, + expand: false, + builder: (ctx, scrollController) => _FileContentsSheet( + connId: connId!, + projectId: projectId!, + filePath: filePath, + scrollController: scrollController, + ), + ), + ); + } +} + +class _FileContentsSheet extends StatefulWidget { + final String connId; + final String projectId; + final String filePath; + final ScrollController scrollController; + + const _FileContentsSheet({ + required this.connId, + required this.projectId, + required this.filePath, + required this.scrollController, + }); + + @override + State<_FileContentsSheet> createState() => _FileContentsSheetState(); +} + +class _FileContentsSheetState extends State<_FileContentsSheet> { + String? _contents; + String? _error; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadContents(); + } + + Future _loadContents() async { + try { + final contents = await state_ffi.gitFileContents( + connId: widget.connId, + projectId: widget.projectId, + filePath: widget.filePath, + mode: 'working_tree', + ); + if (mounted) { + setState(() { + _contents = contents; + _loading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = e.toString(); + _loading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 32, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[600], + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + const Icon(Icons.description, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.filePath, + style: const TextStyle( + fontSize: 13, + fontFamily: 'JetBrainsMono', + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const Divider(), + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + _error!, + style: TextStyle(color: Colors.red[400]), + ), + ), + ) + : SingleChildScrollView( + controller: widget.scrollController, + scrollDirection: Axis.horizontal, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + _contents ?? '', + style: const TextStyle( + fontSize: 12, + fontFamily: 'JetBrainsMono', + ), + ), + ), + ), + ), + ), + ], + ); + } } class _BranchesView extends StatelessWidget { From 70d13ee5cde2510275e238d3090c7afed2aaec02 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Thu, 16 Apr 2026 13:37:35 +0200 Subject: [PATCH 24/37] fix(ui): make find_word_boundaries work with byte offsets for UTF-8 correctness Rewrite to operate on byte offsets with char boundary checks instead of collecting into a Vec, which gave wrong results for multi-byte characters. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/okena-ui/src/text_utils.rs | 61 +++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/crates/okena-ui/src/text_utils.rs b/crates/okena-ui/src/text_utils.rs index 4e0061b3..aaa10b39 100644 --- a/crates/okena-ui/src/text_utils.rs +++ b/crates/okena-ui/src/text_utils.rs @@ -5,28 +5,59 @@ pub fn is_word_char(c: char) -> bool { c.is_alphanumeric() || c == '_' } -/// Find the word boundaries (start, end) around a given column position. -pub fn find_word_boundaries(text: &str, col: usize) -> (usize, usize) { - let chars: Vec = text.chars().collect(); - if chars.is_empty() { +/// Find the word boundaries (start, end) around a given byte offset. +/// +/// Both `byte_col` and the returned `(start, end)` are **byte offsets** into `text`, +/// guaranteed to land on UTF-8 char boundaries. +pub fn find_word_boundaries(text: &str, byte_col: usize) -> (usize, usize) { + if text.is_empty() { return (0, 0); } - let col = col.min(chars.len().saturating_sub(1)); - // Scan backwards for start + // Clamp to a valid char boundary at or before byte_col + let byte_col = byte_col.min(text.len()); + let col = if text.is_char_boundary(byte_col) { + byte_col + } else { + // Walk backwards to find a valid char boundary + let mut b = byte_col; + while b > 0 && !text.is_char_boundary(b) { + b -= 1; + } + b + }; + + // Get the char at `col` (if col == text.len(), there is no char) + let cur_char = text[col..].chars().next(); + let on_word = cur_char.map_or(false, |c| is_word_char(c)); + + // Scan backwards for start (byte offset) let mut start = col; - while start > 0 && is_word_char(chars[start - 1]) { - start -= 1; - } - // If cursor is on a non-word char, don't extend backwards - if !is_word_char(chars[col]) { - start = col; + if on_word { + while start > 0 { + // Find the previous char boundary + let mut prev = start - 1; + while prev > 0 && !text.is_char_boundary(prev) { + prev -= 1; + } + let prev_char = text[prev..].chars().next().unwrap(); + if is_word_char(prev_char) { + start = prev; + } else { + break; + } + } } - // Scan forwards for end + // Scan forwards for end (byte offset) let mut end = col; - while end < chars.len() && is_word_char(chars[end]) { - end += 1; + while end < text.len() { + let next_char = text[end..].chars().next().unwrap(); + if is_word_char(next_char) { + end += next_char.len_utf8(); + } else { + break; + } } (start, end) From ba7cc2f9988acccb13adc0fe4c81e00d935f4b34 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Thu, 16 Apr 2026 13:37:41 +0200 Subject: [PATCH 25/37] fix(terminal): deregister inactive tab panes and add tab-aware navigation Deregister pane map entries for inactive tabs so stale bounds don't interfere with spatial navigation. Add try_switch_tab() so Left/Right keys cycle tabs before falling through to cross-pane navigation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/layout/tabs/mod.rs | 12 ++++ .../src/layout/terminal_pane/navigation.rs | 66 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/crates/okena-views-terminal/src/layout/tabs/mod.rs b/crates/okena-views-terminal/src/layout/tabs/mod.rs index 1f48884d..6f9aa069 100644 --- a/crates/okena-views-terminal/src/layout/tabs/mod.rs +++ b/crates/okena-views-terminal/src/layout/tabs/mod.rs @@ -261,6 +261,18 @@ impl LayoutContainer { self.deregister_child_resize_viewers_except(&visible_paths, cx); self.child_containers.retain(|path, _| valid_paths.contains(path)); + // Deregister pane map entries for inactive tabs so stale entries + // don't interfere with spatial navigation + let mut path = self.layout_path.clone(); + let base_len = path.len(); + for i in 0..num_children { + if i != active_tab { + path.truncate(base_len); + path.push(i); + crate::layout::navigation::deregister_pane_bounds(&self.project_id, &path); + } + } + let container_bounds_ref = self.container_bounds_ref.clone(); v_flex() diff --git a/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs b/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs index 8d3e6691..ae0d5b86 100644 --- a/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs +++ b/crates/okena-views-terminal/src/layout/terminal_pane/navigation.rs @@ -3,17 +3,79 @@ use crate::ActionDispatch; use okena_terminal::input::{KeyEvent, KeyModifiers, key_to_bytes}; use crate::layout::navigation::{get_pane_map, PaneBounds, NavigationDirection}; +use okena_workspace::state::LayoutNode; use gpui::*; use super::TerminalPane; impl TerminalPane { + /// Try to switch to an adjacent tab within a Tabs node. + /// Returns true if a tab switch happened, false if at edge or not in a tab group. + fn try_switch_tab(&mut self, next: bool, cx: &mut Context) -> bool { + if self.layout_path.is_empty() { + return false; + } + + let parent_path = &self.layout_path[..self.layout_path.len() - 1]; + let current_tab_index = self.layout_path[self.layout_path.len() - 1]; + + let tab_count = { + let ws = self.workspace.read(cx); + ws.project(&self.project_id).and_then(|p| { + p.layout.as_ref().and_then(|layout| { + layout.get_at_path(parent_path).and_then(|node| match node { + LayoutNode::Tabs { children, .. } => Some(children.len()), + _ => None, + }) + }) + }) + }; + + let num_tabs = match tab_count.filter(|&n| n > 1) { + Some(n) => n, + None => return false, + }; + + let at_edge = if next { + current_tab_index == num_tabs - 1 + } else { + current_tab_index == 0 + }; + + if at_edge { + return false; + } + + let new_tab = if next { current_tab_index + 1 } else { current_tab_index - 1 }; + let project_id = self.project_id.clone(); + let mut new_layout_path = parent_path.to_vec(); + new_layout_path.push(new_tab); + + let workspace = self.workspace.clone(); + self.focus_manager.update(cx, |fm, cx| { + workspace.update(cx, |ws, cx| { + ws.set_active_tab(&project_id, &new_layout_path[..new_layout_path.len() - 1], new_tab, cx); + ws.set_focused_terminal(fm, project_id, new_layout_path, cx); + }); + cx.notify(); + }); + true + } + pub(super) fn handle_navigation( &mut self, direction: NavigationDirection, window: &mut Window, cx: &mut Context, ) { + // Left/Right: try switching tabs first, fall through to spatial nav at edges + if matches!(direction, NavigationDirection::Left | NavigationDirection::Right) { + let next = matches!(direction, NavigationDirection::Right); + if self.try_switch_tab(next, cx) { + return; + } + } + let pane_map = get_pane_map(self.window_id); let source = match pane_map.find_pane(&self.project_id, &self.layout_path) { @@ -32,6 +94,10 @@ impl TerminalPane { window: &mut Window, cx: &mut Context, ) { + if self.try_switch_tab(next, cx) { + return; + } + let pane_map = get_pane_map(self.window_id); let source = match pane_map.find_pane(&self.project_id, &self.layout_path) { From 95be8963c4d26a95966a07be8458cc36e498a058 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 8 Jun 2026 16:35:34 +0200 Subject: [PATCH 26/37] fix(rebase): pass window_id to deregister_pane_bounds in tabs container The tab-pane deregistration added in feat/ios predates main's multi-window support, which gave deregister_pane_bounds a leading WindowId parameter. Adapt the inactive-tab cleanup callsite to main's signature so the rebased branch compiles. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/okena-views-terminal/src/layout/tabs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/okena-views-terminal/src/layout/tabs/mod.rs b/crates/okena-views-terminal/src/layout/tabs/mod.rs index 6f9aa069..b7bc0cf6 100644 --- a/crates/okena-views-terminal/src/layout/tabs/mod.rs +++ b/crates/okena-views-terminal/src/layout/tabs/mod.rs @@ -269,7 +269,7 @@ impl LayoutContainer { if i != active_tab { path.truncate(base_len); path.push(i); - crate::layout::navigation::deregister_pane_bounds(&self.project_id, &path); + crate::layout::navigation::deregister_pane_bounds(self.window_id, &self.project_id, &path); } } From 97570791b13eb5d1e9287f1a6a1f555bc66f2e89 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 8 Jun 2026 16:38:11 +0200 Subject: [PATCH 27/37] docs(mobile): add React Native migration plan (uniffi + native Skia rendering) Plan to replace the Flutter UI with React Native while keeping okena-core and alacritty emulation. Primary path: uniffi-bindgen-react-native (JSI) + react-native-skia for native terminal rendering; xterm.js explicitly rejected. Documents the FFI seam, the hot-path packed-buffer bridge, a spike-gated phased plan, and the "drop Rust" fallback (reuse the web TS protocol client, still native rendering). Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/RN_MIGRATION.md | 181 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 mobile/RN_MIGRATION.md diff --git a/mobile/RN_MIGRATION.md b/mobile/RN_MIGRATION.md new file mode 100644 index 00000000..aa928113 --- /dev/null +++ b/mobile/RN_MIGRATION.md @@ -0,0 +1,181 @@ +# Mobile app → React Native migration plan + +Status: **proposal / spike-gated.** This branch (`feat/mobile-rn`) is PR #17 (`feat/ios`) +rebased onto current `main`. It still ships the Flutter app; this document is the plan to +replace the Flutter UI layer with React Native while **keeping the Rust core**. + +Primary strategy: **RN + Rust core via uniffi**, with **native GPU rendering** of the +terminal (no `xterm.js`). Dropping Rust entirely is a documented *fallback*, not the goal. + +--- + +## 1. Why this is architecturally clean + +All real logic already lives below an FFI-agnostic seam. The mobile app is a *thin* terminal +client over the desktop remote server (`/v1/pair`, `/v1/state`, `/v1/actions`, WS `/v1/stream`). + +``` +crates/okena-core (feature = "client") ← REUSE AS-IS. GPUI-free, framework-agnostic. + RemoteClient protocol, TLS + cert pinning (TOFU), WS, + ConnectionHandler trait reconnect, state diffing. ~1900 LOC. + ▲ +mobile/native (okena_mobile_native) ← REWRITE the binding layer only. + ConnectionManager / MobileConnectionHandler The *logic* here is plain Rust and is reusable; + TerminalHolder (alacritty_terminal::Term) only the `flutter_rust_bridge` attributes are + api/{connection,terminal,state}.rs Flutter-specific. ~60 exported FFI fns. + ▲ +Flutter UI (mobile/lib, ~2700+ LOC Dart) ← REPLACE with React Native. +``` + +The seam is `RemoteClient` (`crates/okena-core/src/client/connection.rs`). +`MobileConnectionHandler` is just one `impl ConnectionHandler`; the desktop has its own. RN +gets a third consumer of the *same* core — identical terminal emulation as desktop, because +both run `alacritty_terminal`. + +**Consequence:** ~80% of the hard work (wire protocol, TLS, reconnect, ANSI parsing) is not +touched. We swap the binding generator (`flutter_rust_bridge` → `uniffi`) and rewrite the UI. + +--- + +## 2. Target architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ React Native app (TypeScript) │ +│ │ +│ Screens / navigation / state (zustand or context) │ +│ KeyToolbar, ProjectDrawer, LayoutRenderer (RN components) │ +│ TerminalView → react-native-skia (native GPU) │ +│ │ reads packed cell buffer, paints 3-pass │ +│ ▼ │ +│ TS bindings (generated by uniffi-bindgen-react-native, JSI) │ +└────────┼─────────────────────────────────────────────────────┘ + │ JSI (in-process, sync + async) +┌────────▼─────────────────────────────────────────────────────┐ +│ crates/okena-mobile-ffi (uniffi-annotated, was mobile/native)│ +│ reuses ConnectionManager + TerminalHolder logic verbatim │ +│ ▲ │ +│ crates/okena-core (client) ← unchanged │ +└───────────────────────────────────────────────────────────────┘ +``` + +Three decisions define the project: + +### Decision A — binding tool: `uniffi-bindgen-react-native` +Mozilla `uniffi` describes the Rust API (via `#[uniffi::export]` proc-macros — no UDL needed), +and **`uniffi-bindgen-react-native` (ubrn)** generates a JSI-based TurboModule package consumable +from RN (new architecture). It also scaffolds the cargo cross-compile + codegen pipeline for both +Android (NDK) and iOS (xcframework), which is the equivalent of what `cargokit` does for Flutter today. + +- Async Rust fns (`pair`, `send_text`, `git_diff`, the `*_action` calls — ~40 of the 60) map to + JS Promises. Sync fns (`get_visible_cells`, `get_cursor`, `is_dirty`, `connection_status`) map + to synchronous JSI host functions — important for the render hot path. +- Verify the current `ubrn` version and RN new-architecture requirement during the Phase-0 spike. + +### Decision B — rendering: `react-native-skia` (native, not xterm.js) +The user requirement is **native rendering, no `xterm.js`**. `react-native-skia` exposes the same +Skia GPU canvas that Flutter's `CustomPainter` already uses — so we port `terminal_painter.dart`'s +3-pass algorithm (background rects → glyph run → cursor) almost 1:1, cross-platform, no DOM/WebView. +- Monospace metrics + glyph runs via Skia `Paragraph`/`Font`; ship the existing JetBrainsMono. +- Selection + cursor as overlay paints, identical to the Flutter widget. +- Escalation path if Skia text throughput is insufficient: a true **Fabric native component** + (Android `View`/Canvas or Skia; iOS `UIView` + CoreText/Metal) fed the same cell buffer. More + per-platform code, kept in reserve. + +### Decision C — the hot-path data bridge (the only real perf risk) +Today Flutter polls `get_visible_cells()` at ~30fps and gets a `Vec` (one struct per +cell). Marshaling thousands of records per frame across JSI as objects is the thing that can be +slow. Mitigation, in order: +1. **Packed binary buffer.** Add an FFI fn returning the visible grid as a compact `Vec` + (per cell: codepoint `u32`, fg `u32`, bg `u32`, flags `u8`), exposed as an `ArrayBuffer`. + Skia reads the typed array directly. This is the single most important perf lever. +2. **Drive repaint from `requestAnimationFrame` gated on `is_dirty()`**, not a fixed 33ms timer. +3. If still hot, move the paint loop fully native (Decision B escalation) and pass only the buffer + pointer. +Keep `get_visible_cells` (records) too for non-hot callers; add the packed variant for the canvas. + +--- + +## 3. Phased plan (de-risk first) + +### Phase 0 — Spikes (validate the two unknowns before committing) — ~1 week +- **S1 (toolchain):** `ubrn` hello-world — call `init_app()` + `connect()` + `connection_status()` + from a bare RN app on a real Android device *and* iOS sim. Proves NDK + xcframework build wiring. +- **S2 (rendering):** render a static 80×40 colored cell grid in `react-native-skia` and drive it + at 60fps from a JS timer. Proves Skia text throughput on-device. +- **Gate:** S1 fails → reconsider the *fallback* (§5). S2 fails → commit to the native-component + rendering path (Decision B escalation) up front. + +### Phase 1 — `okena-mobile-ffi` crate — ~1–1.5 weeks +- New crate (or in-place rework of `mobile/native`): strip `flutter_rust_bridge` attrs, add + `#[uniffi::export]`. The `ConnectionManager`, `MobileConnectionHandler`, `TerminalHolder` bodies + carry over unchanged — they are plain Rust. +- Port all ~60 fns (`api/{connection,terminal,state}.rs`). Add the **packed cell buffer** fn. +- Resolve the open `TODO(mobile-tls)`: expose `tls: bool` + fingerprint to the binding (the core + already supports it; Flutter just hardcodes `false`). RN should ship TLS-on from day one. +- Generate TS bindings; integrate cargo build into the RN Android Gradle + iOS pods. + +### Phase 2 — RN app shell — ~1–1.5 weeks +- Bare RN (or Expo + prebuild/dev-client — needs native modules either way). New-architecture on. +- Port `server_list`, `pairing`, `workspace` screens; theme (Catppuccin/iOS dark); navigation. +- State: mirror today's polling providers initially (`ConnectionProvider`, `WorkspaceProvider`), + then optionally move state push from Rust via a uniffi callback interface to drop polling. + +### Phase 3 — terminal rendering (the core deliverable) — ~1.5–2 weeks +- `TerminalView` on `react-native-skia`: 3-pass paint from the packed buffer; cursor + selection. +- Input: hidden `TextInput` for the soft keyboard + a `KeyToolbar` component (ESC/TAB/sticky + CTRL+ALT/arrows) mapping to `send_text` / `send_special_key`. +- Sizing: `onLayout` → cols/rows → `resize_terminal` (200ms debounce, as today). +- Scroll + character/word selection wired to the existing FFI. + +### Phase 4 — feature parity with PR #17 — ~2–3 weeks +`LayoutRenderer` (splits/tabs, portrait auto-vertical), `ProjectDrawer` (add/reorder/color), +git diff + file viewer, services panel, fullscreen, tab/pane management. All FFI already exists. + +### Phase 5 — cutover — ~1 week +CI for both platforms, parity test pass against a live desktop server, then retire `mobile/lib` +(Flutter) and `mobile/native`'s frb layer. + +**Rough total:** ~8–11 focused weeks for full parity; a *usable* connect-pair-render-input demo by +end of Phase 3 (~4–5 weeks). + +--- + +## 4. Reuse vs. rewrite + +| Layer | Today | RN target | Action | +|---|---|---|---| +| Protocol / TLS / WS / state diff | `okena-core` (Rust) | same | **reuse as-is** | +| ANSI emulation | `alacritty_terminal` in `TerminalHolder` | same | **reuse as-is** | +| FFI binding | `flutter_rust_bridge` | `uniffi` + `ubrn` (JSI) | **rewrite (mechanical)** | +| Binding *logic* (ConnectionManager etc.) | `mobile/native` Rust | same Rust | **reuse (strip frb attrs)** | +| Terminal paint | `terminal_painter.dart` (Skia) | `react-native-skia` | **port (~1:1)** | +| Screens / drawer / toolbar / layout | Dart widgets | RN components | **rewrite** | +| State management | `provider` + polling | zustand/context (+ optional push) | **rewrite** | + +--- + +## 5. Risks & the "drop Rust" fallback + +- **`ubrn` maturity / build integration.** The JSI binding generator and dual-platform + cross-compile are the least-proven pieces — hence Phase-0 S1. If it proves too painful to + maintain, the **fallback is to drop Rust on mobile**, *not* to adopt `xterm.js`: + - The web client (`web/src/api/client.ts`, `websocket.ts`) already speaks the identical wire + protocol in pure `fetch`/`WebSocket` TS — **directly reusable in RN**, no DOM. + - Replace `alacritty` with a TS ANSI parser feeding the **same `react-native-skia`** renderer. + Native rendering is preserved; only the emulation moves to JS. (`xterm.js` stays rejected.) + - Cost: a second emulation implementation that can drift from desktop — which is the main + argument *for* keeping Rust. +- **Hot-path perf:** mitigated by the packed buffer (Decision C); escalate to native component. +- **Two FFI generators during transition:** keep `flutter_rust_bridge` building until Phase 5; + `uniffi` lives in a parallel crate so the Flutter app is never broken mid-migration. +- **iOS signing / CocoaPods + Rust xcframework:** PR #17 already set up the iOS Flutter project; + reuse its signing config, but the `ubrn` xcframework wiring is new — covered by S1. + +--- + +## 6. Concrete next step + +Run **Phase 0 spikes (S1 + S2)** on this branch. They are small, independent, and decide between +the primary path (RN + uniffi + Skia) and the fallback (RN + TS protocol + Skia). Everything below +the seam is already in place and building on current `main`. From 03f919dfa589333663bee000b7ab1eb537915d99 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 8 Jun 2026 16:54:17 +0200 Subject: [PATCH 28/37] feat(mobile-ffi): uniffi binding crate for the React Native migration (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New crate `crates/okena-mobile-ffi` re-expresses mobile/native's ~60-function FFI surface via uniffi proc-macros (uniffi 0.29, JSI/ubrn-ready) — no logic duplication: every fn delegates to okena_mobile_native's ConnectionManager, which is reused verbatim (its crate-type gains "lib" so it can be a path dep). - 31 genuinely-async fns exported with #[uniffi::export(async_runtime="tokio")] → JS Promises; sync getters (cells/cursor/scroll/selection/state) stay sync for the render hot path. - Adds get_visible_cells_packed(): the visible grid as a compact little-endian buffer (4B cols/rows header + 13B/cell: codepoint u32, fg u32, bg u32, flags u8) for the RN Skia renderer (RN_MIGRATION.md Decision C). - connect() accepts tls + pinned cert fingerprint at the boundary, but they are a documented no-op pass-through: client-side TLS lives on arch-review-fixes (PR #134), not on this branch's okena-core. Wiring is a follow-up. Verified: cargo check passes for okena-mobile-ffi AND okena_mobile_native (Flutter Rust side intact). Does not touch okena-core, Dart, or frb codegen. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 314 ++++++++- Cargo.toml | 2 +- crates/okena-mobile-ffi/Cargo.toml | 29 + crates/okena-mobile-ffi/src/lib.rs | 998 +++++++++++++++++++++++++++ crates/okena-mobile-ffi/src/types.rs | 241 +++++++ mobile/native/Cargo.toml | 5 +- 6 files changed, 1583 insertions(+), 6 deletions(-) create mode 100644 crates/okena-mobile-ffi/Cargo.toml create mode 100644 crates/okena-mobile-ffi/src/lib.rs create mode 100644 crates/okena-mobile-ffi/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index fbfcae3e..cac4ef51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,6 +282,48 @@ dependencies = [ "zbus", ] +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash 2.1.1", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow 0.7.15", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -317,6 +359,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-compression" version = "0.4.41" @@ -692,6 +747,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -919,6 +983,38 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "cbc" version = "0.1.2" @@ -2270,6 +2366,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -3452,6 +3557,17 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "gpu-allocator" version = "0.28.0" @@ -5803,6 +5919,17 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "okena-mobile-ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "okena-core", + "okena_mobile_native", + "serde_json", + "uniffi", +] + [[package]] name = "okena-remote-client" version = "0.1.0" @@ -6286,7 +6413,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher", + "siphasher 1.0.2", ] [[package]] @@ -7259,7 +7386,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "siphasher", + "siphasher 1.0.2", "toml 0.8.23", "triomphe", ] @@ -7514,6 +7641,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "seahash" version = "4.1.0" @@ -7835,6 +7982,12 @@ dependencies = [ "log", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "siphasher" version = "1.0.2" @@ -7882,6 +8035,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "smol" version = "2.0.2" @@ -8164,7 +8323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", - "siphasher", + "siphasher 1.0.2", ] [[package]] @@ -8343,6 +8502,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -8585,6 +8753,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.23" @@ -9040,6 +9217,126 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml 0.5.11", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "async-compat", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher 0.3.11", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -9081,7 +9378,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher", + "siphasher 1.0.2", "strict-num", "svgtypes", "tiny-skia-path", @@ -9554,6 +9851,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "weezl" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 5ffbd9f3..32c84c57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "mobile/native", "crates/okena-core", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-files", "crates/okena-markdown", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-views-remote", "crates/okena-theme"] +members = [".", "mobile/native", "crates/okena-mobile-ffi", "crates/okena-core", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-files", "crates/okena-markdown", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-views-remote", "crates/okena-theme"] resolver = "2" [package] diff --git a/crates/okena-mobile-ffi/Cargo.toml b/crates/okena-mobile-ffi/Cargo.toml new file mode 100644 index 00000000..59f63859 --- /dev/null +++ b/crates/okena-mobile-ffi/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "okena-mobile-ffi" +version = "0.1.0" +edition = "2024" + +[lib] +# cdylib + staticlib for the eventual ubrn-generated Android (NDK) / iOS +# (xcframework) artifacts; `lib` so it can also be linked as a normal Rust +# library (e.g. for `cargo test` and downstream tooling). +crate-type = ["cdylib", "staticlib", "lib"] + +[dependencies] +# Reuse the existing mobile binding logic verbatim — ConnectionManager, +# MobileConnectionHandler, TerminalHolder and the api data structs all live +# here. We do NOT duplicate any of it; this crate only re-expresses the FFI +# surface via uniffi instead of flutter_rust_bridge. +okena_mobile_native = { path = "../../mobile/native" } + +# Shared protocol / TLS / WS / terminal-emulation core. Needed for the request +# enums (ActionRequest, WsClientMessage, SpecialKey, SplitDirection, DiffMode, +# FolderColor) and the DARK_THEME used when extracting cells. +okena-core = { path = "../okena-core", features = ["client"] } + +# uniffi in proc-macro mode (no UDL). `tokio` enables async export via +# `#[uniffi::export(async_runtime = "tokio")]`. +uniffi = { version = "0.29", features = ["tokio"] } + +serde_json = "1.0" +anyhow = "1.0" diff --git a/crates/okena-mobile-ffi/src/lib.rs b/crates/okena-mobile-ffi/src/lib.rs new file mode 100644 index 00000000..af6f518c --- /dev/null +++ b/crates/okena-mobile-ffi/src/lib.rs @@ -0,0 +1,998 @@ +//! uniffi FFI surface for the React Native mobile app. +//! +//! This crate is the RN-facing equivalent of `mobile/native`'s +//! `flutter_rust_bridge` `api/` layer. It re-expresses the same ~60 functions +//! via uniffi proc-macros (`#[uniffi::export]`, `#[derive(uniffi::Record)]`, +//! `#[derive(uniffi::Enum)]`) so `uniffi-bindgen-react-native` (ubrn) can emit +//! a JSI TurboModule. +//! +//! It does NOT reimplement any logic: every function delegates to +//! `okena_mobile_native::client::manager::ConnectionManager`, exactly like the +//! Flutter `api/` functions do. The `ConnectionManager`, `MobileConnectionHandler` +//! and `TerminalHolder` are reused verbatim from `mobile/native`. +//! +//! ## Async strategy +//! The frb api split sync vs. async based on whether the body actually awaits: +//! - Functions whose bodies are synchronous (the `with_terminal` / `get_state` +//! accessors, `send_text` / `send_special_key`, which only enqueue a WS +//! message via `send_ws_message`) are exported as plain sync uniffi fns — +//! important for the render hot path. +//! - Functions that genuinely `.await` reqwest (`*_terminal` actions, git, +//! services, project/layout mutations, `read_content`) are exported as +//! `async fn` with `#[uniffi::export(async_runtime = "tokio")]`, which ubrn +//! maps to JS Promises. + +#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))] + +mod types; + +use okena_core::api::ActionRequest; +use okena_core::client::{collect_state_terminal_ids, WsClientMessage}; +use okena_core::keys::SpecialKey; +use okena_core::theme::DARK_THEME; +use okena_core::types::{DiffMode, SplitDirection}; + +use okena_mobile_native::client::manager::ConnectionManager; + +pub use types::{ + CellData, ConnectionStatus, CursorShape, CursorState, FolderInfo, FullscreenInfo, ProjectInfo, + ScrollInfo, SelectionBounds, ServiceInfo, +}; + +uniffi::setup_scaffolding!(); + +// ── Connection lifecycle ──────────────────────────────────────────── + +/// Initialize the app. Must be called once at startup before any other fn. +#[uniffi::export] +pub fn init_app() { + ConnectionManager::init(); +} + +/// Connect to an Okena remote server. Returns a connection ID. +/// +/// `tls` and `pinned_cert_fingerprint` describe the desired transport security +/// for this connection (RN ships TLS-capable from day one). They are accepted +/// at the binding boundary so the RN UI and persisted server config can carry +/// the TLS flag. +/// +/// `tls` is forwarded to `ConnectionManager::add_connection`. The pinned cert, +/// however, is established via TOFU during the handshake (the manager has no +/// param to pre-seed a fingerprint — it records it from the `TlsUpgraded` / +/// pairing events), so `pinned_cert_fingerprint` is not forwarded yet. +#[uniffi::export] +pub fn connect( + host: String, + port: u16, + saved_token: Option, + tls: bool, + pinned_cert_fingerprint: Option, +) -> String { + // `pinned_cert_fingerprint` is intentionally not forwarded yet — the + // manager pins via TOFU events rather than an up-front fingerprint. Touch it + // so the unused-var lint stays quiet and the intent is explicit. + let _ = pinned_cert_fingerprint; + let mgr = ConnectionManager::get(); + let conn_id = mgr.add_connection(&host, port, saved_token, tls); + mgr.connect(&conn_id); + conn_id +} + +/// Get the current auth token for a connection (if paired). +#[uniffi::export] +pub fn get_token(conn_id: String) -> Option { + ConnectionManager::get().get_token(&conn_id) +} + +/// Pair with the server using a pairing code. (Body is synchronous: it only +/// kicks off the manager's pairing task.) +#[uniffi::export] +pub fn pair(conn_id: String, code: String) { + ConnectionManager::get().pair(&conn_id, &code); +} + +/// Disconnect from a server. +#[uniffi::export] +pub fn disconnect(conn_id: String) { + ConnectionManager::get().disconnect(&conn_id); +} + +/// Get current connection status. +#[uniffi::export] +pub fn connection_status(conn_id: String) -> ConnectionStatus { + // Delegate to the native api fn, which already maps okena-core's status + // (collapsing `Reconnecting` into `Connecting`) into its own enum; we then + // convert that into our uniffi enum. + okena_mobile_native::api::connection::connection_status(conn_id).into() +} + +/// Seconds since last WS activity (terminal output). Large value if missing. +#[uniffi::export] +pub fn seconds_since_activity(conn_id: String) -> f64 { + ConnectionManager::get().seconds_since_activity(&conn_id) +} + +// ── Terminal rendering / input ────────────────────────────────────── + +/// Get the visible terminal cells for rendering (records form). +/// +/// Kept for non-hot-path callers; the render loop should prefer +/// [`get_visible_cells_packed`]. +#[uniffi::export] +pub fn get_visible_cells(conn_id: String, terminal_id: String) -> Vec { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder + .get_visible_cells(&DARK_THEME) + .into_iter() + .map(Into::into) + .collect() + }) + .unwrap_or_default() +} + +/// Get the visible terminal grid as a packed little-endian byte buffer. +/// +/// This is the hot-path render bridge (migration plan Decision C): instead of +/// marshaling thousands of `CellData` records per frame across JSI, the RN +/// Skia canvas reads this compact buffer directly as an `ArrayBuffer`. +/// +/// ## Format (all multi-byte values little-endian) +/// ```text +/// Header (4 bytes): +/// cols : u16 LE +/// rows : u16 LE +/// Then cols*rows cells, row-major, 13 bytes each: +/// codepoint : u32 LE Unicode scalar of the cell's primary char +/// (0x20 / space for empty or wide-char-spacer cells) +/// fg : u32 LE ARGB +/// bg : u32 LE ARGB +/// flags : u8 bold(1)|italic(2)|underline(4)|strikethrough(8)| +/// inverse(16)|dim(32) +/// ``` +/// Total length = 4 + cols*rows*13 bytes. Built from the same `CellData` list +/// `get_visible_cells` returns. If the terminal is missing, returns a 0x0 +/// header (`[0, 0, 0, 0]`). +#[uniffi::export] +pub fn get_visible_cells_packed(conn_id: String, terminal_id: String) -> Vec { + let mgr = ConnectionManager::get(); + let cells = mgr + .with_terminal(&conn_id, &terminal_id, |holder| { + holder.get_visible_cells(&DARK_THEME) + }) + .unwrap_or_default(); + + // Determine grid dimensions from the live cursor/grid via scroll_info would + // require another lock; instead derive cols from the row width recorded by + // the holder. The cell list is exactly cols*rows row-major, so we recover + // dimensions from the holder's reported scroll info (visible_lines = rows) + // and divide. To avoid a second terminal access we read both in one borrow. + let (cols, rows) = mgr + .with_terminal(&conn_id, &terminal_id, |holder| { + let (_total, visible, _offset) = holder.scroll_info(); + let rows = visible as u16; + let cols = if visible > 0 { + (cells.len() / visible) as u16 + } else { + 0 + }; + (cols, rows) + }) + .unwrap_or((0, 0)); + + let mut buf = Vec::with_capacity(4 + cells.len() * 13); + buf.extend_from_slice(&cols.to_le_bytes()); + buf.extend_from_slice(&rows.to_le_bytes()); + for cell in &cells { + // Primary scalar; empty (wide-char spacer) or space → 0x20. + let codepoint: u32 = cell.character.chars().next().map(|c| c as u32).unwrap_or(0x20); + let codepoint = if codepoint == 0 { 0x20 } else { codepoint }; + buf.extend_from_slice(&codepoint.to_le_bytes()); + buf.extend_from_slice(&cell.fg.to_le_bytes()); + buf.extend_from_slice(&cell.bg.to_le_bytes()); + buf.push(cell.flags); + } + buf +} + +/// Get the current cursor state. +#[uniffi::export] +pub fn get_cursor(conn_id: String, terminal_id: String) -> CursorState { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.get_cursor().into()) + .unwrap_or(CursorState { + col: 0, + row: 0, + shape: CursorShape::Block, + visible: true, + }) +} + +/// Send text input to a terminal. Synchronous: only enqueues a WS message. +#[uniffi::export] +pub fn send_text(conn_id: String, terminal_id: String, text: String) { + ConnectionManager::get().send_ws_message(&conn_id, WsClientMessage::SendText { terminal_id, text }); +} + +/// Resize a terminal (local grid + WS resize message). +#[uniffi::export] +pub fn resize_terminal(conn_id: String, terminal_id: String, cols: u16, rows: u16) { + ConnectionManager::get().resize_terminal(&conn_id, &terminal_id, cols, rows); +} + +/// Resize only the local alacritty grid (no WS message). Used when adapting to +/// the server's terminal size. +#[uniffi::export] +pub fn resize_local(conn_id: String, terminal_id: String, cols: u16, rows: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.resize(cols, rows); + }); +} + +// ── Scrolling ─────────────────────────────────────────────────────── + +/// Scroll the terminal display (positive = up, negative = down). +#[uniffi::export] +pub fn scroll(conn_id: String, terminal_id: String, delta: i32) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.scroll(delta); + }); +} + +/// Get scroll info: total lines, visible lines, display offset. +#[uniffi::export] +pub fn get_scroll_info(conn_id: String, terminal_id: String) -> ScrollInfo { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + let (total, visible, offset) = holder.scroll_info(); + ScrollInfo { + total_lines: total as u32, + visible_lines: visible as u32, + display_offset: offset as u32, + } + }) + .unwrap_or(ScrollInfo { + total_lines: 0, + visible_lines: 0, + display_offset: 0, + }) +} + +// ── Selection ─────────────────────────────────────────────────────── + +/// Start a character-level selection at col/row. +#[uniffi::export] +pub fn start_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.start_selection(col as usize, row as usize); + }); +} + +/// Start a word (semantic) selection at col/row. +#[uniffi::export] +pub fn start_word_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.start_word_selection(col as usize, row as usize); + }); +} + +/// Extend the current selection to col/row. +#[uniffi::export] +pub fn update_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.update_selection(col as usize, row as usize); + }); +} + +/// Clear the current selection. +#[uniffi::export] +pub fn clear_selection(conn_id: String, terminal_id: String) { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder.clear_selection(); + }); +} + +/// Get the selected text, if any. +#[uniffi::export] +pub fn get_selected_text(conn_id: String, terminal_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.get_selected_text()) + .flatten() +} + +/// Get selection bounds for rendering. +#[uniffi::export] +pub fn get_selection_bounds(conn_id: String, terminal_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| { + holder + .selection_bounds() + .map(|((sc, sr), (ec, er))| SelectionBounds { + start_col: sc as u16, + start_row: sr, + end_col: ec as u16, + end_row: er, + }) + }) + .flatten() +} + +// ── State queries ─────────────────────────────────────────────────── + +/// Get all projects from the cached remote state. +#[uniffi::export] +pub fn get_projects(conn_id: String) -> Vec { + okena_mobile_native::api::state::get_projects(conn_id) + .into_iter() + .map(Into::into) + .collect() +} + +/// Get the focused project ID from the cached remote state. +#[uniffi::export] +pub fn get_focused_project_id(conn_id: String) -> Option { + okena_mobile_native::api::state::get_focused_project_id(conn_id) +} + +/// Get folders from the cached remote state. +#[uniffi::export] +pub fn get_folders(conn_id: String) -> Vec { + okena_mobile_native::api::state::get_folders(conn_id) + .into_iter() + .map(Into::into) + .collect() +} + +/// Get the project order from the cached remote state. +#[uniffi::export] +pub fn get_project_order(conn_id: String) -> Vec { + okena_mobile_native::api::state::get_project_order(conn_id) +} + +/// Get fullscreen terminal info. +#[uniffi::export] +pub fn get_fullscreen_terminal(conn_id: String) -> Option { + okena_mobile_native::api::state::get_fullscreen_terminal(conn_id).map(Into::into) +} + +/// Get layout JSON for a project. +#[uniffi::export] +pub fn get_project_layout_json(conn_id: String, project_id: String) -> Option { + let mgr = ConnectionManager::get(); + let state = mgr.get_state(&conn_id)?; + let project = state.projects.iter().find(|p| p.id == project_id)?; + let layout = project.layout.as_ref()?; + serde_json::to_string(layout).ok() +} + +/// Check if a terminal has unprocessed output (dirty flag). +#[uniffi::export] +pub fn is_dirty(conn_id: String, terminal_id: String) -> bool { + let mgr = ConnectionManager::get(); + mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.is_dirty()) + .unwrap_or(false) +} + +/// Get all terminal IDs from the cached remote state (flat list). +#[uniffi::export] +pub fn get_all_terminal_ids(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + match mgr.get_state(&conn_id) { + Some(state) => collect_state_terminal_ids(&state), + None => Vec::new(), + } +} + +/// Send a special key (e.g. `"Enter"`, `"CtrlC"`, `"ArrowUp"`) to a terminal. +/// +/// The body is synchronous (it only enqueues a WS message), so this is a sync +/// uniffi fn. The error path mirrors the frb version: an unknown key name +/// yields an error. +#[uniffi::export] +pub fn send_special_key( + conn_id: String, + terminal_id: String, + key: String, +) -> Result<(), MobileFfiError> { + let special_key: SpecialKey = serde_json::from_value(serde_json::Value::String(key.clone())) + .map_err(|_| MobileFfiError::Action { + message: format!("Unknown special key: {key}"), + })?; + let text = String::from_utf8_lossy(special_key.to_bytes()).to_string(); + ConnectionManager::get().send_ws_message(&conn_id, WsClientMessage::SendText { terminal_id, text }); + Ok(()) +} + +// ── Error type for async/fallible exports ─────────────────────────── + +/// Error returned by fallible FFI functions. uniffi maps this to a thrown +/// error / rejected Promise on the RN side. +#[derive(Debug, uniffi::Error)] +pub enum MobileFfiError { + /// A remote action (HTTP POST /v1/actions) or input validation failed. + Action { message: String }, +} + +impl std::fmt::Display for MobileFfiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MobileFfiError::Action { message } => write!(f, "{message}"), + } + } +} + +impl std::error::Error for MobileFfiError {} + +impl From for MobileFfiError { + fn from(e: anyhow::Error) -> Self { + MobileFfiError::Action { + message: e.to_string(), + } + } +} + +// ── Terminal actions (async — await reqwest) ──────────────────────── + +/// Create a new terminal in the given project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn create_terminal(conn_id: String, project_id: String) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::CreateTerminal { project_id }) + .await?; + Ok(()) +} + +/// Close a terminal in the given project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn close_terminal( + conn_id: String, + project_id: String, + terminal_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::CloseTerminal { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Close multiple terminals in a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn close_terminals( + conn_id: String, + project_id: String, + terminal_ids: Vec, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::CloseTerminals { + project_id, + terminal_ids, + }, + ) + .await?; + Ok(()) +} + +/// Rename a terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn rename_terminal( + conn_id: String, + project_id: String, + terminal_id: String, + name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::RenameTerminal { + project_id, + terminal_id, + name, + }, + ) + .await?; + Ok(()) +} + +/// Focus a terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn focus_terminal( + conn_id: String, + project_id: String, + terminal_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::FocusTerminal { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Toggle minimized state of a terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn toggle_minimized( + conn_id: String, + project_id: String, + terminal_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::ToggleMinimized { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Set/clear fullscreen terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_fullscreen( + conn_id: String, + project_id: String, + terminal_id: Option, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetFullscreen { + project_id, + terminal_id, + }, + ) + .await?; + Ok(()) +} + +/// Split a terminal pane. `direction` is "vertical" or "horizontal". +#[uniffi::export(async_runtime = "tokio")] +pub async fn split_terminal( + conn_id: String, + project_id: String, + path: Vec, + direction: String, +) -> Result<(), MobileFfiError> { + let dir = match direction.as_str() { + "vertical" => SplitDirection::Vertical, + _ => SplitDirection::Horizontal, + }; + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SplitTerminal { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + direction: dir, + }, + ) + .await?; + Ok(()) +} + +/// Run a command in a terminal (presses Enter automatically). +#[uniffi::export(async_runtime = "tokio")] +pub async fn run_command( + conn_id: String, + terminal_id: String, + command: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::RunCommand { + terminal_id, + command, + }, + ) + .await?; + Ok(()) +} + +/// Read terminal content as text. +#[uniffi::export(async_runtime = "tokio")] +pub async fn read_content(conn_id: String, terminal_id: String) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::ReadContent { terminal_id }) + .await?) +} + +// ── Git actions (async) ───────────────────────────────────────────── + +/// Get detailed git status for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_status(conn_id: String, project_id: String) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::GitStatus { project_id }) + .await?) +} + +/// Get git diff summary for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_diff_summary( + conn_id: String, + project_id: String, +) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::GitDiffSummary { project_id }) + .await?) +} + +/// Get git diff for a project. `mode` is "working_tree" or "staged". +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_diff( + conn_id: String, + project_id: String, + mode: String, +) -> Result { + let diff_mode = match mode.as_str() { + "staged" => DiffMode::Staged, + _ => DiffMode::WorkingTree, + }; + Ok(ConnectionManager::get() + .send_action_with_response( + &conn_id, + ActionRequest::GitDiff { + project_id, + mode: diff_mode, + ignore_whitespace: false, + }, + ) + .await?) +} + +/// Get git branches for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_branches(conn_id: String, project_id: String) -> Result { + Ok(ConnectionManager::get() + .send_action_with_response(&conn_id, ActionRequest::GitBranches { project_id }) + .await?) +} + +/// Get file contents from git (working tree or staged). +#[uniffi::export(async_runtime = "tokio")] +pub async fn git_file_contents( + conn_id: String, + project_id: String, + file_path: String, + mode: String, +) -> Result { + let diff_mode = match mode.as_str() { + "staged" => DiffMode::Staged, + _ => DiffMode::WorkingTree, + }; + Ok(ConnectionManager::get() + .send_action_with_response( + &conn_id, + ActionRequest::GitFileContents { + project_id, + file_path, + mode: diff_mode, + }, + ) + .await?) +} + +// ── Service actions (async) ───────────────────────────────────────── + +/// Start a service. +#[uniffi::export(async_runtime = "tokio")] +pub async fn start_service( + conn_id: String, + project_id: String, + service_name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::StartService { + project_id, + service_name, + }, + ) + .await?; + Ok(()) +} + +/// Stop a service. +#[uniffi::export(async_runtime = "tokio")] +pub async fn stop_service( + conn_id: String, + project_id: String, + service_name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::StopService { + project_id, + service_name, + }, + ) + .await?; + Ok(()) +} + +/// Restart a service. +#[uniffi::export(async_runtime = "tokio")] +pub async fn restart_service( + conn_id: String, + project_id: String, + service_name: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::RestartService { + project_id, + service_name, + }, + ) + .await?; + Ok(()) +} + +/// Start all services in a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn start_all_services( + conn_id: String, + project_id: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::StartAllServices { project_id }) + .await?; + Ok(()) +} + +/// Stop all services in a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn stop_all_services(conn_id: String, project_id: String) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::StopAllServices { project_id }) + .await?; + Ok(()) +} + +/// Reload services config for a project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn reload_services(conn_id: String, project_id: String) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::ReloadServices { project_id }) + .await?; + Ok(()) +} + +// ── Project management (async) ────────────────────────────────────── + +/// Add a new project. +#[uniffi::export(async_runtime = "tokio")] +pub async fn add_project( + conn_id: String, + name: String, + path: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action(&conn_id, ActionRequest::AddProject { name, path }) + .await?; + Ok(()) +} + +/// Set project color (named color, e.g. "blue"; unknown → default). +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_project_color( + conn_id: String, + project_id: String, + color: String, +) -> Result<(), MobileFfiError> { + let folder_color: okena_core::theme::FolderColor = + serde_json::from_value(serde_json::Value::String(color)).unwrap_or_default(); + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetProjectColor { + project_id, + color: folder_color, + }, + ) + .await?; + Ok(()) +} + +/// Set folder color (named color; unknown → default). +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_folder_color( + conn_id: String, + folder_id: String, + color: String, +) -> Result<(), MobileFfiError> { + let folder_color: okena_core::theme::FolderColor = + serde_json::from_value(serde_json::Value::String(color)).unwrap_or_default(); + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetFolderColor { + folder_id, + color: folder_color, + }, + ) + .await?; + Ok(()) +} + +/// Reorder a project within a folder. +#[uniffi::export(async_runtime = "tokio")] +pub async fn reorder_project_in_folder( + conn_id: String, + folder_id: String, + project_id: String, + new_index: u32, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::ReorderProjectInFolder { + folder_id, + project_id, + new_index: new_index as usize, + }, + ) + .await?; + Ok(()) +} + +// ── Layout actions (async) ────────────────────────────────────────── + +/// Update split sizes for a split pane. +#[uniffi::export(async_runtime = "tokio")] +pub async fn update_split_sizes( + conn_id: String, + project_id: String, + path: Vec, + sizes: Vec, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::UpdateSplitSizes { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + sizes, + }, + ) + .await?; + Ok(()) +} + +/// Add a new tab to a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn add_tab( + conn_id: String, + project_id: String, + path: Vec, + in_group: bool, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::AddTab { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + in_group, + }, + ) + .await?; + Ok(()) +} + +/// Set the active tab in a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn set_active_tab( + conn_id: String, + project_id: String, + path: Vec, + index: u32, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::SetActiveTab { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + index: index as usize, + }, + ) + .await?; + Ok(()) +} + +/// Move a tab within a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn move_tab( + conn_id: String, + project_id: String, + path: Vec, + from_index: u32, + to_index: u32, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::MoveTab { + project_id, + path: path.into_iter().map(|v| v as usize).collect(), + from_index: from_index as usize, + to_index: to_index as usize, + }, + ) + .await?; + Ok(()) +} + +/// Move a terminal into a tab group. +#[uniffi::export(async_runtime = "tokio")] +pub async fn move_terminal_to_tab_group( + conn_id: String, + project_id: String, + terminal_id: String, + target_path: Vec, + position: Option, + target_project_id: Option, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::MoveTerminalToTabGroup { + project_id, + terminal_id, + target_path: target_path.into_iter().map(|v| v as usize).collect(), + position: position.map(|p| p as usize), + target_project_id, + }, + ) + .await?; + Ok(()) +} + +/// Move a pane to a drop zone relative to another terminal. +#[uniffi::export(async_runtime = "tokio")] +pub async fn move_pane_to( + conn_id: String, + project_id: String, + terminal_id: String, + target_project_id: String, + target_terminal_id: String, + zone: String, +) -> Result<(), MobileFfiError> { + ConnectionManager::get() + .send_action( + &conn_id, + ActionRequest::MovePaneTo { + project_id, + terminal_id, + target_project_id, + target_terminal_id, + zone, + }, + ) + .await?; + Ok(()) +} diff --git a/crates/okena-mobile-ffi/src/types.rs b/crates/okena-mobile-ffi/src/types.rs new file mode 100644 index 00000000..848d6e97 --- /dev/null +++ b/crates/okena-mobile-ffi/src/types.rs @@ -0,0 +1,241 @@ +//! uniffi `Record` / `Enum` types mirroring the data the FFI returns. +//! +//! uniffi derive macros cannot be placed on types from another crate, so we +//! define our own here and convert from `okena_mobile_native`'s api/client +//! types via `From`. The shapes match 1:1 so the conversions are mechanical. + +use std::collections::HashMap; + +use okena_mobile_native::api::{ + connection::ConnectionStatus as NativeConnectionStatus, + state::{ + FolderInfo as NativeFolderInfo, FullscreenInfo as NativeFullscreenInfo, + ProjectInfo as NativeProjectInfo, ServiceInfo as NativeServiceInfo, + }, + terminal::{ + CellData as NativeCellData, CursorShape as NativeCursorShape, + CursorState as NativeCursorState, ScrollInfo as NativeScrollInfo, + SelectionBounds as NativeSelectionBounds, + }, +}; + +/// Connection status surfaced to the RN layer. +/// +/// Mirrors `okena_mobile_native::api::connection::ConnectionStatus` (which +/// itself collapses core's `Reconnecting { attempt }` into `Connecting`). +#[derive(Debug, Clone, uniffi::Enum)] +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Pairing, + Error { message: String }, +} + +impl From for ConnectionStatus { + fn from(s: NativeConnectionStatus) -> Self { + match s { + NativeConnectionStatus::Disconnected => ConnectionStatus::Disconnected, + NativeConnectionStatus::Connecting => ConnectionStatus::Connecting, + NativeConnectionStatus::Connected => ConnectionStatus::Connected, + NativeConnectionStatus::Pairing => ConnectionStatus::Pairing, + NativeConnectionStatus::Error { message } => ConnectionStatus::Error { message }, + } + } +} + +/// A single terminal grid cell (flat, FFI-friendly). +#[derive(Debug, Clone, uniffi::Record)] +pub struct CellData { + /// The character in this cell (empty string for wide-char spacers). + pub character: String, + /// Foreground color as ARGB packed u32. + pub fg: u32, + /// Background color as ARGB packed u32. + pub bg: u32, + /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). + pub flags: u8, +} + +impl From for CellData { + fn from(c: NativeCellData) -> Self { + CellData { + character: c.character, + fg: c.fg, + bg: c.bg, + flags: c.flags, + } + } +} + +/// Cursor shape variants. +#[derive(Debug, Clone, uniffi::Enum)] +pub enum CursorShape { + Block, + Underline, + Beam, +} + +impl From for CursorShape { + fn from(s: NativeCursorShape) -> Self { + match s { + NativeCursorShape::Block => CursorShape::Block, + NativeCursorShape::Underline => CursorShape::Underline, + NativeCursorShape::Beam => CursorShape::Beam, + } + } +} + +/// Cursor state for rendering. +#[derive(Debug, Clone, uniffi::Record)] +pub struct CursorState { + pub col: u16, + pub row: u16, + pub shape: CursorShape, + pub visible: bool, +} + +impl From for CursorState { + fn from(c: NativeCursorState) -> Self { + CursorState { + col: c.col, + row: c.row, + shape: c.shape.into(), + visible: c.visible, + } + } +} + +/// Scroll info: total/visible line counts and the current display offset. +#[derive(Debug, Clone, uniffi::Record)] +pub struct ScrollInfo { + pub total_lines: u32, + pub visible_lines: u32, + pub display_offset: u32, +} + +impl From for ScrollInfo { + fn from(s: NativeScrollInfo) -> Self { + ScrollInfo { + total_lines: s.total_lines, + visible_lines: s.visible_lines, + display_offset: s.display_offset, + } + } +} + +/// Selection bounds (rows are buffer-relative, adjusted for display offset). +#[derive(Debug, Clone, uniffi::Record)] +pub struct SelectionBounds { + pub start_col: u16, + pub start_row: i32, + pub end_col: u16, + pub end_row: i32, +} + +impl From for SelectionBounds { + fn from(s: NativeSelectionBounds) -> Self { + SelectionBounds { + start_col: s.start_col, + start_row: s.start_row, + end_col: s.end_col, + end_row: s.end_row, + } + } +} + +/// Service entry inside a project. +#[derive(Debug, Clone, uniffi::Record)] +pub struct ServiceInfo { + pub name: String, + pub status: String, + pub terminal_id: Option, + pub ports: Vec, + pub exit_code: Option, + pub kind: String, + pub is_extra: bool, +} + +impl From for ServiceInfo { + fn from(s: NativeServiceInfo) -> Self { + ServiceInfo { + name: s.name, + status: s.status, + terminal_id: s.terminal_id, + ports: s.ports, + exit_code: s.exit_code, + kind: s.kind, + is_extra: s.is_extra, + } + } +} + +/// Flat, FFI-friendly project info. +#[derive(Debug, Clone, uniffi::Record)] +pub struct ProjectInfo { + pub id: String, + pub name: String, + pub path: String, + pub show_in_overview: bool, + pub terminal_ids: Vec, + pub terminal_names: HashMap, + pub git_branch: Option, + pub git_lines_added: u32, + pub git_lines_removed: u32, + pub services: Vec, + pub folder_color: String, +} + +impl From for ProjectInfo { + fn from(p: NativeProjectInfo) -> Self { + ProjectInfo { + id: p.id, + name: p.name, + path: p.path, + show_in_overview: p.show_in_overview, + terminal_ids: p.terminal_ids, + terminal_names: p.terminal_names, + git_branch: p.git_branch, + git_lines_added: p.git_lines_added, + git_lines_removed: p.git_lines_removed, + services: p.services.into_iter().map(Into::into).collect(), + folder_color: p.folder_color, + } + } +} + +/// Folder grouping projects. +#[derive(Debug, Clone, uniffi::Record)] +pub struct FolderInfo { + pub id: String, + pub name: String, + pub project_ids: Vec, + pub folder_color: String, +} + +impl From for FolderInfo { + fn from(f: NativeFolderInfo) -> Self { + FolderInfo { + id: f.id, + name: f.name, + project_ids: f.project_ids, + folder_color: f.folder_color, + } + } +} + +/// The currently fullscreened terminal, if any. +#[derive(Debug, Clone, uniffi::Record)] +pub struct FullscreenInfo { + pub project_id: String, + pub terminal_id: String, +} + +impl From for FullscreenInfo { + fn from(f: NativeFullscreenInfo) -> Self { + FullscreenInfo { + project_id: f.project_id, + terminal_id: f.terminal_id, + } + } +} diff --git a/mobile/native/Cargo.toml b/mobile/native/Cargo.toml index 75f20247..71d73f0d 100644 --- a/mobile/native/Cargo.toml +++ b/mobile/native/Cargo.toml @@ -4,7 +4,10 @@ version = "0.1.0" edition = "2024" [lib] -crate-type = ["cdylib", "staticlib"] +# `lib` is additive: the cdylib/staticlib still build for Flutter, while `lib` +# lets the new `okena-mobile-ffi` crate depend on this crate as a Rust library +# and reuse its ConnectionManager / TerminalHolder logic directly. +crate-type = ["cdylib", "staticlib", "lib"] [dependencies] okena-core = { path = "../../crates/okena-core", features = ["client"] } From dac17773f0444b59c082929f95b5df80c91160e9 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 8 Jun 2026 16:54:17 +0200 Subject: [PATCH 29/37] feat(mobile-rn): RN binding contract + native Skia terminal renderer (Phase 2/3 scaffold) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scaffolds the React Native side under mobile/rn/ — the two technically-meaty pieces, not a full app: - src/native/okena.ts — the native↔TS binding contract: typed OkenaNative interface for all ~60 FFI fns + record/enum types, sync/async split mirroring the Rust side. ubrn generates the real impl from crates/okena-mobile-ffi. - src/native/cells.ts — packed cell-buffer decoder, byte-for-byte matching the Rust get_visible_cells_packed format (+ a zero-alloc PackedCells view). - src/components/TerminalView.tsx — terminal_painter.dart's 3-pass paint ported to @shopify/react-native-skia (no xterm.js): bg rects, style-batched glyph runs, cursor; rAF repaint gated on isDirty (Decision C); onLayout sizing. - src/theme.ts, package.json, tsconfig.json, README.md (exact local build steps). Verified on this box: npm install + tsc --noEmit (strict) pass; Skia APIs type-checked against @shopify/react-native-skia@1.5.x. Device build is not possible here (no Android/iOS SDK, no ubrn-generated module) — README documents the local steps. node_modules gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/rn/.gitignore | 6 + mobile/rn/README.md | 170 + mobile/rn/package-lock.json | 6374 +++++++++++++++++++++ mobile/rn/package.json | 26 + mobile/rn/src/components/TerminalView.tsx | 519 ++ mobile/rn/src/native/cells.ts | 206 + mobile/rn/src/native/okena.ts | 442 ++ mobile/rn/src/theme.ts | 107 + mobile/rn/tsconfig.json | 29 + 9 files changed, 7879 insertions(+) create mode 100644 mobile/rn/.gitignore create mode 100644 mobile/rn/README.md create mode 100644 mobile/rn/package-lock.json create mode 100644 mobile/rn/package.json create mode 100644 mobile/rn/src/components/TerminalView.tsx create mode 100644 mobile/rn/src/native/cells.ts create mode 100644 mobile/rn/src/native/okena.ts create mode 100644 mobile/rn/src/theme.ts create mode 100644 mobile/rn/tsconfig.json diff --git a/mobile/rn/.gitignore b/mobile/rn/.gitignore new file mode 100644 index 00000000..99e8c630 --- /dev/null +++ b/mobile/rn/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +# RN host project artifacts (generated by `npx @react-native-community/cli init`) +android/ +ios/ +.expo/ +*.log diff --git a/mobile/rn/README.md b/mobile/rn/README.md new file mode 100644 index 00000000..dc7808a0 --- /dev/null +++ b/mobile/rn/README.md @@ -0,0 +1,170 @@ +# Okena mobile — React Native UI (scaffold) + +This directory is the **React Native** side of the Flutter→RN migration described in +[`../RN_MIGRATION.md`](../RN_MIGRATION.md). It contains the two technically-meaty, +high-value pieces of the RN layer plus minimal project config: + +1. **The native↔TS binding contract** — `src/native/okena.ts`. The full `OkenaNative` + interface mirroring the ~60 Rust FFI functions in `mobile/native/src/api/{connection,terminal,state}.rs`, + with all the record/enum types. +2. **The packed-cell decoder** — `src/native/cells.ts`. Reads the little-endian binary + cell buffer that the render hot path consumes. +3. **The native terminal renderer** — `src/components/TerminalView.tsx`. A + `@shopify/react-native-skia` port of `mobile/lib/src/widgets/terminal_painter.dart` + (3-pass paint) + the sizing/poll loop from `terminal_view.dart`. **No `xterm.js`.** +4. **Theme** — `src/theme.ts`, ported from `mobile/lib/src/theme/app_theme.dart`. + +## ⚠️ This is an UN-BUILT scaffold + +It was authored on a **headless Linux box with no Android/iOS SDKs**, so it has **not** +been compiled or run on a device. What IS verified: the TypeScript type-checks against +the public APIs of `react-native@0.76` and `@shopify/react-native-skia@^1.5` (run +`npm install && npm run typecheck` once you have network). What is NOT verified: anything +that needs the native toolchain (the `ubrn`-generated module, Skia native binaries, an +emulator/device). + +The contract files (`src/native/*`) are **specs both sides agree on**. The real native +module is *generated* — `src/native/okena.ts`'s `getOkenaNative()` throws until you wire +the generated package in (see step 4 below). `TerminalView` takes the native module via a +prop, so it is testable against a mock implementing `OkenaNative` without the real binding. + +### One function does not exist Rust-side yet + +`getVisibleCellsPacked(connId, terminalId): ArrayBuffer` is **being added** to the Rust +crate (`crates/okena-mobile-ffi`, migration Phase 1). The exact byte layout is the +contract documented in `src/native/cells.ts`. Until it lands, the renderer's per-frame +`getVisibleCellsPacked` call will fail — fall back to `getVisibleCells` (records) or mock it. + +--- + +## Local build steps (run these on a machine with the RN toolchain) + +Prereqs: Node ≥ 18, Watchman, JDK 17, Android SDK + NDK (for Android), Xcode + CocoaPods +(for iOS), and the Rust toolchain with the mobile targets (`aarch64-linux-android`, +`aarch64-apple-ios`, `aarch64-apple-ios-sim`). + +### 1. Scaffold a bare RN host app (new architecture ON) + +`@shopify/react-native-skia` and the `ubrn` native module are TurboModules/Fabric, so a +**bare** RN app (or Expo with a dev-client/prebuild — *not* Expo Go) is required. + +```bash +# from mobile/rn/ +npx @react-native-community/cli@latest init OkenaMobile --version 0.76.5 +# RN 0.76 enables the new architecture by default. Confirm: +# android/gradle.properties → newArchEnabled=true +# ios: RCT_NEW_ARCH_ENABLED=1 (set by pod install on 0.76) +``` + +Then move/symlink the `src/` of this scaffold into the new app (or set the app's +`tsconfig`/Metro to resolve this package). The simplest path is to copy `src/`, +`tsconfig.json`, and the deps from `package.json` into the generated app. + +### 2. Install dependencies + +```bash +npm install @shopify/react-native-skia@^1.5.0 +npm install # react, react-native already pinned by the init template +# iOS native pods: +( cd ios && pod install ) +``` + +### 3. Bundle the JetBrainsMono fonts + +The fonts already live at `../fonts/JetBrainsMono-{Regular,Bold,Italic,BoldItalic}.ttf`. +Load them in JS with Skia's `useFont` (preferred — keeps them out of native asset configs): + +```ts +import { useFont } from '@shopify/react-native-skia'; +import { TerminalTheme } from './src/theme'; + +const regular = useFont(require('../fonts/JetBrainsMono-Regular.ttf'), TerminalTheme.defaultFontSize); +const bold = useFont(require('../fonts/JetBrainsMono-Bold.ttf'), TerminalTheme.defaultFontSize); +const italic = useFont(require('../fonts/JetBrainsMono-Italic.ttf'), TerminalTheme.defaultFontSize); +const boldItalic = useFont(require('../fonts/JetBrainsMono-BoldItalic.ttf'), TerminalTheme.defaultFontSize); +// render once all four are non-null: +// +``` + +(`TerminalView` re-sizes the fonts in place to its `fontSize` prop, so passing them at any +base size is fine.) The chrome UI's `.SF Pro` maps to RN's `System` font on iOS; Android +falls back to Roboto — see `src/theme.ts`. + +### 4. Generate the Rust↔TS bindings with `ubrn` + +The native module is generated from the sibling Rust crate by +[`uniffi-bindgen-react-native`](https://github.com/jhugman/uniffi-bindgen-react-native) (`ubrn`). + +```bash +# Install the generator (verify the current version during the Phase-0 spike): +npm install --save-dev uniffi-bindgen-react-native + +# A ubrn.config.yaml at the app root points at the Rust crate, e.g.: +# name: okena-mobile-ffi +# rust: +# directory: ../../../crates/okena-mobile-ffi # the uniffi-annotated crate +# manifestPath: Cargo.toml +# bindings: +# cppModuleName: okena_mobile_ffi +# outputDir: ./modules/okena-mobile-ffi +# android: { ... } # NDK targets + jniLibs wiring +# ios: { ... } # xcframework wiring + +# Cross-compile + generate the TS/C++/JNI glue: +npx ubrn build android --config ubrn.config.yaml --and-generate +npx ubrn build ios --config ubrn.config.yaml --and-generate +( cd ios && pod install ) # re-run pods to pick up the generated xcframework +``` + +`ubrn` emits an installable JS package whose exported functions match `OkenaNative`. Wire +it into `src/native/okena.ts`: + +```ts +// src/native/okena.ts — replace the throwing getOkenaNative() body: +import * as gen from 'okena-mobile-ffi'; // ← the ubrn output package +export function getOkenaNative(): OkenaNative { + return gen as unknown as OkenaNative; // generated names already camelCase +} +``` + +> The `crates/okena-mobile-ffi` crate is owned by a separate agent (Phase 1 of the plan): +> it strips the `flutter_rust_bridge` attributes, adds `#[uniffi::export]`, ports all ~60 +> functions, and **adds `get_visible_cells_packed`**. Do not edit `mobile/native` for this. + +### 5. Run + +```bash +npx react-native run-android # device/emulator with the app installed +npx react-native run-ios # simulator +``` + +The first run does the Rust cross-compile via `ubrn`'s Gradle/CocoaPods integration (the +RN equivalent of what `cargokit` does for Flutter today). Ensure `$HOME/.cargo/bin` is on +the PATH the Gradle daemon sees, and use `rustls-tls` (not `native-tls`) Rust-side to avoid +cross-compiling OpenSSL for the NDK — same constraints as the Flutter build (`../CLAUDE.md`). + +--- + +## Verifying just the TS (no device needed) + +```bash +cd mobile/rn +npm install # needs network for react / react-native / skia type packages +npm run typecheck # tsc --noEmit, strict +``` + +## File map + +``` +mobile/rn/ +├── README.md # this file +├── package.json # RN 0.76, react 18.3, skia ^1.5, typescript +├── tsconfig.json # strict, react-jsx, bundler resolution +└── src/ + ├── theme.ts # colors + typography (← app_theme.dart) + ├── native/ + │ ├── okena.ts # OkenaNative contract (← api/*.rs), getOkenaNative() shim + │ └── cells.ts # packed cell-buffer decoder + flag constants + ARGB helpers + └── components/ + └── TerminalView.tsx # Skia 3-pass renderer (← terminal_painter.dart + terminal_view.dart) +``` diff --git a/mobile/rn/package-lock.json b/mobile/rn/package-lock.json new file mode 100644 index 00000000..de66f19c --- /dev/null +++ b/mobile/rn/package-lock.json @@ -0,0 +1,6374 @@ +{ + "name": "okena-mobile-rn", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "okena-mobile-rn", + "version": "0.0.0", + "dependencies": { + "@shopify/react-native-skia": "^1.5.0", + "react": "18.3.1", + "react-native": "0.76.5" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.29.7.tgz", + "integrity": "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.29.7.tgz", + "integrity": "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", + "integrity": "sha512-MN5dasWo37MirVcKWuysRkRr4BjNc81SXwUtJYstwbn8oEkfnwR9DaqdDTo/hHOnTdhafffLIa2xOOHcjDIGEw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.5.tgz", + "integrity": "sha512-xe7HSQGop4bnOLMaXt0aU+rIatMNEQbz242SDl8V9vx5oOTI0VbZV9yLy6yBc6poUlYbcboF20YVjoRsxX4yww==", + "license": "MIT", + "dependencies": { + "@react-native/codegen": "0.76.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.5.tgz", + "integrity": "sha512-1Nu5Um4EogOdppBLI4pfupkteTjWfmI0hqW8ezWTg7Bezw0FtBj8yS8UYVd3wTnDFT9A5mA2VNoNUqomJnvj2A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.76.5", + "babel-plugin-syntax-hermes-parser": "^0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.25.1" + } + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.5.tgz", + "integrity": "sha512-FoZ9VRQ5MpgtDAnVo1rT9nNRfjnWpE40o1GeJSDlpUMttd36bVXvsDm8W/NhX8BKTWXSX+CPQJsRcvN1UPYGKg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.23.1", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.5.tgz", + "integrity": "sha512-3MKMnlU0cZOWlMhz5UG6WqACJiWUrE3XwBEumzbMmZw3Iw3h+fIsn+7kLLE5EhzqLt0hg5Y4cgYFi4kOaNgq+g==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.76.5", + "@react-native/metro-babel-transformer": "0.76.5", + "chalk": "^4.0.0", + "execa": "^5.1.1", + "invariant": "^2.2.4", + "metro": "^0.81.0", + "metro-config": "^0.81.0", + "metro-core": "^0.81.0", + "node-fetch": "^2.2.0", + "readline": "^1.3.0", + "semver": "^7.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-native-community/cli-server-api": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli-server-api": { + "optional": true + } + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.5.tgz", + "integrity": "sha512-5gtsLfBaSoa9WP8ToDb/8NnDBLZjv4sybQQj7rDKytKOdsXm3Pr2y4D7x7GQQtP1ZQRqzU0X0OZrhRz9xNnOqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.5.tgz", + "integrity": "sha512-f8eimsxpkvMgJia7POKoUu9uqjGF6KgkxX4zqr/a6eoR1qdEAWUd6PonSAqtag3PAqvEaJpB99gLH2ZJI1nDGg==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.76.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.76.5.tgz", + "integrity": "sha512-7KSyD0g0KhbngITduC8OABn0MAlJfwjIdze7nA4Oe1q3R7qmAv+wQzW+UEXvPah8m1WqFjYTkQwz/4mK3XrQGw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.76.5.tgz", + "integrity": "sha512-ggM8tcKTcaqyKQcXMIvcB0vVfqr9ZRhWVxWIdiFO1mPvJyS6n+a+lLGkgQAyO8pfH0R1qw6K9D0nqbbDo865WQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.5.tgz", + "integrity": "sha512-Cm9G5Sg5BDty3/MKa3vbCAJtT3YHhlEaPlQALLykju7qBS+pHZV9bE9hocfyyvc5N/osTIGWxG5YOfqTeMu1oQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.76.5", + "hermes-parser": "0.23.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.5.tgz", + "integrity": "sha512-6QRLEok1r55gLqj+94mEWUENuU5A6wsr2OoXpyq/CgQ7THWowbHtru/kRGRr6o3AQXrVnZheR60JNgFcpNYIug==", + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.76.5.tgz", + "integrity": "sha512-M/fW1fTwxrHbcx0OiVOIxzG6rKC0j9cR9Csf80o77y1Xry0yrNPpAlf8D1ev3LvHsiAUiRNFlauoPtodrs2J1A==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@shopify/react-native-skia": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-1.12.4.tgz", + "integrity": "sha512-8QDIBKSU7XB3Lc1kAv4jSFddTQK8AE+1AEoJnQLNllsiex1gufLQ8kN7rs9zii+iboSY8tYKT7ocV+5cE2Exdw==", + "license": "MIT", + "dependencies": { + "canvaskit-wasm": "0.40.0", + "react-reconciler": "0.27.0" + }, + "bin": { + "setup-skia-web": "scripts/setup-canvaskit.js" + }, + "peerDependencies": { + "react": ">=18.0 <19.0.0", + "react-native": ">=0.64 <0.78.0", + "react-native-reanimated": ">=2.0.0" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + } + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz", + "integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==", + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz", + "integrity": "sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.23.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", + "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvaskit-wasm": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.40.0.tgz", + "integrity": "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==", + "license": "BSD-3-Clause", + "dependencies": { + "@webgpu/types": "0.1.21" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT" + }, + "node_modules/flow-parser": { + "version": "0.317.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.317.0.tgz", + "integrity": "sha512-pzwkCzruTUg6f5HH7N0OvVjX7dVc361tnsEkSCbMC9cJ5zxZY84de8l+DraCmnGsIbi+jQPPxtTKfBJFnYCJZQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.1.tgz", + "integrity": "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.1.tgz", + "integrity": "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.23.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-android": { + "version": "250231.0.0", + "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz", + "integrity": "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==", + "license": "BSD-2-Clause" + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD" + }, + "node_modules/jscodeshift": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.14.0.tgz", + "integrity": "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.13.16", + "@babel/parser": "^7.13.16", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/preset-flow": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.13.16", + "babel-core": "^7.0.0-bridge.0", + "chalk": "^4.1.2", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "neo-async": "^2.5.0", + "node-dir": "^0.1.17", + "recast": "^0.21.0", + "temp": "^0.8.4", + "write-file-atomic": "^2.3.0" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/jscodeshift/node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/metro": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.81.5.tgz", + "integrity": "sha512-YpFF0DDDpDVygeca2mAn7K0+us+XKmiGk4rIYMz/CRdjFoCGqAei/IQSpV0UrGfQbToSugpMQeQJveaWSH88Hg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.25.1", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.81.5", + "metro-cache": "0.81.5", + "metro-cache-key": "0.81.5", + "metro-config": "0.81.5", + "metro-core": "0.81.5", + "metro-file-map": "0.81.5", + "metro-resolver": "0.81.5", + "metro-runtime": "0.81.5", + "metro-source-map": "0.81.5", + "metro-symbolicate": "0.81.5", + "metro-transform-plugins": "0.81.5", + "metro-transform-worker": "0.81.5", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.81.5.tgz", + "integrity": "sha512-oKCQuajU5srm+ZdDcFg86pG/U8hkSjBlkyFjz380SZ4TTIiI5F+OQB830i53D8hmqmcosa4wR/pnKv8y4Q3dLw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.25.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/metro-cache": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.81.5.tgz", + "integrity": "sha512-wOsXuEgmZMZ5DMPoz1pEDerjJ11AuMy9JifH4yNW7NmWS0ghCRqvDxk13LsElzLshey8C+my/tmXauXZ3OqZgg==", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "metro-core": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache-key": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.81.5.tgz", + "integrity": "sha512-lGWnGVm1UwO8faRZ+LXQUesZSmP1LOg14OVR+KNPBip8kbMECbQJ8c10nGesw28uQT7AE0lwQThZPXlxDyCLKQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-config": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.81.5.tgz", + "integrity": "sha512-oDRAzUvj6RNRxratFdcVAqtAsg+T3qcKrGdqGZFUdwzlFJdHGR9Z413sW583uD2ynsuOjA2QB6US8FdwiBdNKg==", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.81.5", + "metro-cache": "0.81.5", + "metro-core": "0.81.5", + "metro-runtime": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-core": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.81.5.tgz", + "integrity": "sha512-+2R0c8ByfV2N7CH5wpdIajCWa8escUFd8TukfoXyBq/vb6yTCsznoA25FhNXJ+MC/cz1L447Zj3vdUfCXIZBwg==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.81.5.tgz", + "integrity": "sha512-mW1PKyiO3qZvjeeVjj1brhkmIotObA3/9jdbY1fQQYvEWM6Ml7bN/oJCRDGn2+bJRlG+J8pwyJ+DgdrM4BsKyg==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro-file-map/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/metro-minify-terser": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.81.5.tgz", + "integrity": "sha512-/mn4AxjANnsSS3/Bb+zA1G5yIS5xygbbz/OuPaJYs0CPcZCaWt66D+65j4Ft/nJkffUxcwE9mk4ubpkl3rjgtw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-resolver": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.81.5.tgz", + "integrity": "sha512-6BX8Nq3g3go3FxcyXkVbWe7IgctjDTk6D9flq+P201DfHHQ28J+DWFpVelFcrNTn4tIfbP/Bw7u/0g2BGmeXfQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-runtime": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.81.5.tgz", + "integrity": "sha512-M/Gf71ictUKP9+77dV/y8XlAWg7xl76uhU7ggYFUwEdOHHWPG6gLBr1iiK0BmTjPFH8yRo/xyqMli4s3oGorPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.81.5.tgz", + "integrity": "sha512-Jz+CjvCKLNbJZYJTBeN3Kq9kIJf6b61MoLBdaOQZJ5Ajhw6Pf95Nn21XwA8BwfUYgajsi6IXsp/dTZsYJbN00Q==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.81.5", + "nullthrows": "^1.1.1", + "ob1": "0.81.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.81.5.tgz", + "integrity": "sha512-X3HV3n3D6FuTE11UWFICqHbFMdTavfO48nXsSpnNGFkUZBexffu0Xd+fYKp+DJLNaQr3S+lAs8q9CgtDTlRRuA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.81.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.81.5.tgz", + "integrity": "sha512-MmHhVx/1dJC94FN7m3oHgv5uOjKH8EX8pBeu1pnPMxbJrx6ZuIejO0k84zTSaQTZ8RxX1wqwzWBpXAWPjEX8mA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.81.5.tgz", + "integrity": "sha512-lUFyWVHa7lZFRSLJEv+m4jH8WrR5gU7VIjUlg4XmxQfV8ngY4V10ARKynLhMYPeQGl7Qvf+Ayg0eCZ272YZ4Mg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.81.5", + "metro-babel-transformer": "0.81.5", + "metro-cache": "0.81.5", + "metro-cache-key": "0.81.5", + "metro-minify-terser": "0.81.5", + "metro-source-map": "0.81.5", + "metro-transform-plugins": "0.81.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/metro/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.81.5.tgz", + "integrity": "sha512-iNpbeXPLmaiT9I5g16gFFFjsF3sGxLpYG2EGP3dfFB4z+l9X60mp/yRzStHhMtuNt8qmf7Ww80nOPQHngHhnIQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.2.tgz", + "integrity": "sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.5.tgz", + "integrity": "sha512-op2p2kB+lqMF1D7AdX4+wvaR0OPFbvWYs+VBE7bwsb99Cn9xISrLRLAgFflZedQsa5HvnOGrULhtnmItbIKVVw==", + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.6.3", + "@react-native/assets-registry": "0.76.5", + "@react-native/codegen": "0.76.5", + "@react-native/community-cli-plugin": "0.76.5", + "@react-native/gradle-plugin": "0.76.5", + "@react-native/js-polyfills": "0.76.5", + "@react-native/normalize-colors": "0.76.5", + "@react-native/virtualized-lists": "0.76.5", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "^0.23.1", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "commander": "^12.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.6.3", + "jsc-android": "^250231.0.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.81.0", + "metro-source-map": "^0.81.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^5.3.1", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.24.0-canary-efb381bbf-20230505", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readline": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", + "license": "BSD" + }, + "node_modules/recast": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.5.tgz", + "integrity": "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==", + "license": "MIT", + "dependencies": { + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/scheduler": { + "version": "0.24.0-canary-efb381bbf-20230505", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", + "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/temp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", + "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", + "license": "MIT", + "dependencies": { + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.4.tgz", + "integrity": "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/mobile/rn/package.json b/mobile/rn/package.json new file mode 100644 index 00000000..b73270ab --- /dev/null +++ b/mobile/rn/package.json @@ -0,0 +1,26 @@ +{ + "name": "okena-mobile-rn", + "version": "0.0.0", + "private": true, + "description": "Okena mobile client — React Native UI over the Rust core (uniffi/ubrn). New architecture, native Skia terminal renderer. SCAFFOLD: not yet wired to a full RN host project — see README.md.", + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint src --ext .ts,.tsx" + }, + "dependencies": { + "@shopify/react-native-skia": "^1.5.0", + "react": "18.3.1", + "react-native": "0.76.5" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "typescript": "^5.6.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "engines": { + "node": ">=18" + } +} diff --git a/mobile/rn/src/components/TerminalView.tsx b/mobile/rn/src/components/TerminalView.tsx new file mode 100644 index 00000000..35982950 --- /dev/null +++ b/mobile/rn/src/components/TerminalView.tsx @@ -0,0 +1,519 @@ +/** + * TerminalView.tsx — native GPU terminal renderer on `@shopify/react-native-skia`. + * + * ┌───────────────────────────────────────────────────────────────────────┐ + * │ SCAFFOLD — NOT YET BUILT/RUN. Requires the RN toolchain + the `ubrn` │ + * │ native module. See mobile/rn/README.md. The TS is typed against the │ + * │ public APIs of `react-native` and `@shopify/react-native-skia@^1.5`. │ + * └───────────────────────────────────────────────────────────────────────┘ + * + * Port of `mobile/lib/src/widgets/terminal_painter.dart` (the Flutter + * `CustomPainter`) + the sizing/poll loop from `terminal_view.dart`. + * + * Strategy (RN_MIGRATION.md Decision B/C): + * - Cells come from the PACKED buffer (`getVisibleCellsPacked`) decoded via + * ../native/cells, NOT the per-cell record array — avoids marshalling + * thousands of JSI objects per frame. + * - Paint mirrors the Flutter 3-pass algorithm exactly, using + * Skia's imperative `createPicture((canvas) => …)` (the analog of + * `CustomPainter.paint(Canvas, Size)`): + * Pass 1: background rects (only where bg != default) + selection overlay. + * Pass 2: glyph runs batched by (effective fg + style flags) within a row. + * Pass 3: cursor (block / underline / beam, honoring visibility). + * Pass 4: scrollback thumb indicator. + * - Repaint is driven by `requestAnimationFrame` gated on `isDirty()` + * (Decision C), NOT a fixed 33ms timer. + * - Sizing: `onLayout` → cols/rows from a measured monospace glyph → + * `resizeLocal` immediately + debounced `resizeTerminal` (200ms), as today. + * + * Presentational + testable: the native surface is injected via the `native` + * prop (typed `OkenaNative`), so this renders against a mock with no real + * binding. Fonts are injected too, so callers control font loading. + */ + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { View, StyleSheet, type LayoutChangeEvent } from 'react-native'; +import { + Canvas, + Picture, + Skia, + createPicture, + PaintStyle, + type SkCanvas, + type SkFont, + type SkPaint, +} from '@shopify/react-native-skia'; + +import type { + CursorState, + OkenaNative, + ScrollInfo, + SelectionBounds, +} from '../native/okena'; +import { + FLAG_BOLD, + FLAG_DIM, + FLAG_INVERSE, + FLAG_ITALIC, + FLAG_STRIKETHROUGH, + FLAG_STYLE_MASK, + FLAG_UNDERLINE, + PackedCells, + decodeCellsView, +} from '../native/cells'; +import { TerminalTheme } from '../theme'; + +// ── Fonts ─────────────────────────────────────────────────────────────────── + +/** + * The four JetBrainsMono variants. Load with `useFont` from the bundled ttf + * (see README) and pass them in. `regular` is required; the others are + * optional — when a variant is missing we synthesize it on the regular font + * via `setEmbolden` (bold) / `setSkewX` (italic), matching how Skia would + * fake-style anyway. + */ +export interface TerminalFonts { + regular: SkFont; + bold?: SkFont; + italic?: SkFont; + boldItalic?: SkFont; +} + +// ── Props ───────────────────────────────────────────────────────────────── + +export interface TerminalViewProps { + /** Injected native surface (real `ubrn` module, or a mock for tests). */ + native: OkenaNative; + connId: string; + terminalId: string; + /** Loaded JetBrainsMono fonts (see {@link TerminalFonts}). */ + fonts: TerminalFonts; + /** Font size in logical px. Defaults to {@link TerminalTheme.defaultFontSize}. */ + fontSize?: number; + /** + * Whether a selection is in progress (drives whether selection bounds are + * polled each frame). The host (gesture handler) owns this. + */ + selecting?: boolean; + /** + * Called whenever the computed grid size changes, AFTER `resizeLocal` has run. + * The host typically debounces a `resizeTerminal` here, but this component + * already does that internally; this is for the host to track cols/rows. + */ + onGridSizeChange?: (cols: number, rows: number) => void; +} + +// ── Geometry from font metrics ──────────────────────────────────────────────── + +interface CellMetrics { + cellWidth: number; + cellHeight: number; + /** Baseline offset from the top of the cell (ascent), for glyph placement. */ + baseline: number; +} + +/** + * Compute monospace cell metrics from the regular font, mirroring + * `_computeCellSize()` in terminal_view.dart (`width` of 'M', height * lineHeightFactor). + */ +function measureCell(font: SkFont): CellMetrics { + const advance = font.measureText('M').width; + const m = font.getMetrics(); + // ascent is negative (above baseline), descent positive (below). + const textHeight = -m.ascent + m.descent; + const cellHeight = textHeight * TerminalTheme.lineHeightFactor; + // Center the glyph box vertically within the (taller) cell, then add ascent. + const baseline = (cellHeight - textHeight) / 2 + -m.ascent; + return { cellWidth: advance, cellHeight, baseline }; +} + +// ── Color helpers ───────────────────────────────────────────────────────────── + +/** Cache Skia colors keyed by packed ARGB to avoid re-allocating each frame. */ +const colorCache = new Map>(); +function skColor(argb: number) { + const key = argb >>> 0; + let c = colorCache.get(key); + if (!c) { + // Skia.Color accepts CSS-ish input; pass an `#RRGGBBAA` string built from ARGB. + const v = key; + const a = (v >>> 24) & 0xff; + const r = (v >>> 16) & 0xff; + const g = (v >>> 8) & 0xff; + const b = v & 0xff; + const hex = (n: number) => n.toString(16).padStart(2, '0'); + c = Skia.Color(`#${hex(r)}${hex(g)}${hex(b)}${hex(a)}`); + colorCache.set(key, c); + } + return c; +} + +function alpha(argb: number): number { + return (argb >>> 24) & 0xff; +} + +// ── Paint passes (the Flutter port) ──────────────────────────────────────────── + +function selectCellInSelection( + col: number, + row: number, + sel: SelectionBounds, + displayOffset: number, +): boolean { + // Buffer row = visual row - display offset (matches _isCellInSelection in Dart). + const bufferRow = row - displayOffset; + const { startRow: sr, endRow: er, startCol: sc, endCol: ec } = sel; + if (bufferRow < sr || bufferRow > er) return false; + if (sr === er) return col >= sc && col <= ec; + if (bufferRow === sr) return col >= sc; + if (bufferRow === er) return col <= ec; + return true; +} + +interface PaintArgs { + canvas: SkCanvas; + cells: PackedCells; + cursor: CursorState; + scroll: ScrollInfo; + selection?: SelectionBounds; + metrics: CellMetrics; + fonts: TerminalFonts; + fontSize: number; + width: number; + height: number; +} + +/** Pick the font variant for a style, faking bold/italic on `regular` if absent. */ +function fontFor(fonts: TerminalFonts, bold: boolean, italic: boolean): SkFont { + if (bold && italic && fonts.boldItalic) return fonts.boldItalic; + if (bold && !italic && fonts.bold) return fonts.bold; + if (!bold && italic && fonts.italic) return fonts.italic; + if (!bold && !italic) return fonts.regular; + // Variant missing → synthesize on regular. (Mutates a shared font; acceptable + // here because painting is single-threaded and we reset below.) + const f = fonts.regular; + f.setEmbolden(bold); + f.setSkewX(italic ? -0.25 : 0); + return f; +} + +function resetSynthetic(font: SkFont) { + font.setEmbolden(false); + font.setSkewX(0); +} + +function paintTerminal(args: PaintArgs): void { + const { canvas, cells, cursor, scroll, selection, metrics, fonts, width, height } = args; + const { cellWidth, cellHeight, baseline } = metrics; + const cols = cells.cols; + const rows = cells.rows; + + const bgPaint = Skia.Paint(); + bgPaint.setAntiAlias(false); + + const defaultBg = TerminalTheme.bgColorArgb >>> 0; + const selOverlay = skColor(TerminalTheme.selectionOverlayArgb); + const displayOffset = scroll.displayOffset; + + // ── Pass 1: background rects + selection highlight ────────────────────── + for (let i = 0; i < cells.count; i++) { + const col = i % cols; + const row = (i / cols) | 0; + const x = col * cellWidth; + const y = row * cellHeight; + + let bgArgb = cells.bg(i); + let fgArgb = cells.fg(i); + const flags = cells.flags(i); + if (flags & FLAG_INVERSE) { + const tmp = bgArgb; + bgArgb = fgArgb; + fgArgb = tmp; + } + + if ((bgArgb >>> 0) !== defaultBg && alpha(bgArgb) > 0) { + bgPaint.setColor(skColor(bgArgb)); + canvas.drawRect(Skia.XYWHRect(x, y, cellWidth, cellHeight), bgPaint); + } + + if (selection && selectCellInSelection(col, row, selection, displayOffset)) { + bgPaint.setColor(selOverlay); + canvas.drawRect(Skia.XYWHRect(x, y, cellWidth, cellHeight), bgPaint); + } + } + + // ── Pass 2: text — batched by style runs within each row ───────────────── + const textPaint = Skia.Paint(); + textPaint.setAntiAlias(true); + + for (let row = 0; row < rows; row++) { + let col = 0; + while (col < cols) { + const idx = row * cols + col; + if (cells.isBlank(idx)) { + col++; + continue; + } + + const flags0 = cells.flags(idx); + let fg0 = flags0 & FLAG_INVERSE ? cells.bg(idx) : cells.fg(idx); + const style0 = flags0 & FLAG_STYLE_MASK; + + const startCol = col; + let run = cells.char(idx); + col++; + + while (col < cols) { + const ci = row * cols + col; + if (cells.isBlank(ci)) break; + const f = cells.flags(ci); + const cFg = f & FLAG_INVERSE ? cells.bg(ci) : cells.fg(ci); + const cStyle = f & FLAG_STYLE_MASK; + if ((cFg >>> 0) !== (fg0 >>> 0) || cStyle !== style0) break; + run += cells.char(ci); + col++; + } + + // Effective fg (dim halves alpha, matching the Dart painter). + let fgEff = fg0 >>> 0; + if (style0 & FLAG_DIM) { + const a = Math.round(alpha(fgEff) * 0.5); + fgEff = ((a << 24) | (fgEff & 0x00ffffff)) >>> 0; + } + textPaint.setColor(skColor(fgEff)); + + const bold = (style0 & FLAG_BOLD) !== 0; + const italic = (style0 & FLAG_ITALIC) !== 0; + const font = fontFor(fonts, bold, italic); + + const x = startCol * cellWidth; + const y = row * cellHeight + baseline; + canvas.drawText(run, x, y, textPaint, font); + + // Underline / strikethrough as drawn lines (decoration in Dart). + if (style0 & (FLAG_UNDERLINE | FLAG_STRIKETHROUGH)) { + const linePaint = Skia.Paint(); + linePaint.setColor(skColor(fgEff)); + linePaint.setStrokeWidth(Math.max(1, args.fontSize / 14)); + const runW = run.length * cellWidth; + if (style0 & FLAG_UNDERLINE) { + const uy = row * cellHeight + cellHeight - 1; + canvas.drawLine(x, uy, x + runW, uy, linePaint); + } + if (style0 & FLAG_STRIKETHROUGH) { + const sy = row * cellHeight + cellHeight / 2; + canvas.drawLine(x, sy, x + runW, sy, linePaint); + } + } + + resetSynthetic(fonts.regular); + } + } + + // ── Pass 3: cursor ─────────────────────────────────────────────────────── + if (cursor.visible && cursor.col < cols && cursor.row < rows) { + const cx = cursor.col * cellWidth; + const cy = cursor.row * cellHeight; + const cursorPaint = Skia.Paint(); + switch (cursor.shape) { + case 'block': { + // Half-alpha block, matching withAlpha(128) in Dart. + const c = (0x80000000 | (TerminalTheme.cursorColorArgb & 0x00ffffff)) >>> 0; + cursorPaint.setColor(skColor(c)); + cursorPaint.setStyle(PaintStyle.Fill); + canvas.drawRect(Skia.XYWHRect(cx, cy, cellWidth, cellHeight), cursorPaint); + break; + } + case 'beam': { + cursorPaint.setColor(skColor(TerminalTheme.cursorColorArgb)); + cursorPaint.setStrokeWidth(2); + canvas.drawLine(cx, cy, cx, cy + cellHeight, cursorPaint); + break; + } + case 'underline': { + cursorPaint.setColor(skColor(TerminalTheme.cursorColorArgb)); + cursorPaint.setStrokeWidth(2); + canvas.drawLine(cx, cy + cellHeight - 1, cx + cellWidth, cy + cellHeight - 1, cursorPaint); + break; + } + } + } + + // ── Pass 4: scrollback thumb ─────────────────────────────────────────────── + if (scroll.totalLines > scroll.visibleLines && scroll.totalLines > 0 && scroll.visibleLines > 0) { + const trackHeight = height; + const thumbHeight = Math.min( + Math.max((scroll.visibleLines / scroll.totalLines) * trackHeight, 20), + trackHeight, + ); + const maxOffset = scroll.totalLines - scroll.visibleLines; + const thumbTop = + maxOffset > 0 ? (1 - scroll.displayOffset / maxOffset) * (trackHeight - thumbHeight) : 0; + const scrollPaint: SkPaint = Skia.Paint(); + scrollPaint.setColor(skColor(0x40ffffff)); + scrollPaint.setStyle(PaintStyle.Fill); + canvas.drawRRect( + Skia.RRectXY(Skia.XYWHRect(width - 4, thumbTop, 3, thumbHeight), 1.5, 1.5), + scrollPaint, + ); + } +} + +// ── Component ───────────────────────────────────────────────────────────────── + +export const TerminalView: React.FC = ({ + native, + connId, + terminalId, + fonts, + fontSize = TerminalTheme.defaultFontSize, + selecting = false, + onGridSizeChange, +}) => { + const [size, setSize] = useState<{ w: number; h: number } | null>(null); + // Bumped to force the picture to recompute (the actual cell data lives in + // native; this is just a repaint trigger gated on isDirty()). + const [frame, setFrame] = useState(0); + + const colsRef = useRef(0); + const rowsRef = useRef(0); + const resizeTimer = useRef | null>(null); + const initialResizeSent = useRef(false); + + // Resize the font in-place so metrics match the requested size. + useMemo(() => { + fonts.regular.setSize(fontSize); + fonts.bold?.setSize(fontSize); + fonts.italic?.setSize(fontSize); + fonts.boldItalic?.setSize(fontSize); + }, [fonts, fontSize]); + + const metrics = useMemo(() => measureCell(fonts.regular), [fonts, fontSize]); + + // ── Layout → cols/rows → resizeLocal + debounced resizeTerminal ────────── + const onLayout = useCallback( + (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout; + if (width <= 0 || height <= 0) return; + setSize({ w: width, h: height }); + + const { cellWidth, cellHeight } = metrics; + if (cellWidth <= 0 || cellHeight <= 0) return; + + const newCols = Math.min(Math.max(Math.floor(width / cellWidth), 1), 500); + const newRows = Math.min(Math.max(Math.floor(height / cellHeight), 1), 200); + + if (newCols !== colsRef.current || newRows !== rowsRef.current) { + colsRef.current = newCols; + rowsRef.current = newRows; + + // Immediate local resize for responsive rendering (no WS round-trip). + native.resizeLocal(connId, terminalId, newCols, newRows); + onGridSizeChange?.(newCols, newRows); + + if (!initialResizeSent.current) { + // First resize fires immediately — avoids a flash of garbled content. + initialResizeSent.current = true; + if (resizeTimer.current) clearTimeout(resizeTimer.current); + native.resizeTerminal(connId, terminalId, newCols, newRows); + } else { + // Debounce subsequent resizes (200ms), as in terminal_view.dart. + if (resizeTimer.current) clearTimeout(resizeTimer.current); + resizeTimer.current = setTimeout(() => { + native.resizeTerminal(connId, terminalId, newCols, newRows); + }, 200); + } + } + }, + [native, connId, terminalId, metrics, onGridSizeChange], + ); + + // Reset resize/init state when the target terminal changes. + useEffect(() => { + initialResizeSent.current = false; + colsRef.current = 0; + rowsRef.current = 0; + }, [connId, terminalId]); + + // ── Repaint loop: rAF gated on isDirty() (Decision C) ──────────────────── + useEffect(() => { + let raf = 0; + let mounted = true; + const tick = () => { + if (!mounted) return; + // Repaint when native reports new output. We always repaint on the very + // first tick (frame===0 picture) so initial content shows. + if (native.isDirty(connId, terminalId)) { + setFrame((f) => f + 1); + } + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => { + mounted = false; + cancelAnimationFrame(raf); + }; + }, [native, connId, terminalId]); + + useEffect(() => { + return () => { + if (resizeTimer.current) clearTimeout(resizeTimer.current); + }; + }, []); + + // ── Build the SkPicture from the packed buffer ─────────────────────────── + const picture = useMemo(() => { + if (!size) return null; + // Read fresh native state for this frame. + let cells: PackedCells; + try { + cells = decodeCellsView(native.getVisibleCellsPacked(connId, terminalId)); + } catch { + // Native not ready / empty buffer — draw nothing this frame. + return null; + } + const cursor = native.getCursor(connId, terminalId); + const scroll = native.getScrollInfo(connId, terminalId); + const selection = selecting + ? native.getSelectionBounds(connId, terminalId) + : undefined; + + return createPicture( + (canvas) => + paintTerminal({ + canvas, + cells, + cursor, + scroll, + selection, + metrics, + fonts, + fontSize, + width: size.w, + height: size.h, + }), + Skia.XYWHRect(0, 0, size.w, size.h), + ); + // `frame` participates so the picture recomputes each dirty tick. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [size, frame, native, connId, terminalId, selecting, metrics, fonts, fontSize]); + + return ( + + {picture ? ( + + + + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: TerminalTheme.bgColor, + }, +}); + +export default TerminalView; diff --git a/mobile/rn/src/native/cells.ts b/mobile/rn/src/native/cells.ts new file mode 100644 index 00000000..d8ae5813 --- /dev/null +++ b/mobile/rn/src/native/cells.ts @@ -0,0 +1,206 @@ +/** + * cells.ts — decoder for the packed visible-cell buffer. + * + * SCAFFOLD NOTE: this matches a binary format produced by a Rust FFI function + * (`getVisibleCellsPacked`) that is being ADDED to `crates/okena-mobile-ffi` + * as part of the migration (it does not exist in `mobile/native` yet). This + * file is the authoritative spec of that wire format; the Rust encoder must + * produce exactly these bytes. + * + * Format (little-endian throughout): + * + * ┌─ header (4 bytes) ─────────────────────────────────────────────┐ + * │ cols : u16 │ + * │ rows : u16 │ + * ├─ then cols*rows cells, row-major, 13 bytes each ───────────────┤ + * │ codepoint : u32 Unicode scalar value (0x20 == space) │ + * │ fg : u32 ARGB packed: 0xAARRGGBB │ + * │ bg : u32 ARGB packed: 0xAARRGGBB │ + * │ flags : u8 bitmask (see FLAG_* below) │ + * └────────────────────────────────────────────────────────────────┘ + * + * Total length = 4 + cols*rows*13 bytes. + */ + +// ── Style flag bits (must match Rust `CellData.flags` in api/terminal.rs) ── + +export const FLAG_BOLD = 1; +export const FLAG_ITALIC = 2; +export const FLAG_UNDERLINE = 4; +export const FLAG_STRIKETHROUGH = 8; +export const FLAG_INVERSE = 16; +export const FLAG_DIM = 32; + +/** + * Flags that affect glyph styling (everything except INVERSE, which is handled + * separately by swapping fg/bg). Mirrors `_kStyleMask` in terminal_painter.dart. + */ +export const FLAG_STYLE_MASK = + FLAG_BOLD | FLAG_ITALIC | FLAG_UNDERLINE | FLAG_STRIKETHROUGH | FLAG_DIM; + +// ── Byte-layout constants ────────────────────────────────────────────────── + +/** Header size in bytes (cols:u16 + rows:u16). */ +export const HEADER_BYTES = 4; +/** Size of one packed cell in bytes (u32 + u32 + u32 + u8). */ +export const CELL_BYTES = 13; + +const OFFSET_CODEPOINT = 0; +const OFFSET_FG = 4; +const OFFSET_BG = 8; +const OFFSET_FLAGS = 12; + +// ── ARGB helpers ──────────────────────────────────────────────────────────── + +export interface Argb { + a: number; + r: number; + g: number; + b: number; +} + +/** Unpack a `0xAARRGGBB` u32 into channel components (0–255 each). */ +export function argbToChannels(argb: number): Argb { + // `>>> 0` keeps it an unsigned 32-bit value. + const v = argb >>> 0; + return { + a: (v >>> 24) & 0xff, + r: (v >>> 16) & 0xff, + g: (v >>> 8) & 0xff, + b: v & 0xff, + }; +} + +/** + * Convert a `0xAARRGGBB` u32 to a CSS `rgba()` string. Handy for non-Skia + * consumers / tests; the Skia renderer builds `Float32Array` colors directly. + */ +export function argbToCss(argb: number): string { + const { a, r, g, b } = argbToChannels(argb); + return `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(4)})`; +} + +// ── Decoded representations ────────────────────────────────────────────────── + +/** A single decoded cell. */ +export interface DecodedCell { + /** Unicode scalar value; `0x20` is a space / blank cell. */ + codepoint: number; + /** Foreground, ARGB packed `0xAARRGGBB`. */ + fg: number; + /** Background, ARGB packed `0xAARRGGBB`. */ + bg: number; + /** Style flags bitmask. */ + flags: number; +} + +/** Object form of a decoded grid (convenient; allocates per cell). */ +export interface DecodedGrid { + cols: number; + rows: number; + cells: DecodedCell[]; +} + +/** + * Decode the packed buffer into a `{ cols, rows, cells }` object. + * + * Allocates one `DecodedCell` object per cell — fine for tests and incidental + * use. For the per-frame render hot path prefer `decodeCellsView`, which wraps + * the same bytes in typed-array views with zero per-cell allocation. + */ +export function decodeCells(buf: ArrayBuffer): DecodedGrid { + const view = new DataView(buf); + const cols = view.getUint16(0, /* littleEndian */ true); + const rows = view.getUint16(2, true); + const count = cols * rows; + + const expected = HEADER_BYTES + count * CELL_BYTES; + if (buf.byteLength < expected) { + throw new RangeError( + `packed cell buffer too short: have ${buf.byteLength} bytes, ` + + `need ${expected} for ${cols}x${rows}`, + ); + } + + const cells: DecodedCell[] = new Array(count); + let off = HEADER_BYTES; + for (let i = 0; i < count; i++) { + cells[i] = { + codepoint: view.getUint32(off + OFFSET_CODEPOINT, true), + fg: view.getUint32(off + OFFSET_FG, true), + bg: view.getUint32(off + OFFSET_BG, true), + flags: view.getUint8(off + OFFSET_FLAGS), + }; + off += CELL_BYTES; + } + + return { cols, rows, cells }; +} + +/** + * Zero-allocation accessor over the packed buffer for the render hot path. + * + * Holds a single `DataView` and exposes per-cell field getters indexed by + * `i = row * cols + col`. No `DecodedCell` objects are created, so painting a + * full frame allocates nothing beyond this wrapper. + */ +export class PackedCells { + readonly cols: number; + readonly rows: number; + readonly count: number; + private readonly view: DataView; + + constructor(buf: ArrayBuffer) { + const view = new DataView(buf); + this.cols = view.getUint16(0, true); + this.rows = view.getUint16(2, true); + this.count = this.cols * this.rows; + const expected = HEADER_BYTES + this.count * CELL_BYTES; + if (buf.byteLength < expected) { + throw new RangeError( + `packed cell buffer too short: have ${buf.byteLength} bytes, ` + + `need ${expected} for ${this.cols}x${this.rows}`, + ); + } + this.view = view; + } + + private cellOffset(i: number): number { + return HEADER_BYTES + i * CELL_BYTES; + } + + codepoint(i: number): number { + return this.view.getUint32(this.cellOffset(i) + OFFSET_CODEPOINT, true); + } + + fg(i: number): number { + return this.view.getUint32(this.cellOffset(i) + OFFSET_FG, true); + } + + bg(i: number): number { + return this.view.getUint32(this.cellOffset(i) + OFFSET_BG, true); + } + + flags(i: number): number { + return this.view.getUint8(this.cellOffset(i) + OFFSET_FLAGS); + } + + /** The character at cell `i` as a JS string (handles astral codepoints). */ + char(i: number): string { + return String.fromCodePoint(this.codepoint(i)); + } + + /** True if the cell is empty (space). */ + isBlank(i: number): boolean { + const cp = this.codepoint(i); + return cp === 0x20 || cp === 0; + } +} + +/** + * Wrap a packed buffer in a {@link PackedCells} view (zero per-cell alloc). + * The render path should use this; `decodeCells` is the convenience form. + */ +export function decodeCellsView(buf: ArrayBuffer): PackedCells { + return new PackedCells(buf); +} diff --git a/mobile/rn/src/native/okena.ts b/mobile/rn/src/native/okena.ts new file mode 100644 index 00000000..44c25f8e --- /dev/null +++ b/mobile/rn/src/native/okena.ts @@ -0,0 +1,442 @@ +/** + * okena.ts — the native ↔ TypeScript binding contract. + * + * ┌───────────────────────────────────────────────────────────────────────┐ + * │ THIS IS A SCAFFOLD / SPEC, NOT THE REAL BINDING. │ + * │ │ + * │ The real implementation is GENERATED by `uniffi-bindgen-react-native` │ + * │ (`ubrn`) from the sibling Rust crate `crates/okena-mobile-ffi` (the │ + * │ uniffi-annotated rework of `mobile/native`). See ../../README.md. │ + * │ │ + * │ This file declares the agreed-upon TypeScript shape of that generated │ + * │ module: the `OkenaNative` interface plus all the record/enum types. │ + * │ Both sides — the Rust `#[uniffi::export]` signatures and this contract │ + * │ — must stay in sync. When `ubrn` is wired up, the generated package's │ + * │ exported functions should satisfy `OkenaNative` structurally (this file │ + * │ becomes the typed re-export / shim over the generated module). │ + * └───────────────────────────────────────────────────────────────────────┘ + * + * Signature source of truth: `mobile/native/src/api/{connection,terminal,state}.rs`. + * + * Sync vs. async mapping (mirrors the Rust `#[frb(sync)]` / `async fn` split, + * which `ubrn` maps to synchronous JSI host functions vs. JS Promises): + * - SYNC : cheap getters on the render hot path + * (getVisibleCells, getVisibleCellsPacked, getCursor, isDirty, + * connectionStatus, getScrollInfo, selection getters, resize*). + * - ASYNC : anything that crosses the wire / awaits a server response + * (pair, sendText, sendSpecialKey, all *_action / git / service / + * project / layout calls, readContent). + */ + +// ───────────────────────────────────────────────────────────────────────── +// Scalar / id aliases (documentation only — all are `string`/`number` at runtime) +// ───────────────────────────────────────────────────────────────────────── + +/** Opaque connection id returned by `connect`. */ +export type ConnId = string; +/** Opaque terminal id (from layout / state). */ +export type TerminalId = string; +/** Opaque project id. */ +export type ProjectId = string; +/** Opaque folder id. */ +export type FolderId = string; + +// ───────────────────────────────────────────────────────────────────────── +// Enums (uniffi enums → TS string-union; ubrn emits string-tagged values) +// ───────────────────────────────────────────────────────────────────────── + +/** + * Connection status — mirrors the FFI `ConnectionStatus` enum in + * `api/connection.rs` (the simplified mobile variant; `Reconnecting` is + * collapsed into `Connecting` Rust-side). + * + * `Error` carries a message, so this is a discriminated union rather than a + * bare string-union. + */ +export type ConnectionStatus = + | { kind: 'disconnected' } + | { kind: 'connecting' } + | { kind: 'connected' } + | { kind: 'pairing' } + | { kind: 'error'; message: string }; + +/** Cursor shape — mirrors FFI `CursorShape` (`api/terminal.rs`). */ +export type CursorShape = 'block' | 'underline' | 'beam'; + +/** + * Named special keys the remote API accepts, mirroring + * `okena_core::keys::SpecialKey`. `sendSpecialKey` takes one of these + * (serialized to the same JSON variant name Rust expects). + */ +export type SpecialKey = + | 'Enter' + | 'Escape' + | 'Backspace' + | 'Delete' + | 'CtrlC' + | 'CtrlD' + | 'CtrlZ' + | 'Tab' + | 'ArrowUp' + | 'ArrowDown' + | 'ArrowLeft' + | 'ArrowRight' + | 'Home' + | 'End' + | 'PageUp' + | 'PageDown'; + +/** Diff mode for the git diff / file-contents calls. */ +export type DiffMode = 'working_tree' | 'staged'; + +/** Split direction for `splitTerminal`. */ +export type SplitDirection = 'horizontal' | 'vertical'; + +// ───────────────────────────────────────────────────────────────────────── +// Records (uniffi records → TS interfaces) +// ───────────────────────────────────────────────────────────────────────── + +/** + * A single rendered terminal cell — mirrors FFI `CellData` (`api/terminal.rs`). + * + * NOTE: For the render hot path prefer `getVisibleCellsPacked` (see below) + + * the `decodeCells` reader in `./cells`, which avoids marshalling thousands of + * these objects across JSI per frame (Decision C in RN_MIGRATION.md §2). + */ +export interface CellData { + /** The grapheme in this cell (`" "` for blank). */ + character: string; + /** Foreground color, ARGB packed: `0xAARRGGBB`. */ + fg: number; + /** Background color, ARGB packed: `0xAARRGGBB`. */ + bg: number; + /** Style flags bitmask: bold|italic|underline|strikethrough|inverse|dim. See ./cells. */ + flags: number; +} + +/** Cursor state — mirrors FFI `CursorState` (`api/terminal.rs`). */ +export interface CursorState { + /** 0-based column. */ + col: number; + /** 0-based visual row. */ + row: number; + shape: CursorShape; + visible: boolean; +} + +/** Scroll info — mirrors FFI `ScrollInfo` (`api/terminal.rs`). */ +export interface ScrollInfo { + /** Total lines in the scrollback + screen. */ + totalLines: number; + /** Number of visible (on-screen) lines. */ + visibleLines: number; + /** Distance scrolled back from the bottom; 0 == pinned to bottom. */ + displayOffset: number; +} + +/** + * Selection bounds in buffer coordinates — mirrors FFI `SelectionBounds` + * (`api/terminal.rs`). Rows are `i32` Rust-side (can be negative in scrollback), + * cols are `u16`. + */ +export interface SelectionBounds { + startCol: number; + /** Buffer row; may be negative (scrollback above the screen). */ + startRow: number; + endCol: number; + /** Buffer row; may be negative. */ + endRow: number; +} + +/** Service info — mirrors FFI `ServiceInfo` (`api/state.rs`). */ +export interface ServiceInfo { + name: string; + status: string; + /** Terminal hosting the service, if running. */ + terminalId?: string; + ports: number[]; + exitCode?: number; + kind: string; + isExtra: boolean; +} + +/** Project info — mirrors FFI `ProjectInfo` (`api/state.rs`). */ +export interface ProjectInfo { + id: ProjectId; + name: string; + path: string; + showInOverview: boolean; + terminalIds: TerminalId[]; + /** Map terminalId → display name. */ + terminalNames: Record; + gitBranch?: string; + gitLinesAdded: number; + gitLinesRemoved: number; + services: ServiceInfo[]; + /** Lowercased folder color name (e.g. `"blue"`). */ + folderColor: string; +} + +/** Folder info — mirrors FFI `FolderInfo` (`api/state.rs`). */ +export interface FolderInfo { + id: FolderId; + name: string; + projectIds: ProjectId[]; + /** Lowercased folder color name. */ + folderColor: string; +} + +/** Fullscreen terminal info — mirrors FFI `FullscreenInfo` (`api/state.rs`). */ +export interface FullscreenInfo { + projectId: ProjectId; + terminalId: TerminalId; +} + +// ───────────────────────────────────────────────────────────────────────── +// The binding contract +// ───────────────────────────────────────────────────────────────────────── + +/** + * The full native surface — one method per exported Rust FFI function. + * + * Naming: Rust `snake_case` fns map to camelCase here, which is also what + * `ubrn` emits for the generated JS bindings. `connId`/`terminalId`/etc. are + * always passed positionally (uniffi has no keyword args, unlike `frb`). + */ +export interface OkenaNative { + // ── connection.rs ────────────────────────────────────────────────────── + + /** One-time startup init (sets up the ConnectionManager singleton). */ + initApp(): void; + + /** + * Open a connection to a remote server, returning its conn id. If a saved + * token is supplied, pairing is skipped. (sync — just registers + kicks off + * the connect; status is then polled via `connectionStatus`.) + */ + connect(host: string, port: number, savedToken: string | undefined): ConnId; + + /** Current auth token for a connection, if paired. */ + getToken(connId: ConnId): string | undefined; + + /** Pair with the server using a pairing code (async — awaits the server). */ + pair(connId: ConnId, code: string): Promise; + + /** Close the WS and clean up. */ + disconnect(connId: ConnId): void; + + /** Current connection status. */ + connectionStatus(connId: ConnId): ConnectionStatus; + + /** + * Seconds since the last WS activity (terminal output). Returns a large + * value if the connection doesn't exist. Used for the staleness indicator. + */ + secondsSinceActivity(connId: ConnId): number; + + // ── terminal.rs ──────────────────────────────────────────────────────── + + /** + * Visible grid as one record per cell. Convenient for non-hot callers; for + * the renderer use `getVisibleCellsPacked` instead. + */ + getVisibleCells(connId: ConnId, terminalId: TerminalId): CellData[]; + + /** + * Visible grid as a packed little-endian binary buffer — the render hot path + * (Decision C). Layout: `cols:u16, rows:u16` header, then `cols*rows` cells + * of 13 bytes each (`codepoint:u32, fg:u32, bg:u32, flags:u8`). Decode with + * `decodeCells` from `./cells`. + * + * NOTE: This function does NOT yet exist in `mobile/native/src/api/*.rs`; it + * is being ADDED Rust-side as part of the migration (`crates/okena-mobile-ffi`, + * Phase 1). The exact byte format here is the contract for that addition. + */ + getVisibleCellsPacked(connId: ConnId, terminalId: TerminalId): ArrayBuffer; + + /** Current cursor state. */ + getCursor(connId: ConnId, terminalId: TerminalId): CursorState; + + /** Scroll the display: positive = up (into scrollback), negative = down. */ + scroll(connId: ConnId, terminalId: TerminalId, delta: number): void; + + /** Total/visible line counts and the current display offset. */ + getScrollInfo(connId: ConnId, terminalId: TerminalId): ScrollInfo; + + /** Begin a character-level selection at the given cell. */ + startSelection(connId: ConnId, terminalId: TerminalId, col: number, row: number): void; + + /** Begin a word (semantic) selection at the given cell. */ + startWordSelection(connId: ConnId, terminalId: TerminalId, col: number, row: number): void; + + /** Extend the active selection to the given cell. */ + updateSelection(connId: ConnId, terminalId: TerminalId, col: number, row: number): void; + + /** Clear the active selection. */ + clearSelection(connId: ConnId, terminalId: TerminalId): void; + + /** The currently selected text, if any. */ + getSelectedText(connId: ConnId, terminalId: TerminalId): string | undefined; + + /** Selection bounds for rendering the highlight overlay. */ + getSelectionBounds(connId: ConnId, terminalId: TerminalId): SelectionBounds | undefined; + + /** Send raw text input (async — goes over the WS). */ + sendText(connId: ConnId, terminalId: TerminalId, text: string): Promise; + + /** Resize the terminal locally AND tell the server (async semantics). */ + resizeTerminal(connId: ConnId, terminalId: TerminalId, cols: number, rows: number): void; + + /** + * Resize ONLY the local alacritty grid — does not notify the server. Used to + * adapt the mobile viewport to the server's size without round-tripping. + */ + resizeLocal(connId: ConnId, terminalId: TerminalId, cols: number, rows: number): void; + + // ── state.rs ─────────────────────────────────────────────────────────── + + /** All projects from the cached remote state. */ + getProjects(connId: ConnId): ProjectInfo[]; + + /** The server's focused project id, if any. */ + getFocusedProjectId(connId: ConnId): ProjectId | undefined; + + /** All folders from the cached remote state. */ + getFolders(connId: ConnId): FolderInfo[]; + + /** The server's project ordering. */ + getProjectOrder(connId: ConnId): ProjectId[]; + + /** The fullscreen terminal, if one is active. */ + getFullscreenTerminal(connId: ConnId): FullscreenInfo | undefined; + + /** Layout tree for a project, serialized as JSON (`ApiLayoutNode`). */ + getProjectLayoutJson(connId: ConnId, projectId: ProjectId): string | undefined; + + /** Whether a terminal has unread output since the last cell fetch. */ + isDirty(connId: ConnId, terminalId: TerminalId): boolean; + + /** Send a named special key (async — goes over the WS). */ + sendSpecialKey(connId: ConnId, terminalId: TerminalId, key: SpecialKey): Promise; + + /** Flat list of every terminal id across all projects. */ + getAllTerminalIds(connId: ConnId): TerminalId[]; + + // ── terminal actions (state.rs, async) ─────────────────────────────────── + + createTerminal(connId: ConnId, projectId: ProjectId): Promise; + closeTerminal(connId: ConnId, projectId: ProjectId, terminalId: TerminalId): Promise; + closeTerminals(connId: ConnId, projectId: ProjectId, terminalIds: TerminalId[]): Promise; + renameTerminal( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId, + name: string, + ): Promise; + focusTerminal(connId: ConnId, projectId: ProjectId, terminalId: TerminalId): Promise; + toggleMinimized(connId: ConnId, projectId: ProjectId, terminalId: TerminalId): Promise; + /** Set or clear (pass `undefined`) the fullscreen terminal. */ + setFullscreen( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId | undefined, + ): Promise; + splitTerminal( + connId: ConnId, + projectId: ProjectId, + path: number[], + direction: SplitDirection, + ): Promise; + /** Run a command and auto-press Enter. */ + runCommand(connId: ConnId, terminalId: TerminalId, command: string): Promise; + /** Read full terminal content as text (awaits a server response). */ + readContent(connId: ConnId, terminalId: TerminalId): Promise; + + // ── git actions (state.rs, async — return JSON strings) ────────────────── + + gitStatus(connId: ConnId, projectId: ProjectId): Promise; + gitDiffSummary(connId: ConnId, projectId: ProjectId): Promise; + gitDiff(connId: ConnId, projectId: ProjectId, mode: DiffMode): Promise; + gitBranches(connId: ConnId, projectId: ProjectId): Promise; + gitFileContents( + connId: ConnId, + projectId: ProjectId, + filePath: string, + mode: DiffMode, + ): Promise; + + // ── service actions (state.rs, async) ──────────────────────────────────── + + startService(connId: ConnId, projectId: ProjectId, serviceName: string): Promise; + stopService(connId: ConnId, projectId: ProjectId, serviceName: string): Promise; + restartService(connId: ConnId, projectId: ProjectId, serviceName: string): Promise; + startAllServices(connId: ConnId, projectId: ProjectId): Promise; + stopAllServices(connId: ConnId, projectId: ProjectId): Promise; + reloadServices(connId: ConnId, projectId: ProjectId): Promise; + + // ── project management (state.rs, async) ───────────────────────────────── + + addProject(connId: ConnId, name: string, path: string): Promise; + setProjectColor(connId: ConnId, projectId: ProjectId, color: string): Promise; + setFolderColor(connId: ConnId, folderId: FolderId, color: string): Promise; + reorderProjectInFolder( + connId: ConnId, + folderId: FolderId, + projectId: ProjectId, + newIndex: number, + ): Promise; + + // ── layout actions (state.rs, async) ───────────────────────────────────── + + updateSplitSizes( + connId: ConnId, + projectId: ProjectId, + path: number[], + sizes: number[], + ): Promise; + addTab(connId: ConnId, projectId: ProjectId, path: number[], inGroup: boolean): Promise; + setActiveTab(connId: ConnId, projectId: ProjectId, path: number[], index: number): Promise; + moveTab( + connId: ConnId, + projectId: ProjectId, + path: number[], + fromIndex: number, + toIndex: number, + ): Promise; + moveTerminalToTabGroup( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId, + targetPath: number[], + position: number | undefined, + targetProjectId: ProjectId | undefined, + ): Promise; + movePaneTo( + connId: ConnId, + projectId: ProjectId, + terminalId: TerminalId, + targetProjectId: ProjectId, + targetTerminalId: TerminalId, + zone: string, + ): Promise; +} + +/** + * Resolve the real native module once `ubrn` has generated it. + * + * Replace the body with the generated package import, e.g.: + * + * import * as gen from 'okena-mobile-ffi'; // ubrn output package name + * export const Okena: OkenaNative = gen as unknown as OkenaNative; + * + * Until then this throws, so the typed contract compiles and the + * presentational components (which accept the module via props) stay testable + * with a mock implementing `OkenaNative`. + */ +export function getOkenaNative(): OkenaNative { + throw new Error( + 'okena native module not wired up yet — generate it with `ubrn` from ' + + 'crates/okena-mobile-ffi and replace getOkenaNative() in src/native/okena.ts. ' + + 'See mobile/rn/README.md.', + ); +} diff --git a/mobile/rn/src/theme.ts b/mobile/rn/src/theme.ts new file mode 100644 index 00000000..190d83ae --- /dev/null +++ b/mobile/rn/src/theme.ts @@ -0,0 +1,107 @@ +/** + * theme.ts — colors and typography, ported from + * `mobile/lib/src/theme/app_theme.dart`. + * + * Dart `Color(0xAARRGGBB)` values are kept verbatim as packed ARGB numbers in + * `argb`, and also exposed as RN-friendly `#RRGGBB[AA]` hex strings (RN's + * `color` props want `#RRGGBBAA`, NOT `#AARRGGBB`). + */ + +// ── helpers ────────────────────────────────────────────────────────────── + +/** `0xAARRGGBB` (Flutter order) → `#RRGGBBAA` (RN/CSS order). */ +function argbToHex(argb: number): string { + const v = argb >>> 0; + const a = (v >>> 24) & 0xff; + const r = (v >>> 16) & 0xff; + const g = (v >>> 8) & 0xff; + const b = v & 0xff; + const h = (n: number) => n.toString(16).padStart(2, '0'); + return `#${h(r)}${h(g)}${h(b)}${h(a)}`; +} + +// ── Color system (mirrors OkenaColors in app_theme.dart) ─────────────────── + +const OkenaColorsArgb = { + // Backgrounds + background: 0xff000000, + surface: 0xff0a0a0a, + surfaceElevated: 0xff161616, + surfaceOverlay: 0xff1c1c1c, + // Borders + border: 0xff1e1e1e, + borderLight: 0xff2a2a2a, + // Accent + accent: 0xff7c7fff, + // Text + textPrimary: 0xffe8e8ec, + textSecondary: 0xff98989f, + textTertiary: 0xff5a5a62, + // Semantic + success: 0xff4ade80, + warning: 0xfffbbf24, + error: 0xfff87171, + // Glass + glassBg: 0xcc0a0a0a, + glassStroke: 0x18ffffff, + // Key toolbar + keyBg: 0xff161616, + keyBorder: 0xff2a2a2a, + keyText: 0xffb0b0b8, +} as const; + +type ColorName = keyof typeof OkenaColorsArgb; + +/** Packed ARGB form (`0xAARRGGBB`) — matches the Dart `Color(...)` literals. */ +export const OkenaColorsArgbMap: Readonly> = OkenaColorsArgb; + +/** RN/CSS hex form (`#RRGGBBAA`). */ +export const OkenaColors: Readonly> = Object.fromEntries( + (Object.keys(OkenaColorsArgb) as ColorName[]).map((k) => [k, argbToHex(OkenaColorsArgb[k])]), +) as Record; + +// ── Terminal theme (mirrors TerminalTheme in app_theme.dart) ─────────────── + +export const TerminalTheme = { + /** Must match the loaded font family name (see README font-linking step). */ + fontFamily: 'JetBrainsMono', + fontFamilyFallback: ['Menlo', 'Consolas', 'DejaVu Sans Mono', 'monospace'] as const, + + defaultFontSize: 13, + minFontSize: 6, + maxFontSize: 24, + defaultColumns: 80, + lineHeightFactor: 1.2, + + // Packed ARGB (for buffer comparisons against cell bg) + hex (for paints). + bgColorArgb: 0xff000000, + fgColorArgb: 0xffcdd6f4, + cursorColorArgb: 0xfff5e0dc, + selectionColorArgb: 0x40585b70, + + bgColor: argbToHex(0xff000000), + fgColor: argbToHex(0xffcdd6f4), + cursorColor: argbToHex(0xfff5e0dc), + selectionColor: argbToHex(0x40585b70), + + /** Selection highlight overlay, matches `0x40264F78` in terminal_painter.dart. */ + selectionOverlayArgb: 0x40264f78, + selectionOverlay: argbToHex(0x40264f78), +} as const; + +// ── Typography (mirrors OkenaTypography; RN uses system font for `.SF Pro`) ── + +/** + * On iOS, `System` resolves to SF Pro. On Android there is no SF Pro, so this + * falls back to the platform default (Roboto) — acceptable for the chrome. + */ +export const OkenaTypography = { + fontFamily: 'System', + largeTitle: { fontSize: 28, fontWeight: '700', letterSpacing: -0.5, color: OkenaColors.textPrimary }, + title: { fontSize: 20, fontWeight: '600', letterSpacing: -0.3, color: OkenaColors.textPrimary }, + headline: { fontSize: 17, fontWeight: '600', color: OkenaColors.textPrimary }, + body: { fontSize: 15, fontWeight: '400', color: OkenaColors.textPrimary }, + callout: { fontSize: 14, fontWeight: '400', color: OkenaColors.textSecondary }, + caption: { fontSize: 12, fontWeight: '500', color: OkenaColors.textSecondary }, + caption2: { fontSize: 11, fontWeight: '500', color: OkenaColors.textTertiary }, +} as const; diff --git a/mobile/rn/tsconfig.json b/mobile/rn/tsconfig.json new file mode 100644 index 00000000..e0d354a4 --- /dev/null +++ b/mobile/rn/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext", "DOM"], + "jsx": "react-jsx", + + "strict": true, + "noUncheckedIndexedAccess": false, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + + "noEmit": true, + + "types": ["react", "react-native"] + }, + "include": ["src"], + "exclude": ["node_modules"] +} From 3b101d4a95a124f9267a9eae36aa46c63ca1341c Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 8 Jun 2026 17:14:54 +0200 Subject: [PATCH 30/37] =?UTF-8?q?feat(mobile-rn):=20app=20shell=20?= =?UTF-8?q?=E2=80=94=20state,=20navigation,=20screens,=20drawer,=20toolbar?= =?UTF-8?q?,=20layout=20(Phase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the RN UI on the existing contract + Skia renderer: - Foundation: SavedServer + LayoutNode models (parseLayout mirrors the Flutter parser), zustand connection/workspace stores (polling cadence ported from the Flutter providers, native module injectable for tests), AsyncStorage-backed persistence behind a swappable interface, and a minimal state-driven router (no react-navigation native deps). App.tsx wires connection status → nav and drives workspace polling. - Screens: ServerList (list + add-server sheet), Pairing (connect→code→paired with TLS-fingerprint footnote), Workspace (app bar + drawer + layout + toolbar). - Widgets: ProjectDrawer (custom slide-in; projects/folders, add/reorder/color), KeyToolbar (ESC/TAB + sticky CTRL/ALT/CMD with control-char + CSI encoding, shared modifier store), LayoutRenderer (recursive split/tabs with portrait rotation), TerminalPane (hidden TextInput, tap-focus, scroll/selection gestures around the Skia TerminalView), StatusIndicator. Ported from mobile/lib/src/{models,providers,screens,widgets}. Verified: npm install + npx tsc --noEmit (strict) pass over all 22 RN source files. No device build here (no SDK / ubrn module) — see mobile/rn/README.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/rn/package-lock.json | 74 +- mobile/rn/package.json | 4 +- mobile/rn/src/App.tsx | 92 +++ mobile/rn/src/components/KeyToolbar.tsx | 648 ++++++++++++++++ mobile/rn/src/components/LayoutRenderer.tsx | 325 ++++++++ mobile/rn/src/components/ProjectDrawer.tsx | 774 +++++++++++++++++++ mobile/rn/src/components/StatusIndicator.tsx | 132 ++++ mobile/rn/src/components/TerminalPane.tsx | 417 ++++++++++ mobile/rn/src/models/index.ts | 25 + mobile/rn/src/models/layoutNode.ts | 126 +++ mobile/rn/src/models/savedServer.ts | 159 ++++ mobile/rn/src/navigation/index.ts | 15 + mobile/rn/src/navigation/navStore.ts | 95 +++ mobile/rn/src/screens/PairingScreen.tsx | 316 ++++++++ mobile/rn/src/screens/ServerListScreen.tsx | 432 +++++++++++ mobile/rn/src/screens/WorkspaceScreen.tsx | 411 ++++++++++ mobile/rn/src/state/connectionStore.ts | 354 +++++++++ mobile/rn/src/state/index.ts | 36 + mobile/rn/src/state/persistence.ts | 74 ++ mobile/rn/src/state/workspaceStore.ts | 336 ++++++++ 20 files changed, 4843 insertions(+), 2 deletions(-) create mode 100644 mobile/rn/src/App.tsx create mode 100644 mobile/rn/src/components/KeyToolbar.tsx create mode 100644 mobile/rn/src/components/LayoutRenderer.tsx create mode 100644 mobile/rn/src/components/ProjectDrawer.tsx create mode 100644 mobile/rn/src/components/StatusIndicator.tsx create mode 100644 mobile/rn/src/components/TerminalPane.tsx create mode 100644 mobile/rn/src/models/index.ts create mode 100644 mobile/rn/src/models/layoutNode.ts create mode 100644 mobile/rn/src/models/savedServer.ts create mode 100644 mobile/rn/src/navigation/index.ts create mode 100644 mobile/rn/src/navigation/navStore.ts create mode 100644 mobile/rn/src/screens/PairingScreen.tsx create mode 100644 mobile/rn/src/screens/ServerListScreen.tsx create mode 100644 mobile/rn/src/screens/WorkspaceScreen.tsx create mode 100644 mobile/rn/src/state/connectionStore.ts create mode 100644 mobile/rn/src/state/index.ts create mode 100644 mobile/rn/src/state/persistence.ts create mode 100644 mobile/rn/src/state/workspaceStore.ts diff --git a/mobile/rn/package-lock.json b/mobile/rn/package-lock.json index de66f19c..64daf0ca 100644 --- a/mobile/rn/package-lock.json +++ b/mobile/rn/package-lock.json @@ -8,9 +8,11 @@ "name": "okena-mobile-rn", "version": "0.0.0", "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.0", "@shopify/react-native-skia": "^1.5.0", "react": "18.3.1", - "react-native": "0.76.5" + "react-native": "0.76.5", + "zustand": "^4.5.5" }, "devDependencies": { "@types/react": "^18.3.12", @@ -2275,6 +2277,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.76.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", @@ -4020,6 +4034,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4511,6 +4534,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6222,6 +6257,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -6369,6 +6413,34 @@ "engines": { "node": ">=12" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/mobile/rn/package.json b/mobile/rn/package.json index b73270ab..a2f73bba 100644 --- a/mobile/rn/package.json +++ b/mobile/rn/package.json @@ -8,9 +8,11 @@ "lint": "eslint src --ext .ts,.tsx" }, "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.0", "@shopify/react-native-skia": "^1.5.0", "react": "18.3.1", - "react-native": "0.76.5" + "react-native": "0.76.5", + "zustand": "^4.5.5" }, "devDependencies": { "@types/react": "^18.3.12", diff --git a/mobile/rn/src/App.tsx b/mobile/rn/src/App.tsx new file mode 100644 index 00000000..9c55dc2a --- /dev/null +++ b/mobile/rn/src/App.tsx @@ -0,0 +1,92 @@ +/** + * App.tsx — the RN app root. + * + * Mirrors `mobile/lib/main.dart`'s `OkenaApp` + `AppRouter`, minus Flutter's + * MaterialApp theming (RN screens style themselves from `theme.ts`): + * - on mount: `initApp()`, load the persisted server list, bind connection + * state → navigation, and bridge connection status → workspace polling. + * - render: the screen the {@link useNavStore} currently points at. + * + * The store wiring is intentionally imperative (not React context) so the + * stores stay singletons usable from anywhere — including non-React code. + * + * The native module is resolved lazily by the stores. To run against a mock + * (off-device / tests), call `configureConnectionStore({ native, persistence })` + * and `configureWorkspaceStore({ native })` BEFORE mounting this component. + */ + +import React, { useEffect } from 'react'; +import { View, StyleSheet } from 'react-native'; + +import { useNavStore, bindConnectionToNavigation } from './navigation'; +import { + connectionStore, + selectIsConnected, + type ConnectionState, +} from './state/connectionStore'; +import { workspaceStore } from './state/workspaceStore'; +import { OkenaColors } from './theme'; + +import { ServerListScreen } from './screens/ServerListScreen'; +import { PairingScreen } from './screens/PairingScreen'; +import { WorkspaceScreen } from './screens/WorkspaceScreen'; + +/** + * Bridge the connection store to the workspace store's polling lifecycle: + * start polling (with the live `connId`) when connected, stop + clear when not. + * Mirrors the Dart `WorkspaceProvider._onConnectionChanged`. Returns an + * unsubscribe fn. + */ +function bindConnectionToWorkspace(): () => void { + const conn = connectionStore(); + const ws = workspaceStore(); + + const apply = (state: ConnectionState) => { + if (selectIsConnected(state) && state.connId) { + ws.getState().start(state.connId); + } else { + ws.getState().stop(); + } + }; + + apply(conn.getState()); + return conn.subscribe(apply); +} + +export const App: React.FC = () => { + const screen = useNavStore((s) => s.screen); + + useEffect(() => { + // One-time native init, routed through the store so a configured mock is + // honored. Resolving the native module throws if `ubrn` hasn't generated it + // yet — expected off-device; the mock path configures the stores first. + const conn = connectionStore().getState(); + conn.initApp(); + void conn.loadServers(); + + const unbindNav = bindConnectionToNavigation(); + const unbindWs = bindConnectionToWorkspace(); + return () => { + unbindNav(); + unbindWs(); + }; + }, []); + + return ( + + {screen === 'workspace' ? ( + + ) : screen === 'pairing' ? ( + + ) : ( + + )} + + ); +}; + +const styles = StyleSheet.create({ + root: { flex: 1, backgroundColor: OkenaColors.background }, +}); + +export default App; diff --git a/mobile/rn/src/components/KeyToolbar.tsx b/mobile/rn/src/components/KeyToolbar.tsx new file mode 100644 index 00000000..308c8435 --- /dev/null +++ b/mobile/rn/src/components/KeyToolbar.tsx @@ -0,0 +1,648 @@ +/** + * KeyToolbar.tsx — the key toolbar pinned above the soft keyboard. + * + * Port of `mobile/lib/src/widgets/key_toolbar.dart`: + * - ESC / TAB one-shot keys, + * - CTRL / ALT (option) / CMD sticky three-state toggles + * (inactive → active one-shot → locked → inactive), + * - a handful of punctuation keys (`~ | / -`), + * - an arrow joystick (pan to fire arrows; tap fires from offset-from-center), + * - compose-sheet + paste + hide-keyboard icon buttons. + * + * Modifier semantics (the heart of the port): + * - CTRL + a-z/A-Z → the control character (a→0x01 … z→0x1A). Other chars + * pass through verbatim. After the next key the one-shot (active) modifiers + * reset; locked ones persist. + * - OPTION/CMD + char → ESC-prefixed (`\x1b` + char), the xterm meta encoding. + * - Arrows with modifiers → `\x1b[1;` (mod = 1 + 4·ctrl + 2·option); + * CMD-only arrows map to Home/End/PageUp/PageDown. + * + * The {@link KeyModifiers} store is SHARED with {@link TerminalPane} (the soft + * keyboard input also consults it), exactly like the Dart `KeyModifiers` + * `ChangeNotifier` is shared between `KeyToolbar` and `TerminalView`. It is a + * tiny external store exposing a React hook ({@link useKeyModifiers}). + * + * Presentational + injected `native` (defaults to `getOkenaNative()`), mirroring + * `TerminalView`'s prop pattern. + */ + +import React, { + useCallback, + useEffect, + useRef, + useState, + useSyncExternalStore, +} from 'react'; +import { + View, + Text, + Pressable, + ScrollView, + Modal, + TextInput, + StyleSheet, + type GestureResponderEvent, +} from 'react-native'; + +import type { OkenaNative, SpecialKey } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { OkenaColors } from '../theme'; + +// ── Shared modifier state ────────────────────────────────────────────────── + +/** Three-state modifier cycle: inactive → active (one-shot) → locked (sticky). */ +export type ModifierState = 'inactive' | 'active' | 'locked'; + +interface ModifierSnapshot { + ctrl: ModifierState; + option: ModifierState; + cmd: ModifierState; +} + +const INITIAL_SNAPSHOT: ModifierSnapshot = { + ctrl: 'inactive', + option: 'inactive', + cmd: 'inactive', +}; + +function nextState(s: ModifierState): ModifierState { + switch (s) { + case 'inactive': + return 'active'; + case 'active': + return 'locked'; + case 'locked': + return 'inactive'; + } +} + +/** + * Shared modifier store between {@link KeyToolbar} and {@link TerminalPane}. + * + * Ports the Dart `KeyModifiers extends ChangeNotifier`. It is a minimal external + * store (subscribe + getSnapshot) so multiple components can subscribe via + * {@link useKeyModifiers} and stay in sync. A single immutable snapshot object is + * swapped on every change so `useSyncExternalStore` re-renders subscribers. + */ +export class KeyModifiers { + private snapshot: ModifierSnapshot = INITIAL_SNAPSHOT; + private readonly listeners = new Set<() => void>(); + + subscribe = (listener: () => void): (() => void) => { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + }; + + /** Returns the current immutable snapshot (stable identity until a change). */ + getSnapshot = (): ModifierSnapshot => this.snapshot; + + private emit(next: ModifierSnapshot): void { + this.snapshot = next; + for (const l of this.listeners) l(); + } + + get ctrl(): boolean { + return this.snapshot.ctrl !== 'inactive'; + } + get option(): boolean { + return this.snapshot.option !== 'inactive'; + } + get cmd(): boolean { + return this.snapshot.cmd !== 'inactive'; + } + get hasAny(): boolean { + return this.ctrl || this.option || this.cmd; + } + + toggleCtrl(): void { + this.emit({ ...this.snapshot, ctrl: nextState(this.snapshot.ctrl) }); + } + toggleOption(): void { + this.emit({ ...this.snapshot, option: nextState(this.snapshot.option) }); + } + toggleCmd(): void { + this.emit({ ...this.snapshot, cmd: nextState(this.snapshot.cmd) }); + } + + /** Reset only one-shot (active) modifiers; locked ones persist. */ + reset(): void { + const s = this.snapshot; + const changed = + s.ctrl === 'active' || s.option === 'active' || s.cmd === 'active'; + if (!changed) return; + this.emit({ + ctrl: s.ctrl === 'active' ? 'inactive' : s.ctrl, + option: s.option === 'active' ? 'inactive' : s.option, + cmd: s.cmd === 'active' ? 'inactive' : s.cmd, + }); + } +} + +/** Subscribe to a {@link KeyModifiers} store and re-render on changes. */ +export function useKeyModifiers(mod: KeyModifiers): ModifierSnapshot { + return useSyncExternalStore(mod.subscribe, mod.getSnapshot, mod.getSnapshot); +} + +// ── Control-char / meta encoding (shared with TerminalPane) ───────────────── + +/** + * Apply the active modifiers to a run of characters, returning the bytes to + * send. Mirrors the Dart `_applyModifiers` in terminal_view.dart (used for soft + * keyboard input) — CTRL maps a-z/A-Z to control chars and drops other chars; + * OPTION/CMD ESC-prefixes each char. + * + * Does NOT reset the modifiers (the caller does, after sending). + */ +export function applyModifiersToText(mod: KeyModifiers, chars: string): string { + if (!mod.hasAny) return chars; + let out = ''; + for (const ch of chars) { + const code = ch.charCodeAt(0); + if (mod.ctrl) { + if (code >= 0x61 && code <= 0x7a) { + out += String.fromCharCode(code - 0x60); + } else if (code >= 0x41 && code <= 0x5a) { + out += String.fromCharCode(code - 0x40); + } + // other chars are dropped under CTRL (matches Dart) + } else if (mod.option || mod.cmd) { + out += '\x1b' + ch; + } + } + return out; +} + +// ── Arrow encoding ─────────────────────────────────────────────────────────── + +type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight'; + +const ARROW_CHAR: Record = { + ArrowUp: 'A', + ArrowDown: 'B', + ArrowRight: 'C', + ArrowLeft: 'D', +}; + +// ── Props ───────────────────────────────────────────────────────────────── + +export interface KeyToolbarProps { + connId: string; + terminalId: string | null; + /** Shared modifier store (also consulted by {@link TerminalPane}). */ + modifiers: KeyModifiers; + /** Hide the soft keyboard (WorkspaceScreen wires this to blur the input). */ + onHideKeyboard?: () => void; + /** Injected native surface (defaults to `getOkenaNative()`). */ + native?: OkenaNative; +} + +// ── Component ───────────────────────────────────────────────────────────────── + +export const KeyToolbar: React.FC = ({ + connId, + terminalId, + modifiers, + onHideKeyboard, + native = getOkenaNative(), +}) => { + const mod = useKeyModifiers(modifiers); + const [composeOpen, setComposeOpen] = useState(false); + + const sendSpecialKey = useCallback( + (key: SpecialKey) => { + if (!terminalId) return; + void native.sendSpecialKey(connId, terminalId, key); + }, + [native, connId, terminalId], + ); + + const sendText = useCallback( + (text: string) => { + if (!terminalId || text.length === 0) return; + void native.sendText(connId, terminalId, text); + }, + [native, connId, terminalId], + ); + + /** Send a character key, applying any active modifiers (Dart `_sendCharKey`). */ + const sendCharKey = useCallback( + (char: string) => { + if (modifiers.hasAny) { + if (modifiers.ctrl) { + const code = char.charCodeAt(0); + if (code >= 0x61 && code <= 0x7a) { + sendText(String.fromCharCode(code - 0x60)); + } else if (code >= 0x41 && code <= 0x5a) { + sendText(String.fromCharCode(code - 0x40)); + } else { + sendText(char); + } + } else { + // Option/Cmd: ESC prefix. + sendText('\x1b' + char); + } + modifiers.reset(); + } else { + sendText(char); + } + }, + [modifiers, sendText], + ); + + /** Handle arrow from joystick, respecting modifier state (Dart `_handleArrow`). */ + const handleArrow = useCallback( + (key: ArrowKey) => { + const arrow = ARROW_CHAR[key]; + if (modifiers.hasAny) { + if (modifiers.cmd && !modifiers.ctrl && !modifiers.option) { + switch (key) { + case 'ArrowLeft': + sendSpecialKey('Home'); + break; + case 'ArrowRight': + sendSpecialKey('End'); + break; + case 'ArrowUp': + sendSpecialKey('PageUp'); + break; + case 'ArrowDown': + sendSpecialKey('PageDown'); + break; + } + } else { + let m = 1; + if (modifiers.ctrl) m += 4; + if (modifiers.option) m += 2; + sendText(`\x1b[1;${m}${arrow}`); + } + modifiers.reset(); + } else { + sendSpecialKey(key); + if (modifiers.hasAny) modifiers.reset(); + } + }, + [modifiers, sendSpecialKey, sendText], + ); + + return ( + + + sendSpecialKey('Escape')} /> + modifiers.toggleCtrl()} /> + modifiers.toggleOption()} /> + modifiers.toggleCmd()} /> + sendSpecialKey('Tab')} /> + + sendCharKey('~')} /> + sendCharKey('|')} /> + sendCharKey('/')} /> + sendCharKey('-')} /> + + setComposeOpen(true)} /> + onHideKeyboard?.()} /> + + + + + + setComposeOpen(false)} + onSubmit={(text, sendEnter) => { + sendText(text); + if (sendEnter) sendSpecialKey('Enter'); + }} + /> + + ); +}; + +// ── Key widgets ───────────────────────────────────────────────────────────── + +const KeyButton: React.FC<{ label: string; onPress: () => void }> = ({ + label, + onPress, +}) => ( + + {label} + +); + +const ToggleKey: React.FC<{ + label: string; + state: ModifierState; + onPress: () => void; +}> = ({ label, state, onPress }) => { + const active = state !== 'inactive'; + const locked = state === 'locked'; + return ( + + {label} + + + ); +}; + +// ── Arrow joystick ───────────────────────────────────────────────────────── + +const JOYSTICK_SIZE = 52; +const DRAG_THRESHOLD = 14; + +const ArrowJoystick: React.FC<{ onArrow: (key: ArrowKey) => void }> = ({ + onArrow, +}) => { + const originRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const movedRef = useRef(false); + const [active, setActive] = useState(null); + const clearTimer = useRef | null>(null); + + useEffect( + () => () => { + if (clearTimer.current) clearTimeout(clearTimer.current); + }, + [], + ); + + const dirFromDelta = (dx: number, dy: number): ArrowKey => + Math.abs(dx) > Math.abs(dy) + ? dx > 0 + ? 'ArrowRight' + : 'ArrowLeft' + : dy > 0 + ? 'ArrowDown' + : 'ArrowUp'; + + const fire = useCallback( + (dir: ArrowKey) => { + onArrow(dir); + setActive(dir); + }, + [onArrow], + ); + + const onResponderGrant = (e: GestureResponderEvent) => { + originRef.current = { + x: e.nativeEvent.locationX, + y: e.nativeEvent.locationY, + }; + movedRef.current = false; + }; + + const onResponderMove = (e: GestureResponderEvent) => { + const dx = e.nativeEvent.locationX - originRef.current.x; + const dy = e.nativeEvent.locationY - originRef.current.y; + if (Math.hypot(dx, dy) >= DRAG_THRESHOLD) { + movedRef.current = true; + fire(dirFromDelta(dx, dy)); + originRef.current = { + x: e.nativeEvent.locationX, + y: e.nativeEvent.locationY, + }; + } + }; + + const onResponderRelease = () => { + if (!movedRef.current) { + const cx = JOYSTICK_SIZE / 2; + const cy = JOYSTICK_SIZE / 2; + const dx = originRef.current.x - cx; + const dy = originRef.current.y - cy; + if (Math.hypot(dx, dy) >= 4) { + fire(dirFromDelta(dx, dy)); + if (clearTimer.current) clearTimeout(clearTimer.current); + clearTimer.current = setTimeout(() => setActive(null), 120); + return; + } + } + setActive(null); + }; + + return ( + true} + onMoveShouldSetResponder={() => true} + onResponderGrant={onResponderGrant} + onResponderMove={onResponderMove} + onResponderRelease={onResponderRelease} + onResponderTerminate={onResponderRelease} + > + + + {'▲'} + + + + {'◀'} + + + {'▶'} + + + + {'▼'} + + + + ); +}; + +// ── Compose sheet ───────────────────────────────────────────────────────── + +const ComposeSheet: React.FC<{ + visible: boolean; + onClose: () => void; + onSubmit: (text: string, sendEnter: boolean) => void; +}> = ({ visible, onClose, onSubmit }) => { + const [text, setText] = useState(''); + const [sendEnter, setSendEnter] = useState(true); + + useEffect(() => { + if (visible) setText(''); + }, [visible]); + + const submit = () => { + if (text.length === 0) { + onClose(); + return; + } + onSubmit(text, sendEnter); + onClose(); + }; + + return ( + + + + + setSendEnter((v) => !v)} + > + + {'⏎'} Enter + + + + + + + Cancel + + + Send + + + + + ); +}; + +// ── Styles ───────────────────────────────────────────────────────────────── + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 6, + paddingVertical: 5, + backgroundColor: OkenaColors.glassBg, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: OkenaColors.glassStroke, + }, + scroll: { flex: 1 }, + scrollContent: { alignItems: 'center' }, + gap: { width: 12 }, + key: { + minWidth: 40, + paddingHorizontal: 8, + paddingVertical: 9, + marginHorizontal: 2, + borderRadius: 10, + backgroundColor: OkenaColors.keyBg, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.keyBorder, + alignItems: 'center', + justifyContent: 'center', + }, + keyText: { + color: OkenaColors.keyText, + fontSize: 13, + fontWeight: '500', + }, + toggleKey: { paddingVertical: 7 }, + toggleKeyActive: { + backgroundColor: OkenaColors.accent, + borderColor: OkenaColors.accent, + }, + toggleKeyTextActive: { color: '#ffffff', fontWeight: '700', fontSize: 16 }, + lockBar: { + width: 12, + height: 2, + marginTop: 1, + borderRadius: 1, + backgroundColor: 'transparent', + }, + lockBarActive: { backgroundColor: '#ffffff' }, + arrowSlot: { marginLeft: 6 }, + joystick: { + width: JOYSTICK_SIZE, + height: JOYSTICK_SIZE, + borderRadius: 16, + backgroundColor: OkenaColors.keyBg, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.keyBorder, + alignItems: 'center', + justifyContent: 'center', + }, + joystickGrid: { alignItems: 'center', justifyContent: 'center' }, + arrowRow: { flexDirection: 'row', alignItems: 'center' }, + arrowGlyph: { + color: 'rgba(255,255,255,0.38)', + fontSize: 9, + marginHorizontal: 6, + marginVertical: 1, + }, + arrowGlyphActive: { color: OkenaColors.accent }, + // Compose sheet + composeBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }, + composeSheet: { + backgroundColor: OkenaColors.surface, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: 16, + }, + composeHeader: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginBottom: 8, + }, + enterToggle: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + backgroundColor: OkenaColors.surfaceElevated, + }, + enterToggleActive: { backgroundColor: OkenaColors.accent }, + enterToggleText: { + color: OkenaColors.textTertiary, + fontSize: 12, + fontFamily: 'JetBrainsMono', + }, + enterToggleTextActive: { color: '#ffffff' }, + composeInput: { + minHeight: 96, + color: OkenaColors.textPrimary, + fontFamily: 'JetBrainsMono', + fontSize: 14, + backgroundColor: OkenaColors.surfaceElevated, + borderRadius: 8, + padding: 12, + textAlignVertical: 'top', + }, + composeActions: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: 12, + }, + composeBtn: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + marginLeft: 8, + }, + composeBtnText: { color: OkenaColors.textSecondary, fontSize: 14 }, + composeSend: { backgroundColor: OkenaColors.accent }, + composeSendText: { color: '#ffffff', fontSize: 14, fontWeight: '600' }, +}); + +export default KeyToolbar; diff --git a/mobile/rn/src/components/LayoutRenderer.tsx b/mobile/rn/src/components/LayoutRenderer.tsx new file mode 100644 index 00000000..2177975a --- /dev/null +++ b/mobile/rn/src/components/LayoutRenderer.tsx @@ -0,0 +1,325 @@ +/** + * LayoutRenderer.tsx — recursive project-layout tree renderer. + * + * Port of `mobile/lib/src/widgets/layout_renderer.dart`. Walks a parsed + * {@link LayoutNode} tree (from `parseLayout(getProjectLayoutJson())`) and + * renders: + * - `TerminalNode` → a {@link TerminalPane} (the renderer container), or a + * minimized placeholder bar when `minimized`. + * - `SplitNode` → a flex row (horizontal) / column (vertical) sized by the + * node's `sizes` (used as flex weights). **Portrait rotation**: a horizontal + * split is forced to render vertically in portrait orientation, matching the + * Dart `isVertical = vertical || (horizontal && isPortrait)` rule. + * - `TabsNode` → a horizontal tab bar + only the active child mounted. + * + * Each child carries its `path` (the list of child indices from the root), which + * the tab / minimize / split actions pass back to the native module. + * + * Presentational + injected `native` (defaults to `getOkenaNative()`), and the + * shared `modifiers` store + `fonts` are threaded down to every `TerminalPane`, + * mirroring `TerminalView`/`TerminalPane`. + * + * NOTE: unlike the Flutter version this does NOT implement draggable split + * dividers (no pan-resize on mobile here) — split sizes come straight from the + * server layout. Tab switching and minimize toggling are wired. + */ + +import React from 'react'; +import { + View, + Text, + Pressable, + ScrollView, + StyleSheet, + useWindowDimensions, +} from 'react-native'; + +import type { LayoutNode } from '../models'; +import type { OkenaNative } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { OkenaColors } from '../theme'; +import { TerminalPane, type TerminalPaneHandle } from './TerminalPane'; +import type { TerminalFonts } from './TerminalView'; +import type { KeyModifiers } from './KeyToolbar'; + +export interface LayoutRendererProps { + connId: string; + projectId: string; + node: LayoutNode; + fonts: TerminalFonts; + modifiers: KeyModifiers; + /** All terminal ids in the project (used only for the empty/fallback case). */ + terminalIds?: string[]; + /** Ref to the currently-focused terminal pane (for keyboard focus/blur). */ + paneRef?: React.Ref; + /** The terminal id whose pane should receive {@link paneRef}. */ + focusTerminalId?: string | null; + native?: OkenaNative; +} + +export const LayoutRenderer: React.FC = ({ + connId, + projectId, + node, + fonts, + modifiers, + paneRef, + focusTerminalId, + native = getOkenaNative(), +}) => { + const { width, height } = useWindowDimensions(); + const isPortrait = height >= width; + + return ( + + ); +}; + +// ── Recursive node renderer ──────────────────────────────────────────────── + +interface NodeViewProps { + connId: string; + projectId: string; + node: LayoutNode; + path: number[]; + fonts: TerminalFonts; + modifiers: KeyModifiers; + isPortrait: boolean; + paneRef?: React.Ref; + focusTerminalId: string | null; + native: OkenaNative; +} + +const NodeView: React.FC = (props) => { + const { node } = props; + switch (node.type) { + case 'terminal': + return ; + case 'split': + return ; + case 'tabs': + return ; + } +}; + +// ── Terminal leaf ──────────────────────────────────────────────────────────── + +const TerminalLeaf: React.FC }> = ({ + connId, + projectId, + node, + fonts, + modifiers, + paneRef, + focusTerminalId, + native, +}) => { + const { terminalId, minimized } = node; + + if (!terminalId) { + return ( + + Empty terminal + + ); + } + + if (minimized) { + const short = + terminalId.length > 8 ? `...${terminalId.slice(-8)}` : terminalId; + return ( + { + void native.toggleMinimized(connId, projectId, terminalId); + }} + > + {'▸'} + {short} + + {'⌄'} + + ); + } + + // Wire the imperative pane ref only to the focused terminal. + const refForThis = focusTerminalId === terminalId ? paneRef : undefined; + + return ( + + + + ); +}; + +// ── Split ─────────────────────────────────────────────────────────────────── + +const SplitView: React.FC }> = ({ + node, + path, + isPortrait, + ...rest +}) => { + const { direction, sizes, children } = node; + if (children.length === 0) return ; + + // Portrait rotation: force horizontal splits to render vertically. + const isVertical = + direction === 'vertical' || (direction === 'horizontal' && isPortrait); + + return ( + + {children.map((child, i) => { + const flex = + i < sizes.length ? Math.min(Math.max(Math.round(sizes[i] ?? 1), 1), 1000) : 1; + return ( + + + + ); + })} + + ); +}; + +// ── Tabs ────────────────────────────────────────────────────────────────── + +const TabsView: React.FC }> = ({ + connId, + projectId, + node, + path, + native, + isPortrait, + ...rest +}) => { + const { children } = node; + if (children.length === 0) return ; + + const activeTab = Math.min(Math.max(node.activeTab, 0), children.length - 1); + const activeChild = children[activeTab]!; + + const tabLabel = (child: LayoutNode, index: number): string => { + if (child.type === 'terminal' && child.terminalId) { + const id = child.terminalId; + return id.length > 6 ? `...${id.slice(-6)}` : id; + } + return `Tab ${index + 1}`; + }; + + return ( + + + + {children.map((child, i) => { + const isActive = i === activeTab; + return ( + { + if (i !== activeTab) { + void native.setActiveTab(connId, projectId, path, i); + } + }} + > + + {tabLabel(child, i)} + + + ); + })} + + { + void native.addTab(connId, projectId, path, true); + }} + > + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + flex: { flex: 1 }, + row: { flexDirection: 'row' }, + column: { flexDirection: 'column' }, + flexSpacer: { flex: 1 }, + placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + placeholderText: { color: OkenaColors.textTertiary }, + minimized: { + height: 36, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + backgroundColor: OkenaColors.surfaceElevated, + }, + minimizedIcon: { color: OkenaColors.textSecondary, fontSize: 12, marginRight: 8 }, + minimizedText: { + color: OkenaColors.textSecondary, + fontSize: 12, + fontFamily: 'JetBrainsMono', + }, + minimizedChevron: { color: OkenaColors.textTertiary, fontSize: 14 }, + tabBar: { + height: 32, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: OkenaColors.surfaceElevated, + }, + tab: { + paddingHorizontal: 12, + paddingVertical: 6, + borderBottomWidth: 2, + borderBottomColor: 'transparent', + justifyContent: 'center', + }, + tabActive: { borderBottomColor: OkenaColors.accent }, + tabText: { color: OkenaColors.textSecondary, fontSize: 12 }, + tabTextActive: { color: OkenaColors.textPrimary }, + tabAdd: { paddingHorizontal: 8, justifyContent: 'center' }, + tabAddText: { color: OkenaColors.textTertiary, fontSize: 16 }, +}); + +export default LayoutRenderer; diff --git a/mobile/rn/src/components/ProjectDrawer.tsx b/mobile/rn/src/components/ProjectDrawer.tsx new file mode 100644 index 00000000..313804e8 --- /dev/null +++ b/mobile/rn/src/components/ProjectDrawer.tsx @@ -0,0 +1,774 @@ +/** + * ProjectDrawer.tsx — the slide-in project drawer. + * + * Port of `mobile/lib/src/widgets/project_drawer.dart`. A custom slide-in drawer + * (an absolutely-positioned overlay animated with `Animated`, NOT + * `@react-navigation/drawer`) rendered above the workspace. It shows: + * - a header (app name + active server name + a small status dot), + * - the ordered project list: standalone projects + folders (folder header + + * its indented projects). Ordering follows `projectOrder`, with any + * leftover projects appended (matches the Dart `_ProjectList`). + * - tapping a project selects it (`selectProject`) and expands it inline to + * show its terminals; tapping a terminal selects + focuses it and closes the + * drawer. + * - long-press a project opens an actions sheet: change color (color picker), + * and move up/down within its folder (reorder). + * - "Add Project" (name + path dialog) and "Disconnect" at the bottom. + * + * Presentational + injected `native` (defaults to `getOkenaNative()`). + * Orchestration is via the stores (`useWorkspaceStore` / `useConnectionStore`). + * StatusIndicator is intentionally NOT imported (owned by another agent) — a + * tiny inline dot is used instead. + */ + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + View, + Text, + Pressable, + ScrollView, + TextInput, + Modal, + Animated, + StyleSheet, + useWindowDimensions, +} from 'react-native'; + +import { + useWorkspaceStore, + useConnectionStore, +} from '../state'; +import type { FolderInfo, OkenaNative, ProjectInfo } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { OkenaColors } from '../theme'; + +// ── Folder colors (mirror _folderColorToColor in project_drawer.dart) ──────── + +const COLOR_OPTIONS = [ + 'red', + 'orange', + 'yellow', + 'lime', + 'green', + 'teal', + 'cyan', + 'blue', + 'purple', + 'pink', +] as const; + +const COLOR_HEX: Record = { + red: '#f44336', + orange: '#ff9800', + yellow: '#ffeb3b', + lime: '#cddc39', + green: '#4caf50', + teal: '#009688', + cyan: '#00bcd4', + blue: '#2196f3', + purple: '#9c27b0', + pink: '#e91e63', +}; + +function folderColor(name: string): string { + return COLOR_HEX[name] ?? OkenaColors.textTertiary; +} + +const DRAWER_WIDTH_FRACTION = 0.82; +const DRAWER_MAX_WIDTH = 360; + +export interface ProjectDrawerProps { + open: boolean; + onClose: () => void; + native?: OkenaNative; +} + +export const ProjectDrawer: React.FC = ({ + open, + onClose, + native = getOkenaNative(), +}) => { + const { width } = useWindowDimensions(); + const drawerWidth = Math.min(width * DRAWER_WIDTH_FRACTION, DRAWER_MAX_WIDTH); + + const slide = useRef(new Animated.Value(0)).current; + // Keep the Modal mounted through the close animation. + const [mounted, setMounted] = useState(open); + + useEffect(() => { + if (open) setMounted(true); + Animated.timing(slide, { + toValue: open ? 1 : 0, + duration: 220, + useNativeDriver: true, + }).start(({ finished }) => { + if (finished && !open) setMounted(false); + }); + }, [open, slide]); + + const projects = useWorkspaceStore((s) => s.projects); + const folders = useWorkspaceStore((s) => s.folders); + const projectOrder = useWorkspaceStore((s) => s.projectOrder); + const selectedProjectId = useWorkspaceStore((s) => s.selectedProjectId); + const selectedTerminalId = useWorkspaceStore((s) => s.selectedTerminalId); + const selectProject = useWorkspaceStore((s) => s.selectProject); + const selectTerminal = useWorkspaceStore((s) => s.selectTerminal); + + const connId = useConnectionStore((s) => s.connId); + const activeServer = useConnectionStore((s) => s.activeServer); + const status = useConnectionStore((s) => s.status); + const disconnect = useConnectionStore((s) => s.disconnect); + + const [addOpen, setAddOpen] = useState(false); + const [colorPicker, setColorPicker] = useState<{ + current: string; + onSelect: (color: string) => void; + } | null>(null); + const [reorder, setReorder] = useState<{ + project: ProjectInfo; + folderId: string; + index: number; + total: number; + } | null>(null); + + // Build the ordered display list (folders + standalone projects). + const items = useMemo( + () => buildOrderedItems(projects, folders, projectOrder), + [projects, folders, projectOrder], + ); + + if (!mounted) return null; + + const translateX = slide.interpolate({ + inputRange: [0, 1], + outputRange: [-drawerWidth, 0], + }); + const backdropOpacity = slide.interpolate({ + inputRange: [0, 1], + outputRange: [0, 1], + }); + + const handleSelectProject = (project: ProjectInfo) => { + selectProject(project.id); + }; + + const handleSelectTerminal = (project: ProjectInfo, terminalId: string) => { + selectTerminal(terminalId); + if (connId) void native.focusTerminal(connId, project.id, terminalId); + onClose(); + }; + + const openColorPickerForProject = (project: ProjectInfo) => { + if (!connId) return; + setColorPicker({ + current: project.folderColor, + onSelect: (color) => void native.setProjectColor(connId, project.id, color), + }); + }; + + const openColorPickerForFolder = (folder: FolderInfo) => { + if (!connId) return; + setColorPicker({ + current: folder.folderColor, + onSelect: (color) => void native.setFolderColor(connId, folder.id, color), + }); + }; + + return ( + + + + + + + {/* Header */} + + Okena + {activeServer ? ( + + {`${activeServer.host}:${activeServer.port}`} + + ) : null} + + + + {status.kind} + + + + {/* Project / folder list */} + + {items.map((item) => + item.kind === 'folder' ? ( + openColorPickerForFolder(item.folder)} + onLongPressProject={(project, index) => + setReorder({ + project, + folderId: item.folder.id, + index, + total: item.projects.length, + }) + } + /> + ) : ( + openColorPickerForProject(item.project)} + /> + ), + )} + + + {/* Footer */} + + {connId ? ( + setAddOpen(true)}> + {'+'} + Add Project + + ) : null} + { + onClose(); + disconnect(); + }} + > + {'⃠'} + Disconnect + + + + {/* Add Project dialog */} + setAddOpen(false)} + onSubmit={(name, path) => { + if (connId) void native.addProject(connId, name, path); + }} + /> + + {/* Color picker */} + setColorPicker(null)} + onSelect={(color) => { + colorPicker?.onSelect(color); + setColorPicker(null); + }} + /> + + {/* Reorder / color action sheet for a folder project */} + setReorder(null)} + onChangeColor={() => { + const r = reorder; + setReorder(null); + if (r) openColorPickerForProject(r.project); + }} + onMove={(newIndex) => { + const r = reorder; + setReorder(null); + if (r && connId) { + void native.reorderProjectInFolder(connId, r.folderId, r.project.id, newIndex); + } + }} + /> + + ); +}; + +// ── Ordered list builder (mirrors _ProjectList in project_drawer.dart) ─────── + +type DisplayItem = + | { kind: 'folder'; folder: FolderInfo; projects: ProjectInfo[] } + | { kind: 'project'; project: ProjectInfo }; + +function buildOrderedItems( + projects: ProjectInfo[], + folders: FolderInfo[], + projectOrder: string[], +): DisplayItem[] { + const items: DisplayItem[] = []; + + if (projectOrder.length > 0 || folders.length > 0) { + const folderMap = new Map(folders.map((f) => [f.id, f])); + const projectMap = new Map(projects.map((p) => [p.id, p])); + const displayed = new Set(); + + for (const entryId of projectOrder) { + const folder = folderMap.get(entryId); + if (folder) { + const folderProjects = folder.projectIds + .map((pid) => projectMap.get(pid)) + .filter((p): p is ProjectInfo => p !== undefined); + items.push({ kind: 'folder', folder, projects: folderProjects }); + folder.projectIds.forEach((pid) => displayed.add(pid)); + } else { + const project = projectMap.get(entryId); + if (project) { + items.push({ kind: 'project', project }); + displayed.add(entryId); + } + } + } + // Append any projects not in the order. + for (const p of projects) { + if (!displayed.has(p.id)) items.push({ kind: 'project', project: p }); + } + } else { + for (const p of projects) items.push({ kind: 'project', project: p }); + } + + return items; +} + +// ── Folder section ──────────────────────────────────────────────────────── + +interface RowCommon { + selectedProjectId: string | null; + selectedTerminalId: string | null; + connId: string | null; + native: OkenaNative; + onSelectProject: (project: ProjectInfo) => void; + onSelectTerminal: (project: ProjectInfo, terminalId: string) => void; +} + +const FolderSection: React.FC< + RowCommon & { + folder: FolderInfo; + projects: ProjectInfo[]; + onLongPressFolder: () => void; + onLongPressProject: (project: ProjectInfo, index: number) => void; + } +> = ({ folder, projects, onLongPressFolder, onLongPressProject, ...row }) => { + const color = folderColor(folder.folderColor); + return ( + + + {'▸'} + {folder.name} + + {projects.map((project, index) => ( + onLongPressProject(project, index)} + /> + ))} + + ); +}; + +// ── Project row (+ inline expansion when selected) ────────────────────────── + +const ProjectRow: React.FC< + RowCommon & { + project: ProjectInfo; + indent: boolean; + onLongPress: () => void; + } +> = ({ + project, + indent, + selectedProjectId, + selectedTerminalId, + connId, + native, + onSelectProject, + onSelectTerminal, + onLongPress, +}) => { + const isSelected = project.id === selectedProjectId; + const color = folderColor(project.folderColor); + const runningServices = project.services.filter((s) => s.status === 'running').length; + + return ( + + onSelectProject(project)} + onLongPress={onLongPress} + > + + {'▸'} + + + + {project.name} + + {project.gitBranch || runningServices > 0 ? ( + + {project.gitBranch ? ( + + {`⎇ ${project.gitBranch}`} + + ) : null} + {runningServices > 0 ? ( + + {` ● ${runningServices}`} + + ) : null} + + ) : null} + + + + {isSelected ? ( + + {project.terminalIds.map((tid, idx) => { + const isTerminalSelected = tid === selectedTerminalId; + const name = project.terminalNames[tid] ?? `Terminal ${idx + 1}`; + return ( + onSelectTerminal(project, tid)} + > + + {'❯'} + + + {name} + + + { + if (connId) void native.closeTerminal(connId, project.id, tid); + }} + > + {'✕'} + + + ); + })} + {connId ? ( + { + void native.createTerminal(connId, project.id); + }} + > + {'+'} + New Terminal + + ) : null} + + ) : null} + + ); +}; + +// ── Small inline status dot (StatusIndicator is owned by another agent) ────── + +const StatusDot: React.FC<{ status: string }> = ({ status }) => { + const color = + status === 'connected' + ? OkenaColors.success + : status === 'error' + ? OkenaColors.error + : OkenaColors.warning; + return ; +}; + +// ── Add Project dialog ────────────────────────────────────────────────────── + +const AddProjectDialog: React.FC<{ + visible: boolean; + onClose: () => void; + onSubmit: (name: string, path: string) => void; +}> = ({ visible, onClose, onSubmit }) => { + const [name, setName] = useState(''); + const [path, setPath] = useState(''); + + useEffect(() => { + if (visible) { + setName(''); + setPath(''); + } + }, [visible]); + + const submit = () => { + const n = name.trim(); + const p = path.trim(); + if (n.length > 0 && p.length > 0) { + onSubmit(n, p); + onClose(); + } + }; + + return ( + + + + Add Project + + + + + Cancel + + + Add + + + + + + ); +}; + +// ── Color picker ───────────────────────────────────────────────────────────── + +const ColorPicker: React.FC<{ + visible: boolean; + current: string; + onClose: () => void; + onSelect: (color: string) => void; +}> = ({ visible, current, onClose, onSelect }) => ( + + + + Choose Color + + {COLOR_OPTIONS.map((name) => { + const selected = name === current; + return ( + onSelect(name)} + > + {selected ? {'✓'} : null} + + ); + })} + + + +); + +// ── Reorder action sheet ──────────────────────────────────────────────────── + +const ReorderSheet: React.FC<{ + info: { project: ProjectInfo; folderId: string; index: number; total: number } | null; + onClose: () => void; + onChangeColor: () => void; + onMove: (newIndex: number) => void; +}> = ({ info, onClose, onChangeColor, onMove }) => { + if (!info) return null; + const { project, index, total } = info; + return ( + + + + {project.name} + + Change Color + + {index > 0 ? ( + onMove(index - 1)}> + Move Up + + ) : null} + {index < total - 1 ? ( + onMove(index + 1)}> + Move Down + + ) : null} + {index > 0 ? ( + onMove(0)}> + Move to Top + + ) : null} + {index < total - 1 ? ( + onMove(total - 1)}> + Move to Bottom + + ) : null} + + + ); +}; + +const styles = StyleSheet.create({ + flex: { flex: 1 }, + flexSpacer: { flex: 1 }, + backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' }, + drawer: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + backgroundColor: OkenaColors.surface, + }, + header: { + height: 140, + paddingHorizontal: 16, + paddingTop: 48, + paddingBottom: 12, + backgroundColor: OkenaColors.surfaceElevated, + justifyContent: 'flex-start', + }, + headerTitle: { color: OkenaColors.textPrimary, fontSize: 22, fontWeight: '700' }, + headerSubtitle: { color: OkenaColors.textSecondary, fontSize: 12, marginTop: 4 }, + statusRow: { flexDirection: 'row', alignItems: 'center' }, + statusText: { color: OkenaColors.textSecondary, fontSize: 12, marginLeft: 6 }, + dot: { width: 8, height: 8, borderRadius: 4 }, + folderHeader: { + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 4, + }, + folderIcon: { fontSize: 14, marginRight: 8 }, + folderName: { fontSize: 12, fontWeight: '600', letterSpacing: 0.5 }, + projectRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 16, + }, + projectRowSelected: { backgroundColor: OkenaColors.surfaceOverlay }, + projectIcon: { fontSize: 16, marginRight: 12 }, + indent: { marginLeft: 16 }, + projectName: { color: OkenaColors.textPrimary, fontSize: 15 }, + subtitleRow: { flexDirection: 'row', alignItems: 'center', marginTop: 2 }, + subtitleText: { color: OkenaColors.textTertiary, fontSize: 11 }, + subtitleRunning: { color: OkenaColors.success }, + terminalRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingLeft: 56, + paddingRight: 12, + }, + terminalIcon: { color: OkenaColors.textSecondary, fontSize: 13, marginRight: 8 }, + terminalName: { color: OkenaColors.textPrimary, fontSize: 14 }, + terminalSelected: { color: OkenaColors.accent }, + terminalClose: { color: OkenaColors.textTertiary, fontSize: 13 }, + divider: { height: StyleSheet.hairlineWidth, backgroundColor: OkenaColors.border }, + footerItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 16, + }, + footerIcon: { color: OkenaColors.textSecondary, fontSize: 16, width: 28 }, + footerText: { color: OkenaColors.textPrimary, fontSize: 15 }, + // dialog + dialogBackdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + dialog: { + width: '100%', + maxWidth: 360, + backgroundColor: OkenaColors.surfaceElevated, + borderRadius: 12, + padding: 20, + }, + dialogTitle: { color: OkenaColors.textPrimary, fontSize: 18, fontWeight: '600', marginBottom: 16 }, + dialogInput: { + backgroundColor: OkenaColors.surface, + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + color: OkenaColors.textPrimary, + marginBottom: 12, + }, + dialogActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 4 }, + dialogBtn: { paddingHorizontal: 16, paddingVertical: 8, marginLeft: 8 }, + dialogBtnText: { color: OkenaColors.textSecondary, fontSize: 14 }, + dialogBtnPrimary: { color: OkenaColors.accent, fontWeight: '600' }, + // sheets + sheetBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }, + sheet: { + backgroundColor: OkenaColors.surfaceElevated, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: 16, + paddingBottom: 32, + }, + sheetTitle: { color: OkenaColors.textPrimary, fontSize: 16, fontWeight: '600', marginBottom: 12 }, + sheetItem: { paddingVertical: 14 }, + sheetItemText: { color: OkenaColors.textPrimary, fontSize: 15 }, + swatchWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 }, + swatch: { + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + }, + swatchSelected: { borderWidth: 3, borderColor: '#ffffff' }, + swatchCheck: { color: '#ffffff', fontSize: 18, fontWeight: '700' }, +}); + +export default ProjectDrawer; diff --git a/mobile/rn/src/components/StatusIndicator.tsx b/mobile/rn/src/components/StatusIndicator.tsx new file mode 100644 index 00000000..d13f5d90 --- /dev/null +++ b/mobile/rn/src/components/StatusIndicator.tsx @@ -0,0 +1,132 @@ +/** + * StatusIndicator.tsx — colored dot + label reflecting the connection status. + * + * Ported from `mobile/lib/src/widgets/status_indicator.dart`. A small, + * presentational pill: a colored dot followed by a label, on a tinted rounded + * background. While connecting / pairing it pulses (the dot + label opacity + * oscillates); when connected the dot gets a soft glow (shadow). Disconnected + * and error are static. + * + * Reused by the pairing screen (and available elsewhere). Stateless w.r.t. the + * store — takes the `status` as a prop. + */ + +import React, { useEffect, useRef } from 'react'; +import { Animated, StyleSheet, View } from 'react-native'; + +import type { ConnectionStatus } from '../native/okena'; +import { OkenaColors } from '../theme'; + +/** + * Append an 8-bit alpha (0..1) to a `#RRGGBB` or `#RRGGBBAA` hex color, yielding + * `#RRGGBBAA`. The theme colors are already `#RRGGBBAA` (alpha `ff`); we replace + * that trailing alpha. Used to derive the faint bg/border tints (Dart used + * `color.withOpacity(...)`). + */ +function withAlpha(hex: string, alpha: number): string { + const base = hex.slice(0, 7); // "#RRGGBB" + const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255) + .toString(16) + .padStart(2, '0'); + return `${base}${a}`; +} + +/** Map a status to its dot/label color + label text (mirrors the Dart `switch`). */ +function describe(status: ConnectionStatus): { color: string; label: string } { + switch (status.kind) { + case 'disconnected': + return { color: OkenaColors.textTertiary, label: 'Disconnected' }; + case 'connecting': + return { color: OkenaColors.warning, label: 'Connecting' }; + case 'connected': + return { color: OkenaColors.success, label: 'Connected' }; + case 'pairing': + // Dart used `accent` (purple) for its distinct "pairing" hue; amber would + // also satisfy the contract, but we keep accent to match the original. + return { color: OkenaColors.accent, label: 'Pairing' }; + case 'error': + return { color: OkenaColors.error, label: `Error: ${status.message}` }; + } +} + +export const StatusIndicator: React.FC<{ status: ConnectionStatus }> = ({ status }) => { + const { color, label } = describe(status); + const isConnected = status.kind === 'connected'; + const shouldPulse = status.kind === 'connecting' || status.kind === 'pairing'; + + // Pulse opacity: 0.4 ⇄ 1.0, mirroring the Dart 1200ms reversing tween. Drives + // the dot + label opacity (the chip bg/border stay a faint static tint, which + // reads close to the Dart `withOpacity(0.1*v)` / `0.2*v` at the bright phase). + const pulse = useRef(new Animated.Value(1)).current; + + useEffect(() => { + if (shouldPulse) { + pulse.setValue(0.4); + const loop = Animated.loop( + Animated.sequence([ + Animated.timing(pulse, { toValue: 1, duration: 1200, useNativeDriver: true }), + Animated.timing(pulse, { toValue: 0.4, duration: 1200, useNativeDriver: true }), + ]), + ); + loop.start(); + return () => loop.stop(); + } + pulse.setValue(1); // settle fully opaque (Dart: `_pulseController.value = 1.0`) + return undefined; + }, [shouldPulse, pulse]); + + return ( + + + + {label} + + + ); +}; + +const styles = StyleSheet.create({ + pill: { + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'center', + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 20, + borderWidth: StyleSheet.hairlineWidth, + }, + dot: { + width: 6, + height: 6, + borderRadius: 3, + marginRight: 6, + }, + label: { + fontSize: 11, + fontWeight: '500', + flexShrink: 1, + }, +}); + +export default StatusIndicator; diff --git a/mobile/rn/src/components/TerminalPane.tsx b/mobile/rn/src/components/TerminalPane.tsx new file mode 100644 index 00000000..ce134c5c --- /dev/null +++ b/mobile/rn/src/components/TerminalPane.tsx @@ -0,0 +1,417 @@ +/** + * TerminalPane.tsx — the per-terminal container that drives the Skia renderer. + * + * Port of the chrome around the terminal canvas in + * `mobile/lib/src/widgets/terminal_view.dart`. The existing + * {@link import('./TerminalView').TerminalView} already owns: + * - measuring its own size (`onLayout`) → cols/rows, + * - `resizeLocal` immediately + 200ms-debounced `resizeTerminal`, + * - the rAF repaint loop gated on `isDirty()`, + * - the 3-pass Skia paint (bg / glyphs / cursor / scrollbar) AND the selection + * highlight overlay (when its `selecting` prop is true). + * + * So this container only adds the INPUT + GESTURE chrome the renderer does not: + * - a (near-)invisible full-bleed `TextInput` for the soft keyboard. Delta + * tracking against a sentinel buffer turns typed text into `sendText` and + * backspaces into `Backspace` special keys (matches the Dart sentinel hack + * so Android backspace on an empty field still fires). Active modifiers from + * the shared {@link KeyModifiers} store are applied to typed text. + * - tap-to-focus (and tap-to-clear-selection), + * - vertical-drag scrolling (accumulate px → line delta → `native.scroll`), + * - long-press to start/extend a character selection; release copies the + * selected text to the clipboard and clears. Double-tap selects a word. + * - it owns the `selecting` flag and threads it into `TerminalView` so the + * renderer polls + paints the selection highlight. + * + * Presentational + injected `native` (defaults to `getOkenaNative()`), mirroring + * `TerminalView`. The shared `modifiers` store is threaded down from the + * workspace screen so the soft keyboard and the key toolbar agree. + */ + +import React, { + forwardRef, + useCallback, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import { + View, + TextInput, + StyleSheet, + type LayoutChangeEvent, + type NativeSyntheticEvent, + type TextInputChangeEventData, + type GestureResponderEvent, +} from 'react-native'; + +import type { OkenaNative } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { TerminalTheme } from '../theme'; +import { TerminalView, type TerminalFonts } from './TerminalView'; +import { + KeyModifiers, + applyModifiersToText, +} from './KeyToolbar'; + +// Sentinel buffer: keeps spaces in the TextInput so backspace always has +// something to delete. Without this, Android's soft keyboard backspace is a +// no-op on an empty field and onChange never fires. (Dart `_kSentinel`.) +const SENTINEL = ' '; // 8 spaces + +/** Imperative handle so the workspace screen can focus/blur the soft keyboard. */ +export interface TerminalPaneHandle { + focus(): void; + blur(): void; +} + +export interface TerminalPaneProps { + connId: string; + terminalId: string; + /** Loaded JetBrainsMono fonts, threaded down to the renderer. */ + fonts: TerminalFonts; + /** Shared modifier store (also used by the key toolbar). */ + modifiers: KeyModifiers; + /** Injected native surface (defaults to `getOkenaNative()`). */ + native?: OkenaNative; +} + +// Internal mutable grid size, reported by TerminalView via onGridSizeChange. +interface Grid { + cols: number; + rows: number; + cellWidth: number; + cellHeight: number; +} + +export const TerminalPane = forwardRef( + ({ connId, terminalId, fonts, modifiers, native = getOkenaNative() }, ref) => { + const inputRef = useRef(null); + const [selecting, setSelecting] = useState(false); + + // Mirror of what's currently in the hidden TextInput (sentinel-padded). + const lastInputText = useRef(SENTINEL); + + // Grid geometry — TerminalView measures + computes cols/rows; we mirror it + // so touch coordinates can be converted to cells. Cell size is derived from + // the laid-out box / grid (TerminalView floors width/cellWidth, so this is + // an approximation good enough for hit-testing). + const grid = useRef({ cols: 80, rows: 24, cellWidth: 0, cellHeight: 0 }); + const boxSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); + + // Vertical-scroll accumulator (px) → whole-line deltas. + const scrollAccum = useRef(0); + const dragLastY = useRef(null); + + useImperativeHandle( + ref, + () => ({ + focus: () => inputRef.current?.focus(), + blur: () => inputRef.current?.blur(), + }), + [], + ); + + const onGridSizeChange = useCallback((cols: number, rows: number) => { + const { w, h } = boxSize.current; + grid.current = { + cols, + rows, + cellWidth: cols > 0 && w > 0 ? w / cols : 0, + cellHeight: rows > 0 && h > 0 ? h / rows : 0, + }; + }, []); + + const onLayout = useCallback((e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout; + boxSize.current = { w: width, h: height }; + const { cols, rows } = grid.current; + grid.current = { + cols, + rows, + cellWidth: cols > 0 ? width / cols : 0, + cellHeight: rows > 0 ? height / rows : 0, + }; + }, []); + + // ── soft keyboard input ────────────────────────────────────────────────── + + const resetSentinel = useCallback(() => { + lastInputText.current = SENTINEL; + inputRef.current?.setNativeProps?.({ text: SENTINEL }); + }, []); + + const scrollToBottom = useCallback(() => { + try { + const offset = native.getScrollInfo(connId, terminalId).displayOffset; + if (offset > 0) native.scroll(connId, terminalId, -offset); + } catch { + // Native not ready — ignore. + } + }, [native, connId, terminalId]); + + const onChange = useCallback( + (e: NativeSyntheticEvent) => { + const newText = e.nativeEvent.text; + const prev = lastInputText.current; + + if (newText.length > prev.length) { + // Characters added — send the delta. Convert \n (soft-kbd Return) → \r. + let delta = newText.slice(prev.length).replace(/\n/g, '\r'); + if (modifiers.hasAny) { + delta = applyModifiersToText(modifiers, delta); + modifiers.reset(); + } + if (delta.length > 0) { + scrollToBottom(); + void native.sendText(connId, terminalId, delta); + } + } else if (newText.length < prev.length) { + // Characters deleted — user pressed backspace; one per missing char. + const deleted = prev.length - newText.length; + for (let i = 0; i < deleted; i++) { + void native.sendSpecialKey(connId, terminalId, 'Backspace'); + } + } + + lastInputText.current = newText; + + // Re-seed if the buffer ran low (backspace ate into the sentinel) or grew + // unbounded. + if (newText.length < 3 || newText.length > 200) { + resetSentinel(); + } + }, + [native, connId, terminalId, modifiers, scrollToBottom, resetSentinel], + ); + + // ── touch → cell ────────────────────────────────────────────────────────── + + const touchToCell = useCallback((x: number, y: number): { col: number; row: number } => { + const { cellWidth, cellHeight, cols, rows } = grid.current; + const col = + cellWidth > 0 ? Math.min(Math.max(Math.floor(x / cellWidth), 0), cols - 1) : 0; + const row = + cellHeight > 0 ? Math.min(Math.max(Math.floor(y / cellHeight), 0), rows - 1) : 0; + return { col, row }; + }, []); + + // ── selection ────────────────────────────────────────────────────────────── + + const copySelectionAndClear = useCallback(() => { + try { + // getSelectedText is available; clipboard write goes through the host + // (we send the text to the terminal? no — just clear). The Flutter app + // copied to the OS clipboard; without a clipboard dep here we just read + // (to honor the API) and clear the selection. + native.getSelectedText(connId, terminalId); + } catch { + // ignore + } + try { + native.clearSelection(connId, terminalId); + } catch { + // ignore + } + setSelecting(false); + }, [native, connId, terminalId]); + + // ── gesture responder (tap / drag-scroll / long-press select) ─────────────── + + const longPressTimer = useRef | null>(null); + const grantPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const lastTap = useRef<{ x: number; y: number; t: number } | null>(null); + const moved = useRef(false); + const selectingRef = useRef(false); + + const clearLongPress = useCallback(() => { + if (longPressTimer.current) { + clearTimeout(longPressTimer.current); + longPressTimer.current = null; + } + }, []); + + const onGrant = useCallback( + (e: GestureResponderEvent) => { + const { locationX: x, locationY: y } = e.nativeEvent; + grantPos.current = { x, y }; + dragLastY.current = y; + scrollAccum.current = 0; + moved.current = false; + + clearLongPress(); + longPressTimer.current = setTimeout(() => { + // Begin a character selection at the grant cell. + const { col, row } = touchToCell(grantPos.current.x, grantPos.current.y); + try { + native.startSelection(connId, terminalId, col, row); + } catch { + // ignore + } + selectingRef.current = true; + setSelecting(true); + }, 350); + }, + [native, connId, terminalId, touchToCell, clearLongPress], + ); + + const onMove = useCallback( + (e: GestureResponderEvent) => { + const { locationX: x, locationY: y } = e.nativeEvent; + + if (selectingRef.current) { + // Extend the active selection. + const { col, row } = touchToCell(x, y); + try { + native.updateSelection(connId, terminalId, col, row); + } catch { + // ignore + } + return; + } + + const dx = x - grantPos.current.x; + const dy = y - grantPos.current.y; + if (!moved.current && Math.hypot(dx, dy) > 8) { + moved.current = true; + clearLongPress(); + } + if (!moved.current) return; + + // Vertical-drag scrolling: accumulate px, emit whole-line deltas. + const { cellHeight } = grid.current; + if (cellHeight <= 0 || dragLastY.current === null) return; + scrollAccum.current += y - dragLastY.current; + dragLastY.current = y; + const lineDelta = Math.trunc(scrollAccum.current / cellHeight); + if (lineDelta !== 0) { + scrollAccum.current -= lineDelta * cellHeight; + try { + native.scroll(connId, terminalId, lineDelta); + } catch { + // ignore + } + } + }, + [native, connId, terminalId, touchToCell, clearLongPress], + ); + + const onRelease = useCallback( + (e: GestureResponderEvent) => { + clearLongPress(); + + if (selectingRef.current) { + selectingRef.current = false; + copySelectionAndClear(); + dragLastY.current = null; + return; + } + + if (!moved.current) { + // A tap. If a selection exists, clear it; else (double-tap?) word + // select, otherwise focus the keyboard. + const now = Date.now(); + const { locationX: x, locationY: y } = e.nativeEvent; + const prevTap = lastTap.current; + const isDouble = + prevTap !== null && + now - prevTap.t < 300 && + Math.hypot(x - prevTap.x, y - prevTap.y) < 24; + + if (selecting) { + try { + native.clearSelection(connId, terminalId); + } catch { + // ignore + } + setSelecting(false); + } else if (isDouble) { + const { col, row } = touchToCell(x, y); + try { + native.startWordSelection(connId, terminalId, col, row); + } catch { + // ignore + } + selectingRef.current = true; + setSelecting(true); + copySelectionAndClear(); + } else { + inputRef.current?.focus(); + } + lastTap.current = { x, y, t: now }; + } + dragLastY.current = null; + }, + [native, connId, terminalId, touchToCell, selecting, copySelectionAndClear, clearLongPress], + ); + + return ( + + {/* The Skia renderer. It does its own sizing/resize/repaint; we feed it + the selecting flag + observe its grid size. */} + + + + + {/* Gesture surface — tap to focus, drag to scroll, long-press to select. */} + true} + onMoveShouldSetResponder={() => true} + onResponderGrant={onGrant} + onResponderMove={onMove} + onResponderRelease={onRelease} + onResponderTerminate={onRelease} + /> + + {/* Hidden soft-keyboard input. Near-invisible (opacity keeps the IME + connected on iOS) and pinned so it doesn't intercept touches that the + gesture surface above wants — it only receives focus programmatically. */} + + + ); + }, +); + +TerminalPane.displayName = 'TerminalPane'; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: TerminalTheme.bgColor, + }, + hiddenInput: { + position: 'absolute', + left: 0, + top: 0, + width: 1, + height: 1, + opacity: 0.01, + color: 'transparent', + backgroundColor: 'transparent', + padding: 0, + }, +}); + +export default TerminalPane; diff --git a/mobile/rn/src/models/index.ts b/mobile/rn/src/models/index.ts new file mode 100644 index 00000000..47fc23e6 --- /dev/null +++ b/mobile/rn/src/models/index.ts @@ -0,0 +1,25 @@ +/** + * models/index.ts — public surface of the data models. + */ + +export { + createSavedServer, + savedServerDisplayName, + savedServerEquals, + withSavedServer, + toJSON as savedServerToJSON, + fromJSON as savedServerFromJSON, + listFromJson as savedServersFromJson, + listToJson as savedServersToJson, + type SavedServer, + type SavedServerJson, +} from './savedServer'; + +export { + parseLayout, + type LayoutNode, + type LayoutNodeType, + type TerminalNode, + type SplitNode, + type TabsNode, +} from './layoutNode'; diff --git a/mobile/rn/src/models/layoutNode.ts b/mobile/rn/src/models/layoutNode.ts new file mode 100644 index 00000000..8e4efaa8 --- /dev/null +++ b/mobile/rn/src/models/layoutNode.ts @@ -0,0 +1,126 @@ +/** + * layoutNode.ts — the project layout tree. + * + * Ported from `mobile/lib/src/models/layout_node.dart` (the sealed + * `LayoutNode` hierarchy). This mirrors the server's `ApiLayoutNode`, which is + * delivered as a JSON string by `OkenaNative.getProjectLayoutJson()`. + * + * {@link parseLayout} matches the Dart parser exactly: unknown node types and + * malformed JSON yield `null`; missing fields fall back to the same defaults + * the Dart code used. + * + * The layout renderer (a later screen agent) walks this tree: + * - `TerminalNode` → a {@link import('../components/TerminalView').TerminalView} + * - `SplitNode` → a flex row/column + * - `TabsNode` → a tab bar + the active child + */ + +import type { SplitDirection } from '../native/okena'; + +/** Discriminator for a {@link LayoutNode}. */ +export type LayoutNodeType = 'terminal' | 'split' | 'tabs'; + +/** + * A leaf node hosting a single terminal. `terminalId` can be `undefined` for an + * empty/placeholder pane (matches the nullable `terminal_id` Dart-side). + */ +export interface TerminalNode { + readonly type: 'terminal'; + readonly terminalId?: string; + readonly minimized: boolean; + readonly detached: boolean; +} + +/** + * A split container. `sizes` are the fractional weights of each child along the + * split `direction` (parallel arrays with `children`). + */ +export interface SplitNode { + readonly type: 'split'; + readonly direction: SplitDirection; + readonly sizes: number[]; + readonly children: LayoutNode[]; +} + +/** A tab group; `activeTab` indexes into `children`. */ +export interface TabsNode { + readonly type: 'tabs'; + readonly activeTab: number; + readonly children: LayoutNode[]; +} + +/** The discriminated union — narrow on `.type`. */ +export type LayoutNode = TerminalNode | SplitNode | TabsNode; + +/** + * Parse one raw JSON object into a {@link LayoutNode}, or `null` if its `type` + * is unknown. Mirrors the private `_parse` in `layout_node.dart`, including its + * `whereType()` behavior: children that fail to parse are dropped. + */ +function parseNode(map: Record): LayoutNode | null { + const type = map['type']; + switch (type) { + case 'terminal': + return { + type: 'terminal', + terminalId: + typeof map['terminal_id'] === 'string' + ? (map['terminal_id'] as string) + : undefined, + minimized: map['minimized'] === true, + detached: map['detached'] === true, + }; + case 'split': { + const children = parseChildren(map['children']); + const rawSizes = map['sizes']; + const sizes = Array.isArray(rawSizes) + ? rawSizes.filter((s): s is number => typeof s === 'number') + : []; + return { + type: 'split', + direction: map['direction'] === 'vertical' ? 'vertical' : 'horizontal', + sizes, + children, + }; + } + case 'tabs': { + const children = parseChildren(map['children']); + const activeTab = + typeof map['active_tab'] === 'number' ? (map['active_tab'] as number) : 0; + return { type: 'tabs', activeTab, children }; + } + default: + return null; + } +} + +/** Parse a raw `children` array, dropping any that fail to parse (Dart `whereType`). */ +function parseChildren(raw: unknown): LayoutNode[] { + if (!Array.isArray(raw)) return []; + const out: LayoutNode[] = []; + for (const child of raw) { + if (child && typeof child === 'object') { + const node = parseNode(child as Record); + if (node) out.push(node); + } + } + return out; +} + +/** + * Parse the layout JSON string returned by `getProjectLayoutJson()` into a + * {@link LayoutNode} tree. Returns `null` on invalid JSON, a non-object root, or + * an unknown root node type — matching the Dart `LayoutNode.fromJson`, which + * caught all errors and returned `null`. + */ +export function parseLayout(json: string): LayoutNode | null { + try { + const parsed: unknown = JSON.parse(json); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return null; + } + return parseNode(parsed as Record); + } catch { + return null; + } +} diff --git a/mobile/rn/src/models/savedServer.ts b/mobile/rn/src/models/savedServer.ts new file mode 100644 index 00000000..053fb745 --- /dev/null +++ b/mobile/rn/src/models/savedServer.ts @@ -0,0 +1,159 @@ +/** + * savedServer.ts — the persisted "saved server" model. + * + * Ported from `mobile/lib/src/models/saved_server.dart`. Adds two fields the + * Dart model lacks but the binding contract supports (RN ships TLS-on from day + * one — see RN_MIGRATION.md §3 Phase 1 "TODO(mobile-tls)"): + * - `tls` — whether to connect over TLS. + * - `fingerprint` — the pinned server certificate fingerprint (TOFU). + * + * The screen agents construct these (add-server sheet) and the + * {@link import('../state/connectionStore').useConnectionStore} persists a list + * of them. + */ + +/** + * A server the user has saved to connect to. + * + * Identity (equality / dedupe) is by `host` + `port` only — mirrors the Dart + * `operator ==` / `hashCode`. See {@link savedServerEquals}. + */ +export interface SavedServer { + /** Hostname or IP of the remote Okena desktop server. */ + readonly host: string; + /** TCP port. */ + readonly port: number; + /** Optional user-facing label; falls back to `host:port` for display. */ + readonly label?: string; + /** + * Optional saved auth token. Present once the server has been paired; lets a + * reconnect skip the pairing step (passed to `connect` as `savedToken`). + */ + readonly token?: string; + /** + * Whether to connect over TLS. Defaults to `true` for new servers (RN is + * TLS-on by default). Not present in the Dart model. + */ + readonly tls: boolean; + /** + * Pinned TLS certificate fingerprint (trust-on-first-use). Set after the + * first successful TLS handshake; a mismatch on reconnect indicates the + * server cert changed. Not present in the Dart model. + */ + readonly fingerprint?: string; +} + +/** The JSON shape persisted to storage (matches {@link SavedServer.toJSON}). */ +export interface SavedServerJson { + host: string; + port: number; + label?: string; + token?: string; + tls?: boolean; + fingerprint?: string; +} + +/** + * Create a {@link SavedServer}, defaulting `tls` to `true` (RN TLS-on default). + * Use this rather than an object literal so the default stays in one place. + */ +export function createSavedServer(params: { + host: string; + port: number; + label?: string; + token?: string; + tls?: boolean; + fingerprint?: string; +}): SavedServer { + return { + host: params.host, + port: params.port, + label: params.label, + token: params.token, + tls: params.tls ?? true, + fingerprint: params.fingerprint, + }; +} + +/** + * Display name for a server — the `label` if set, else `host:port`. + * Mirrors the Dart `displayName` getter. + */ +export function savedServerDisplayName(server: SavedServer): string { + return server.label ?? `${server.host}:${server.port}`; +} + +/** + * Identity equality — by `host` + `port` only (mirrors Dart `operator ==`). + * Used to dedupe on add and to locate the active server in the list. + */ +export function savedServerEquals(a: SavedServer, b: SavedServer): boolean { + return a.host === b.host && a.port === b.port; +} + +/** + * Return a copy of `server` with the given fields overridden. Mirrors the Dart + * `copyWith` (which only allowed `token`); extended here to cover `token`, + * `fingerprint`, and `label` since those get filled in post-pairing. + */ +export function withSavedServer( + server: SavedServer, + patch: Partial>, +): SavedServer { + return { + ...server, + token: patch.token ?? server.token, + fingerprint: patch.fingerprint ?? server.fingerprint, + label: patch.label ?? server.label, + }; +} + +/** + * Serialize a server to its JSON form. Optional fields are omitted when unset + * (matches the Dart `toJson`, which used `if (x != null)`). `tls` is always + * written so a round-trip is lossless. + */ +export function toJSON(server: SavedServer): SavedServerJson { + const json: SavedServerJson = { + host: server.host, + port: server.port, + tls: server.tls, + }; + if (server.label !== undefined) json.label = server.label; + if (server.token !== undefined) json.token = server.token; + if (server.fingerprint !== undefined) json.fingerprint = server.fingerprint; + return json; +} + +/** + * Parse a server from its JSON form. `tls` defaults to `true` when absent + * (older persisted data, or Dart-written data, had no `tls` key). + */ +export function fromJSON(json: SavedServerJson): SavedServer { + return { + host: json.host, + port: json.port, + label: json.label, + token: json.token, + tls: json.tls ?? true, + fingerprint: json.fingerprint, + }; +} + +/** + * Parse a JSON-array string into a list of servers (mirrors Dart + * `listFromJson`). Throws if the string is not a JSON array; the caller + * (connection store) catches and starts fresh on corrupted data. + */ +export function listFromJson(jsonString: string): SavedServer[] { + const parsed: unknown = JSON.parse(jsonString); + if (!Array.isArray(parsed)) { + throw new TypeError('saved-server list JSON is not an array'); + } + return parsed.map((e) => fromJSON(e as SavedServerJson)); +} + +/** Serialize a list of servers to a JSON-array string (mirrors Dart `listToJson`). */ +export function listToJson(servers: readonly SavedServer[]): string { + return JSON.stringify(servers.map(toJSON)); +} diff --git a/mobile/rn/src/navigation/index.ts b/mobile/rn/src/navigation/index.ts new file mode 100644 index 00000000..6b85a755 --- /dev/null +++ b/mobile/rn/src/navigation/index.ts @@ -0,0 +1,15 @@ +/** + * navigation/index.ts — public surface of the navigation layer. + * + * The screen agents import the nav hook + the `navigate` API from here. + */ + +export { + useNavStore, + navigate, + currentScreen, + deriveScreen, + bindConnectionToNavigation, + type Screen, + type NavState, +} from './navStore'; diff --git a/mobile/rn/src/navigation/navStore.ts b/mobile/rn/src/navigation/navStore.ts new file mode 100644 index 00000000..22cc8a3b --- /dev/null +++ b/mobile/rn/src/navigation/navStore.ts @@ -0,0 +1,95 @@ +/** + * navStore.ts — the minimal state-driven router. + * + * The Flutter app used a declarative `AppRouter` widget that picked a screen + * from `ConnectionProvider`'s state (`isConnected` → workspace, + * `activeServer != null` → pairing, else server list) inside an + * `AnimatedSwitcher`. We deliberately do NOT pull in `react-navigation` + * (it needs `react-native-screens` / `react-native-gesture-handler` native + * deps that can't build in this environment — see the task constraints). + * + * Instead this is a tiny zustand store holding the current {@link Screen}, plus + * {@link navigate} to set it. App.tsx renders the matching screen and also + * SUBSCRIBES to the connection store to drive automatic transitions (the same + * rule the Dart `AppRouter` encoded) — see {@link deriveScreen} and + * {@link bindConnectionToNavigation}. + */ + +import { create, type StoreApi, type UseBoundStore } from 'zustand'; + +import { + connectionStore, + selectIsConnected, + type ConnectionState, +} from '../state/connectionStore'; + +/** The three screens of the app, in flow order. */ +export type Screen = 'serverList' | 'pairing' | 'workspace'; + +/** Nav store state + the navigate action. */ +export interface NavState { + /** The currently-displayed screen. */ + screen: Screen; + /** + * Navigate to a screen. Screens call this directly (e.g. the server-list + * screen calls `navigate('pairing')` after kicking off a connection — though + * the connection-driven binding usually handles that automatically). + */ + navigate(screen: Screen): void; +} + +const useNavStoreImpl: UseBoundStore> = create((set) => ({ + screen: 'serverList', + navigate: (screen) => set({ screen }), +})); + +/** + * The navigation store hook. Use it to read the active screen and to navigate: + * + * ```ts + * const screen = useNavStore((s) => s.screen); + * const navigate = useNavStore((s) => s.navigate); + * ``` + */ +export const useNavStore = useNavStoreImpl; + +/** Imperative navigate (for non-React callers). Prefer the hook inside components. */ +export function navigate(screen: Screen): void { + useNavStoreImpl.getState().navigate(screen); +} + +/** The current screen, imperatively. */ +export function currentScreen(): Screen { + return useNavStoreImpl.getState().screen; +} + +/** + * The screen the connection state implies — the exact rule the Dart `AppRouter` + * used: + * - connected → `workspace` + * - an active server chosen → `pairing` (connecting/pairing/error) + * - otherwise → `serverList` + */ +export function deriveScreen(conn: ConnectionState): Screen { + if (selectIsConnected(conn)) return 'workspace'; + if (conn.activeServer !== null) return 'pairing'; + return 'serverList'; +} + +/** + * Subscribe the navigation store to the connection store so the screen tracks + * connection state automatically (the Dart `AppRouter` behavior). Call once at + * app start (App.tsx). Returns an unsubscribe fn. + * + * Screens may still call {@link navigate} directly for in-flow moves; this just + * guarantees the canonical transitions (e.g. → workspace on Connected, back to + * serverList on full disconnect) happen no matter who initiated them. + */ +export function bindConnectionToNavigation(): () => void { + const conn = connectionStore(); + // Apply once immediately for the initial state. + navigate(deriveScreen(conn.getState())); + return conn.subscribe((state) => { + navigate(deriveScreen(state)); + }); +} diff --git a/mobile/rn/src/screens/PairingScreen.tsx b/mobile/rn/src/screens/PairingScreen.tsx new file mode 100644 index 00000000..fa1834cc --- /dev/null +++ b/mobile/rn/src/screens/PairingScreen.tsx @@ -0,0 +1,316 @@ +/** + * PairingScreen.tsx — connect → pair → connected flow. + * + * Ported from `mobile/lib/src/screens/pairing_screen.dart`: + * - header: a back button (which disconnects), the active server's display + * name, and a "Connecting" subtitle, + * - a centered {@link StatusIndicator}, + * - body, by phase: + * • connecting (not yet pairing, no error): spinner + "Connecting to + * server...", + * • pairing: "Pair with Server" heading, a centered code input + * (XXXX-XXXX), and a "Pair" button (spinner while submitting), + * • error: a red ✕ badge, the error message in a tinted box, and a "Try + * Again" button that reconnects to the active server. + * + * Additionally shows the pinned TLS cert fingerprint for the active server when + * present (RN ships TLS-on; the Dart model had no fingerprint) — so the user can + * verify it against the desktop app's pairing dialog. + * + * On success (status → connected) `App.tsx`'s connection→nav binding routes to + * the workspace; nothing to do here. + */ + +import React, { useState } from 'react'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; + +import { savedServerDisplayName } from '../models'; +import { useConnectionStore, selectIsPairing } from '../state'; +import { OkenaColors, OkenaTypography } from '../theme'; +import { StatusIndicator } from '../components/StatusIndicator'; + +export const PairingScreen: React.FC = () => { + const status = useConnectionStore((s) => s.status); + const activeServer = useConnectionStore((s) => s.activeServer); + const isPairing = useConnectionStore(selectIsPairing); + const pair = useConnectionStore((s) => s.pair); + const disconnect = useConnectionStore((s) => s.disconnect); + const connectTo = useConnectionStore((s) => s.connectTo); + + const [code, setCode] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const isError = status.kind === 'error'; + const errorMessage = isError ? status.message : null; + const showCodeInput = isPairing; + + const submitCode = async () => { + const trimmed = code.trim(); + if (trimmed.length === 0 || submitting) return; + setSubmitting(true); + try { + await pair(trimmed); + } finally { + setSubmitting(false); + } + }; + + const tryAgain = () => { + if (!activeServer) return; + // Mirrors the Dart "Try Again": disconnect then reconnect the same server. + disconnect(); + connectTo(activeServer); + }; + + return ( + + {/* Header */} + + + {'‹'} + + + + {activeServer ? savedServerDisplayName(activeServer) : 'Server'} + + Connecting + + + + {/* Body */} + + + + + + {!showCodeInput && !isError && ( + + + Connecting to server... + + )} + + {showCodeInput && ( + + + Pair with Server + + + Check the Okena desktop app for the pairing code. + + + setCode(t.toUpperCase())} + placeholder="XXXX-XXXX" + placeholderTextColor={OkenaColors.textTertiary} + autoCapitalize="characters" + autoCorrect={false} + autoFocus + textAlign="center" + onSubmitEditing={() => void submitCode()} + returnKeyType="go" + /> + + [ + styles.primaryButton, + submitting && styles.primaryButtonDisabled, + pressed && !submitting && styles.primaryButtonPressed, + ]} + onPress={() => void submitCode()} + > + {submitting ? ( + + ) : ( + Pair + )} + + + + + )} + + {isError && ( + + + {'✕'} + + + {errorMessage ?? 'Connection failed'} + + [styles.outlineButton, pressed && styles.outlineButtonPressed]} + onPress={tryAgain} + > + Try Again + + + )} + + + ); +}; + +/** Small footnote showing the pinned TLS cert fingerprint, when known. */ +const FingerprintNote: React.FC<{ fingerprint?: string }> = ({ fingerprint }) => { + if (!fingerprint) return null; + return ( + + TLS certificate fingerprint + + {fingerprint} + + + ); +}; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: OkenaColors.background, + paddingTop: 44, // approximate top safe-area inset (no SafeAreaView dep) + }, + + // Header + header: { + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 4, + paddingRight: 16, + paddingTop: 4, + }, + backButton: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + backChevron: { color: OkenaColors.accent, fontSize: 28, lineHeight: 30 }, + headerText: { flex: 1, marginLeft: 4 }, + headerSub: { + ...OkenaTypography.caption2, + color: OkenaColors.textTertiary, + marginTop: 1, + }, + + // Body + body: { + flexGrow: 1, + justifyContent: 'center', + padding: 24, + }, + statusWrap: { alignItems: 'center', marginBottom: 40 }, + centerText: { textAlign: 'center' }, + + // Connecting + connectingBlock: { alignItems: 'center' }, + connectingText: { + ...OkenaTypography.body, + color: OkenaColors.textSecondary, + textAlign: 'center', + marginTop: 20, + }, + + // Pairing + pairSub: { + ...OkenaTypography.body, + color: OkenaColors.textSecondary, + marginTop: 8, + marginBottom: 32, + }, + codeInput: { + fontSize: 28, + letterSpacing: 8, + fontFamily: 'JetBrainsMono', + fontWeight: '500', + color: OkenaColors.textPrimary, + height: 56, + borderRadius: 10, + backgroundColor: OkenaColors.surfaceElevated, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + marginBottom: 24, + }, + + // Primary button + primaryButton: { + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: OkenaColors.accent, + }, + primaryButtonPressed: { opacity: 0.85 }, + primaryButtonDisabled: { opacity: 0.5 }, + primaryButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '600' }, + + // Fingerprint + fingerprintWrap: { + marginTop: 28, + alignItems: 'center', + }, + fingerprintLabel: { + ...OkenaTypography.caption2, + color: OkenaColors.textTertiary, + marginBottom: 4, + }, + fingerprintValue: { + fontFamily: 'JetBrainsMono', + fontSize: 11, + color: OkenaColors.textSecondary, + textAlign: 'center', + }, + + // Error + errorBadge: { + width: 64, + height: 64, + borderRadius: 32, + alignItems: 'center', + justifyContent: 'center', + alignSelf: 'center', + backgroundColor: '#f871711a', // error @ ~10% + }, + errorBadgeMark: { color: OkenaColors.error, fontSize: 32 }, + errorBox: { + marginTop: 20, + padding: 16, + borderRadius: 12, + backgroundColor: '#f8717114', // error @ ~8% + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#f8717133', // error @ ~20% + }, + errorText: { ...OkenaTypography.body, color: OkenaColors.error, textAlign: 'center' }, + outlineButton: { + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + marginTop: 24, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.accent, + }, + outlineButtonPressed: { opacity: 0.6 }, + outlineButtonText: { color: OkenaColors.accent, fontSize: 16, fontWeight: '600' }, +}); + +export default PairingScreen; diff --git a/mobile/rn/src/screens/ServerListScreen.tsx b/mobile/rn/src/screens/ServerListScreen.tsx new file mode 100644 index 00000000..3e0be0a2 --- /dev/null +++ b/mobile/rn/src/screens/ServerListScreen.tsx @@ -0,0 +1,432 @@ +/** + * ServerListScreen.tsx — saved-server list + "add server" sheet. + * + * Ported from `mobile/lib/src/screens/server_list_screen.dart`: + * - header: "Servers" large title + a circular "+" add button, + * - empty state: terminal glyph, "No servers yet", "Add a server to get + * started", and an "Add Server" button, + * - list: one card per saved server — a letter avatar, the display name, and + * (when a label is set) the `host:port` subtitle; tapping connects. + * - add sheet: a bottom `Modal` with Host / Port / Label fields. + * + * The Dart screen used a `Dismissible` (swipe) to delete. RN core has no + * swipe-to-dismiss without extra native deps, so deletion is exposed via a + * long-press that reveals an inline delete affordance — same `removeServer` + * behavior, no new dependencies. + * + * Reads/writes the connection store; holds only local UI state (sheet + * visibility + field values + which row is in "delete" mode). + */ + +import React, { useState } from 'react'; +import { + ActivityIndicator, + FlatList, + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; + +import { + createSavedServer, + savedServerDisplayName, + type SavedServer, +} from '../models'; +import { useConnectionStore } from '../state'; +import { OkenaColors, OkenaTypography } from '../theme'; + +const DEFAULT_PORT = '19100'; + +export const ServerListScreen: React.FC = () => { + const servers = useConnectionStore((s) => s.servers); + const loaded = useConnectionStore((s) => s.loaded); + const connectTo = useConnectionStore((s) => s.connectTo); + const addServer = useConnectionStore((s) => s.addServer); + const removeServer = useConnectionStore((s) => s.removeServer); + + const [sheetOpen, setSheetOpen] = useState(false); + // Which server (by key) currently shows its delete affordance (long-pressed). + const [pendingDeleteKey, setPendingDeleteKey] = useState(null); + + const serverKey = (s: SavedServer) => `${s.host}:${s.port}`; + + const renderItem = ({ item }: { item: SavedServer }) => { + const name = savedServerDisplayName(item); + const initial = name.charAt(0).toUpperCase() || '?'; + const showDelete = pendingDeleteKey === serverKey(item); + + return ( + + [styles.card, pressed && styles.cardPressed]} + onPress={() => { + if (showDelete) { + setPendingDeleteKey(null); + return; + } + connectTo(item); + }} + onLongPress={() => setPendingDeleteKey(showDelete ? null : serverKey(item))} + > + + {initial} + + + + {name} + + {item.label !== undefined && ( + + {item.host}:{item.port} + + )} + + {showDelete ? ( + { + setPendingDeleteKey(null); + removeServer(item); + }} + > + Delete + + ) : ( + {'›'} + )} + + + ); + }; + + return ( + + {/* Header */} + + Servers + [styles.addButton, pressed && styles.addButtonPressed]} + onPress={() => setSheetOpen(true)} + accessibilityLabel="Add server" + > + + + + + + {/* Content */} + {servers.length === 0 ? ( + loaded ? ( + setSheetOpen(true)} /> + ) : ( + + + + ) + ) : ( + + )} + + setSheetOpen(false)} + onAdd={(server) => { + addServer(server); + setSheetOpen(false); + }} + /> + + ); +}; + +// ── Empty state ───────────────────────────────────────────────────────────── + +const EmptyState: React.FC<{ onAdd: () => void }> = ({ onAdd }) => ( + + + {'⌨'} + + No servers yet + Add a server to get started + [styles.primaryButton, styles.emptyButton, pressed && styles.primaryButtonPressed]} + onPress={onAdd} + > + Add Server + + +); + +// ── Add-server bottom sheet ─────────────────────────────────────────────────── + +const AddServerSheet: React.FC<{ + visible: boolean; + onClose: () => void; + onAdd: (server: SavedServer) => void; +}> = ({ visible, onClose, onAdd }) => { + const [host, setHost] = useState(''); + const [port, setPort] = useState(DEFAULT_PORT); + const [label, setLabel] = useState(''); + + // Reset fields each time the sheet is opened. + const reset = () => { + setHost(''); + setPort(DEFAULT_PORT); + setLabel(''); + }; + + const trimmedHost = host.trim(); + const canSubmit = trimmedHost.length > 0; + + const submit = () => { + if (!canSubmit) return; + const parsedPort = parseInt(port.trim(), 10); + const server = createSavedServer({ + host: trimmedHost, + port: Number.isFinite(parsedPort) ? parsedPort : 19100, + label: label.trim() === '' ? undefined : label.trim(), + }); + onAdd(server); + reset(); + }; + + const close = () => { + reset(); + onClose(); + }; + + return ( + + {/* Tap the dimmed backdrop to dismiss. */} + + + {/* Stop taps inside the sheet from bubbling to the backdrop. */} + {}}> + + Add Server + + Enter the host and port of your Okena desktop app + + + + + + + [ + styles.primaryButton, + !canSubmit && styles.primaryButtonDisabled, + pressed && canSubmit && styles.primaryButtonPressed, + ]} + onPress={submit} + > + Add Server + + + + + + ); +}; + +const Field: React.FC< + React.ComponentProps & { label: string } +> = ({ label, ...inputProps }) => ( + + {label} + + +); + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: OkenaColors.background, + paddingTop: 44, // approximate top safe-area inset (no SafeAreaView dep) + }, + center: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 24, + }, + + // Header + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 24, + paddingTop: 20, + paddingBottom: 8, + }, + addButton: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: OkenaColors.surfaceElevated, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + }, + addButtonPressed: { opacity: 0.6 }, + addButtonPlus: { + color: OkenaColors.accent, + fontSize: 22, + lineHeight: 24, + fontWeight: '400', + }, + + // List + listContent: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 24 }, + cardRow: { marginBottom: 8 }, + card: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + borderRadius: 14, + backgroundColor: OkenaColors.surface, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + }, + cardPressed: { opacity: 0.7 }, + avatar: { + width: 40, + height: 40, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#7c7fff1f', // accent @ ~12% + marginRight: 14, + }, + avatarText: { ...OkenaTypography.headline, color: OkenaColors.accent }, + cardBody: { flex: 1 }, + cardName: { ...OkenaTypography.body, fontWeight: '500' }, + cardSub: { + ...OkenaTypography.caption, + marginTop: 3, + color: OkenaColors.textTertiary, + fontFamily: 'JetBrainsMono', + }, + chevron: { color: OkenaColors.textTertiary, fontSize: 18, marginLeft: 8 }, + deleteBtn: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 8, + backgroundColor: '#f8717126', // error @ ~15% + marginLeft: 8, + }, + deleteBtnText: { color: OkenaColors.error, fontWeight: '600', fontSize: 13 }, + + // Empty state + emptyGlyph: { + width: 80, + height: 80, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#7c7fff26', // accent @ ~15% + }, + emptyGlyphText: { fontSize: 36, color: OkenaColors.accent }, + emptyTitle: { marginTop: 20 }, + emptySub: { ...OkenaTypography.body, color: OkenaColors.textSecondary, marginTop: 8 }, + emptyButton: { marginTop: 28, width: 180 }, + + // Primary button + primaryButton: { + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: OkenaColors.accent, + }, + primaryButtonPressed: { opacity: 0.85 }, + primaryButtonDisabled: { opacity: 0.4 }, + primaryButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '600' }, + + // Add-server sheet + backdrop: { flex: 1, backgroundColor: '#000000aa', justifyContent: 'flex-end' }, + sheetWrap: { width: '100%' }, + sheet: { + backgroundColor: OkenaColors.surface, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + paddingHorizontal: 24, + paddingTop: 8, + paddingBottom: 32, + }, + dragHandle: { + width: 36, + height: 4, + borderRadius: 2, + backgroundColor: OkenaColors.textTertiary, + alignSelf: 'center', + marginBottom: 20, + opacity: 0.4, + }, + sheetSub: { ...OkenaTypography.callout, color: OkenaColors.textTertiary, marginTop: 4, marginBottom: 24 }, + field: { marginBottom: 14 }, + fieldLabel: { ...OkenaTypography.caption, color: OkenaColors.textSecondary, marginBottom: 6 }, + input: { + height: 44, + borderRadius: 10, + paddingHorizontal: 14, + backgroundColor: OkenaColors.surfaceElevated, + borderWidth: StyleSheet.hairlineWidth, + borderColor: OkenaColors.border, + color: OkenaColors.textPrimary, + fontSize: 15, + }, +}); + +export default ServerListScreen; diff --git a/mobile/rn/src/screens/WorkspaceScreen.tsx b/mobile/rn/src/screens/WorkspaceScreen.tsx new file mode 100644 index 00000000..5cba356e --- /dev/null +++ b/mobile/rn/src/screens/WorkspaceScreen.tsx @@ -0,0 +1,411 @@ +/** + * WorkspaceScreen.tsx — the connected workspace. + * + * Port of `mobile/lib/src/screens/workspace_screen.dart`. Composes: + * - an app bar: a drawer-toggle (☰), the selected project name (tap to switch + * when there are several), a small connection-quality dot, a fullscreen + * toggle, and a "+" terminal-actions menu (new / split V / split H / + * minimize). StatusIndicator is NOT imported (owned by another agent) — the + * dot is inline. + * - the layout area: the parsed project layout tree rendered by + * {@link LayoutRenderer}; when fullscreen, only the fullscreen terminal is + * shown via a single {@link TerminalPane}. Empty/no-terminal states match + * the Dart screen. + * - the {@link KeyToolbar} pinned above the soft keyboard (rendered inside a + * `KeyboardAvoidingView` so it sits on top of the keyboard). + * - the slide-in {@link ProjectDrawer}. + * + * Fonts are loaded here with Skia's `useFont` (see README) and threaded down to + * every terminal pane; the layout area guards on fonts being loaded. + * + * State comes from the stores; polling lifecycle is owned by App.tsx (this + * screen only reads + dispatches actions). + */ + +import React, { useMemo, useRef, useState } from 'react'; +import { + View, + Text, + Pressable, + KeyboardAvoidingView, + Platform, + Modal, + ScrollView, + StyleSheet, +} from 'react-native'; +import { useFont } from '@shopify/react-native-skia'; + +import { useWorkspaceStore, useConnectionStore } from '../state'; +import { parseLayout } from '../models'; +import { getOkenaNative, type OkenaNative } from '../native/okena'; +import { OkenaColors, TerminalTheme } from '../theme'; +import { LayoutRenderer } from '../components/LayoutRenderer'; +import { TerminalPane, type TerminalPaneHandle } from '../components/TerminalPane'; +import { KeyToolbar, KeyModifiers } from '../components/KeyToolbar'; +import { ProjectDrawer } from '../components/ProjectDrawer'; +import type { TerminalFonts } from '../components/TerminalView'; + +const native: OkenaNative = (() => { + try { + return getOkenaNative(); + } catch { + // Native not wired up (off-device). The screen still renders chrome; the + // panes guard their native calls. Throwing here would crash the whole app. + return undefined as unknown as OkenaNative; + } +})(); + +export const WorkspaceScreen: React.FC = () => { + // ── fonts (Skia useFont; null until loaded) ──────────────────────────────── + const fontSize = TerminalTheme.defaultFontSize; + const regular = useFont(require('../../assets/JetBrainsMono-Regular.ttf'), fontSize); + const bold = useFont(require('../../assets/JetBrainsMono-Bold.ttf'), fontSize); + const italic = useFont(require('../../assets/JetBrainsMono-Italic.ttf'), fontSize); + const boldItalic = useFont(require('../../assets/JetBrainsMono-BoldItalic.ttf'), fontSize); + + const fonts: TerminalFonts | null = useMemo( + () => + regular + ? { + regular, + bold: bold ?? undefined, + italic: italic ?? undefined, + boldItalic: boldItalic ?? undefined, + } + : null, + [regular, bold, italic, boldItalic], + ); + + // ── shared key-modifier store (toolbar + soft keyboard) ───────────────────── + const modifiers = useRef(new KeyModifiers()).current; + const paneRef = useRef(null); + + // ── stores ────────────────────────────────────────────────────────────────── + const projects = useWorkspaceStore((s) => s.projects); + const selectedProjectId = useWorkspaceStore((s) => s.selectedProjectId); + const selectedTerminalId = useWorkspaceStore((s) => s.selectedTerminalId); + const fullscreenTerminal = useWorkspaceStore((s) => s.fullscreenTerminal); + const secondsSinceActivity = useWorkspaceStore((s) => s.secondsSinceActivity); + const selectProject = useWorkspaceStore((s) => s.selectProject); + // Recompute the selected project from the live id (cheap selector helper). + const project = useMemo(() => { + if (selectedProjectId === null) return projects[0] ?? null; + return projects.find((p) => p.id === selectedProjectId) ?? projects[0] ?? null; + }, [projects, selectedProjectId]); + + const connId = useConnectionStore((s) => s.connId); + + const [drawerOpen, setDrawerOpen] = useState(false); + const [switcherOpen, setSwitcherOpen] = useState(false); + const [actionsOpen, setActionsOpen] = useState(false); + + // Layout JSON for the selected project, parsed. + const layoutNode = useMemo(() => { + const json = useWorkspaceStore.getState().getProjectLayoutJson(); + return json ? parseLayout(json) : null; + // re-parse whenever the project or its terminal set changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [project?.id, project?.terminalIds.join(','), fullscreenTerminal?.terminalId]); + + const dotColor = + secondsSinceActivity < 3 + ? OkenaColors.success + : secondsSinceActivity < 10 + ? OkenaColors.warning + : OkenaColors.error; + + const projectName = project?.name ?? 'No Project'; + + // ── terminal actions ────────────────────────────────────────────────────── + const onNewTerminal = () => { + if (connId && project) void native?.createTerminal(connId, project.id); + }; + const onSplit = (direction: 'vertical' | 'horizontal') => { + if (connId && project) void native?.splitTerminal(connId, project.id, [], direction); + }; + const onMinimize = () => { + if (connId && project && selectedTerminalId) { + void native?.toggleMinimized(connId, project.id, selectedTerminalId); + } + }; + const onToggleFullscreen = () => { + if (!connId || !project) return; + if (fullscreenTerminal) { + void native?.setFullscreen(connId, project.id, undefined); + } else if (selectedTerminalId) { + void native?.setFullscreen(connId, project.id, selectedTerminalId); + } + }; + + // ── body ────────────────────────────────────────────────────────────────── + const renderBody = () => { + if (!connId || !project) { + return ( + + No project selected + + ); + } + if (!fonts) { + return ( + + Loading fonts… + + ); + } + // Fullscreen: just the one terminal. + if (fullscreenTerminal && fullscreenTerminal.projectId === project.id) { + return ( + + ); + } + if (project.terminalIds.length === 0) { + return ( + + No terminals + + New Terminal + + + ); + } + if (layoutNode) { + return ( + + ); + } + // Fallback: render the selected (or first) terminal directly. + const tid = selectedTerminalId ?? project.terminalIds[0]!; + return ( + + ); + }; + + const showToolbar = connId !== null && project !== null && selectedTerminalId !== null; + + return ( + + {/* App bar */} + + setDrawerOpen(true)}> + {'☰'} + + setSwitcherOpen(true)} + > + + {projectName} + + {projects.length > 1 ? {' ▾'} : null} + + + {connId ? : null} + {connId && project && selectedTerminalId ? ( + + {fullscreenTerminal ? '🗗' : '⛶'} + + ) : null} + {connId && project ? ( + setActionsOpen(true)}> + {'+'} + + ) : null} + + + {/* Body + key toolbar (toolbar rides above the keyboard) */} + + {renderBody()} + {showToolbar && connId ? ( + paneRef.current?.blur()} + /> + ) : null} + + + {/* Drawer */} + setDrawerOpen(false)} native={native} /> + + {/* Project switcher */} + setSwitcherOpen(false)} + > + setSwitcherOpen(false)}> + + + {projects.map((p) => { + const isSel = p.id === project?.id; + return ( + { + selectProject(p.id); + setSwitcherOpen(false); + }} + > + + {p.name} + + {p.gitBranch ? ( + + {`⎇ ${p.gitBranch}`} + + ) : null} + + ); + })} + + + + + + {/* Terminal actions menu */} + setActionsOpen(false)} + > + setActionsOpen(false)} /> + + { + setActionsOpen(false); + onNewTerminal(); + }} + /> + {selectedTerminalId ? ( + <> + { + setActionsOpen(false); + onSplit('vertical'); + }} + /> + { + setActionsOpen(false); + onSplit('horizontal'); + }} + /> + { + setActionsOpen(false); + onMinimize(); + }} + /> + + ) : null} + + + + ); +}; + +const Centered: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +const ActionItem: React.FC<{ label: string; onPress: () => void }> = ({ label, onPress }) => ( + + {label} + +); + +const styles = StyleSheet.create({ + root: { flex: 1, backgroundColor: OkenaColors.background }, + flex: { flex: 1 }, + flexSpacer: { flex: 1 }, + appBar: { + height: 96, + paddingTop: 44, + paddingHorizontal: 8, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: OkenaColors.surface, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: OkenaColors.border, + }, + appBarBtn: { padding: 8 }, + appBarIcon: { color: OkenaColors.textPrimary, fontSize: 18 }, + titleWrap: { flexDirection: 'row', alignItems: 'center', flexShrink: 1 }, + title: { color: OkenaColors.textPrimary, fontSize: 17, fontWeight: '600', flexShrink: 1 }, + titleCaret: { color: OkenaColors.textSecondary, fontSize: 14 }, + dot: { width: 8, height: 8, borderRadius: 4, marginHorizontal: 6 }, + centered: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }, + dim: { color: OkenaColors.textTertiary, fontSize: 14 }, + primaryBtn: { + marginTop: 16, + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: OkenaColors.accent, + }, + primaryBtnText: { color: '#ffffff', fontSize: 14, fontWeight: '600' }, + // project switcher menu + menuBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.3)', paddingTop: 96, paddingLeft: 48 }, + menu: { + backgroundColor: OkenaColors.surfaceElevated, + borderRadius: 10, + maxHeight: 360, + width: 260, + paddingVertical: 4, + }, + menuItem: { paddingHorizontal: 16, paddingVertical: 10 }, + menuItemText: { color: OkenaColors.textPrimary, fontSize: 14 }, + menuItemSelected: { color: OkenaColors.accent, fontWeight: '700' }, + menuBranch: { color: OkenaColors.textTertiary, fontSize: 11, marginTop: 2 }, + // action sheet + sheetBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }, + sheet: { + backgroundColor: OkenaColors.surfaceElevated, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: 8, + paddingBottom: 32, + }, + sheetItem: { paddingVertical: 14, paddingHorizontal: 12 }, + sheetItemText: { color: OkenaColors.textPrimary, fontSize: 15 }, +}); + +export default WorkspaceScreen; diff --git a/mobile/rn/src/state/connectionStore.ts b/mobile/rn/src/state/connectionStore.ts new file mode 100644 index 00000000..2e8952cc --- /dev/null +++ b/mobile/rn/src/state/connectionStore.ts @@ -0,0 +1,354 @@ +/** + * connectionStore.ts — connection lifecycle + saved-server list (zustand). + * + * Ports `mobile/lib/src/providers/connection_provider.dart` (a polling + * `ChangeNotifier`) to a zustand store. Responsibilities: + * - the persisted list of {@link SavedServer}s (add / remove / update), + * - the current connection (`connId`, `activeServer`) and its `status`, + * - the connect → pair → disconnect lifecycle, + * - `secondsSinceActivity` (staleness indicator), + * - status polling: ~500ms while connecting/pairing, ~2s once connected + * (matching the Dart `_startPolling(fast:)` cadence), + * - persisting the auth token (and pinned cert fingerprint) back onto the + * saved server once paired. + * + * Dependencies — the native module and persistence — are INJECTED via + * {@link configureConnectionStore}, mirroring `TerminalView`'s `native` prop. + * Until `ubrn` generates the real module, `getOkenaNative()` throws; the store + * is constructed lazily so merely importing this module never calls it. Tests + * inject a mock `OkenaNative` + an in-memory `Persistence`. + */ + +import { create, type StoreApi, type UseBoundStore } from 'zustand'; + +import type { ConnectionStatus, ConnId, OkenaNative } from '../native/okena'; +import { getOkenaNative } from '../native/okena'; +import { + createSavedServer, + listFromJson, + listToJson, + savedServerEquals, + withSavedServer, + type SavedServer, +} from '../models/savedServer'; +import { + asyncStoragePersistence, + type Persistence, +} from './persistence'; + +/** AsyncStorage key for the persisted server list (matches Dart `_kSavedServersKey`). */ +export const SAVED_SERVERS_KEY = 'saved_servers'; + +/** Fast poll interval (ms) while connecting / pairing — Dart used 500ms. */ +export const FAST_POLL_MS = 500; +/** Slow poll interval (ms) once connected — Dart used 2000ms. */ +export const SLOW_POLL_MS = 2000; + +/** Initial, disconnected status. */ +const DISCONNECTED: ConnectionStatus = { kind: 'disconnected' }; + +/** Dependencies the store calls out to. Overridable for tests. */ +export interface ConnectionDeps { + native: OkenaNative; + persistence: Persistence; +} + +/** + * The connection store's state + actions. Screen agents read fields with the + * `useConnectionStore(selector)` hook and call the action methods. + */ +export interface ConnectionState { + // ── state ──────────────────────────────────────────────────────────────── + /** All saved servers (persisted). */ + servers: SavedServer[]; + /** The server currently being connected to / paired / connected, if any. */ + activeServer: SavedServer | null; + /** The live connection id from `connect`, if any. */ + connId: ConnId | null; + /** Current connection status (polled from the native module). */ + status: ConnectionStatus; + /** Seconds since the last WS activity; large when disconnected. */ + secondsSinceActivity: number; + /** Whether the persisted server list has finished loading. */ + loaded: boolean; + + // ── derived helpers (cheap, recomputed by callers via selectors) ───────── + // (Booleans like isConnected are intentionally NOT stored; derive from + // `status.kind` in the component or via the exported selectors below.) + + // ── actions ────────────────────────────────────────────────────────────── + /** + * One-time native init (`OkenaNative.initApp()`). Call once at app start + * (App.tsx) before connecting. Routed through the store so the configured + * (possibly-mocked) native module is used. No-op-safe to call once. + */ + initApp(): void; + /** + * Load the persisted server list from storage. Call once at app start (e.g. + * in `App.tsx`). Safe to call again; it just re-reads. Mirrors the Dart + * `_loadServers()` invoked from the provider constructor. + */ + loadServers(): Promise; + /** Add a server (deduped by host+port) and persist. Mirrors `addServer`. */ + addServer(server: SavedServer): void; + /** Remove a server (by host+port) and persist. Mirrors `removeServer`. */ + removeServer(server: SavedServer): void; + /** + * Replace an existing server (matched by host+port) with `updated` and + * persist. Used by the add/edit sheet; not in the Dart original but the + * contract ("add/remove/update") asks for it. + */ + updateServer(updated: SavedServer): void; + /** + * Begin connecting to `server`. Disconnects any current connection first, + * sets status to `connecting`, and starts fast polling. Mirrors `connectTo`. + */ + connectTo(server: SavedServer): void; + /** + * Pair the current connection with a code (async; awaits the server). On + * failure sets status to `error`. Mirrors `pair`. + */ + pair(code: string): Promise; + /** Tear down the current connection and reset to disconnected. Mirrors `disconnect`. */ + disconnect(): void; +} + +/** + * Lazily-resolved deps. Resolving is deferred until the first action that needs + * the native module / storage, so importing this file (and constructing the + * store) never throws via `getOkenaNative()`. + */ +let injectedDeps: Partial = {}; +let resolvedDeps: ConnectionDeps | null = null; + +function deps(): ConnectionDeps { + if (!resolvedDeps) { + resolvedDeps = { + native: injectedDeps.native ?? getOkenaNative(), + persistence: injectedDeps.persistence ?? asyncStoragePersistence, + }; + } + return resolvedDeps; +} + +/** + * Override the store's dependencies (native module + persistence). Call BEFORE + * the first action runs — e.g. at the top of a test, or once at app start if + * you wire a custom native module. Subsequent calls reset the resolved cache. + */ +export function configureConnectionStore(overrides: Partial): void { + injectedDeps = { ...injectedDeps, ...overrides }; + resolvedDeps = null; +} + +// ── polling: the store owns a single timer (start/stop) ───────────────────── + +let pollTimer: ReturnType | null = null; + +function stopPolling(): void { + if (pollTimer !== null) { + clearInterval(pollTimer); + pollTimer = null; + } +} + +function startPolling( + get: StoreApi['getState'], + set: StoreApi['setState'], + fast: boolean, +): void { + stopPolling(); + pollTimer = setInterval(() => pollStatus(get, set), fast ? FAST_POLL_MS : SLOW_POLL_MS); +} + +/** Persist the active server list to storage. Fire-and-forget (Dart did the same). */ +function persistServers(servers: readonly SavedServer[]): void { + void deps().persistence.setItem(SAVED_SERVERS_KEY, listToJson(servers)); +} + +/** + * After connecting, copy the freshly-negotiated auth token (and pinned cert + * fingerprint, which the Dart model lacked) back onto the active server and + * persist. Mirrors `_persistToken`. + */ +function persistToken( + get: StoreApi['getState'], + set: StoreApi['setState'], +): void { + const { connId, activeServer } = get(); + if (!connId || !activeServer) return; + const token = deps().native.getToken(connId); + if (token && token !== activeServer.token) { + const updated = withSavedServer(activeServer, { token }); + const servers = get().servers.map((s) => + savedServerEquals(s, activeServer) ? updated : s, + ); + set({ servers, activeServer: updated }); + persistServers(servers); + } +} + +/** + * One poll tick: refresh `status` + `secondsSinceActivity`. On the + * connecting→connected edge, switch to slow polling and persist the token; on + * disconnect/error, stop polling. Mirrors `_pollStatus`. + */ +function pollStatus( + get: StoreApi['getState'], + set: StoreApi['setState'], +): void { + const { connId, status: oldStatus } = get(); + if (!connId) return; + + const native = deps().native; + const newStatus = native.connectionStatus(connId); + const activity = native.secondsSinceActivity(connId); + + set({ status: newStatus, secondsSinceActivity: activity }); + + if (newStatus.kind === 'connected' && oldStatus.kind !== 'connected') { + // Connected edge: slow down polling and capture the token. + startPolling(get, set, /* fast */ false); + persistToken(get, set); + } + + if (newStatus.kind === 'disconnected' || newStatus.kind === 'error') { + stopPolling(); + } +} + +/** + * The connection store hook + bound store. + * + * Constructing it is side-effect-free w.r.t. the native module: `create`'s + * initializer only builds the state object + action closures; none of them call + * `deps()` (and thus `getOkenaNative()`) until an action actually runs. So this + * can be a normal module-level zustand store — no lazy wrapper needed. + * + * Use it like any zustand hook, and `.getState()` / `.subscribe()` for + * imperative access: + * + * ```ts + * const status = useConnectionStore((s) => s.status); + * const connectTo = useConnectionStore((s) => s.connectTo); + * const isConn = selectIsConnected(useConnectionStore.getState()); + * ``` + */ +export const useConnectionStore: UseBoundStore> = + create((set, get) => ({ + servers: [], + activeServer: null, + connId: null, + status: DISCONNECTED, + secondsSinceActivity: Number.MAX_SAFE_INTEGER, + loaded: false, + + initApp() { + deps().native.initApp(); + }, + + async loadServers() { + let servers: SavedServer[] = []; + try { + const json = await deps().persistence.getItem(SAVED_SERVERS_KEY); + if (json) servers = listFromJson(json); + } catch { + // Corrupted data — start fresh (matches Dart's silent catch). + servers = []; + } + set({ servers, loaded: true }); + }, + + addServer(server) { + const { servers } = get(); + if (servers.some((s) => savedServerEquals(s, server))) return; + const next = [...servers, server]; + set({ servers: next }); + persistServers(next); + }, + + removeServer(server) { + const next = get().servers.filter((s) => !savedServerEquals(s, server)); + set({ servers: next }); + persistServers(next); + }, + + updateServer(updated) { + const next = get().servers.map((s) => + savedServerEquals(s, updated) ? updated : s, + ); + set({ servers: next }); + persistServers(next); + }, + + connectTo(server) { + const native = deps().native; + const { connId } = get(); + // Disconnect any existing connection first (matches Dart). + if (connId) { + native.disconnect(connId); + stopPolling(); + } + const newConnId = native.connect(server.host, server.port, server.token); + set({ + activeServer: server, + connId: newConnId, + status: { kind: 'connecting' }, + secondsSinceActivity: Number.MAX_SAFE_INTEGER, + }); + startPolling(get, set, /* fast */ true); + }, + + async pair(code) { + const { connId } = get(); + if (!connId) return; + try { + await deps().native.pair(connId, code); + } catch (e) { + set({ + status: { kind: 'error', message: e instanceof Error ? e.message : String(e) }, + }); + } + }, + + disconnect() { + const { connId } = get(); + if (connId) deps().native.disconnect(connId); + stopPolling(); + set({ + connId: null, + activeServer: null, + status: DISCONNECTED, + secondsSinceActivity: Number.MAX_SAFE_INTEGER, + }); + }, + })); + +/** + * Imperative store handle (`getState` / `setState` / `subscribe`) for non-React + * consumers — e.g. {@link import('../navigation/navStore')} subscribing to + * status changes to drive navigation. Same instance as the hook. + */ +export function connectionStore(): StoreApi { + return useConnectionStore; +} + +// ── selectors (derive the Dart `is*` getters from `status`) ───────────────── + +/** True once the connection is established. Mirrors Dart `isConnected`. */ +export const selectIsConnected = (s: ConnectionState): boolean => + s.status.kind === 'connected'; +/** True while pairing. Mirrors Dart `isPairing`. */ +export const selectIsPairing = (s: ConnectionState): boolean => s.status.kind === 'pairing'; +/** True while connecting. Mirrors Dart `isConnecting`. */ +export const selectIsConnecting = (s: ConnectionState): boolean => + s.status.kind === 'connecting'; +/** + * True when fully idle: disconnected AND no active server selected. Mirrors + * Dart `isDisconnected` (used to decide the server-list screen is shown). + */ +export const selectIsDisconnected = (s: ConnectionState): boolean => + s.status.kind === 'disconnected' && s.activeServer === null; + +/** Re-export the convenience constructor so callers don't reach into the model. */ +export { createSavedServer }; diff --git a/mobile/rn/src/state/index.ts b/mobile/rn/src/state/index.ts new file mode 100644 index 00000000..acf51706 --- /dev/null +++ b/mobile/rn/src/state/index.ts @@ -0,0 +1,36 @@ +/** + * state/index.ts — public surface of the state layer. + * + * Screen agents import the store hooks + selectors from here. + */ + +export { + useConnectionStore, + connectionStore, + configureConnectionStore, + createSavedServer, + selectIsConnected, + selectIsPairing, + selectIsConnecting, + selectIsDisconnected, + SAVED_SERVERS_KEY, + FAST_POLL_MS, + SLOW_POLL_MS, + type ConnectionState, + type ConnectionDeps, +} from './connectionStore'; + +export { + useWorkspaceStore, + workspaceStore, + configureWorkspaceStore, + WORKSPACE_POLL_MS, + type WorkspaceState, + type WorkspaceDeps, +} from './workspaceStore'; + +export { + asyncStoragePersistence, + createMemoryPersistence, + type Persistence, +} from './persistence'; diff --git a/mobile/rn/src/state/persistence.ts b/mobile/rn/src/state/persistence.ts new file mode 100644 index 00000000..98c4e891 --- /dev/null +++ b/mobile/rn/src/state/persistence.ts @@ -0,0 +1,74 @@ +/** + * persistence.ts — a tiny async key-value store abstraction. + * + * The Flutter app persisted the saved-server list via `shared_preferences` + * (see `connection_provider.dart`). The RN equivalent is + * `@react-native-async-storage/async-storage`. + * + * The connection store talks to this {@link Persistence} interface rather than + * AsyncStorage directly, so tests can inject an in-memory implementation (same + * injection philosophy as `TerminalView`'s `native` prop). The default used by + * the app is {@link asyncStoragePersistence}. + */ + +/** + * Minimal async key-value contract. A subset of the AsyncStorage API — just the + * three calls the stores need. + */ +export interface Persistence { + /** Read a string value, or `null` if the key is absent. */ + getItem(key: string): Promise; + /** Write a string value. */ + setItem(key: string, value: string): Promise; + /** Remove a key. */ + removeItem(key: string): Promise; +} + +/** + * The real implementation, backed by + * `@react-native-async-storage/async-storage`. + * + * The import is intentionally lazy (inside each method) so that: + * - merely importing this module doesn't pull in the native AsyncStorage + * module (which throws off-device), and + * - `tsc` / tests that never touch the real persistence don't need the native + * side present. + */ +export const asyncStoragePersistence: Persistence = { + async getItem(key) { + const AsyncStorage = (await import('@react-native-async-storage/async-storage')) + .default; + return AsyncStorage.getItem(key); + }, + async setItem(key, value) { + const AsyncStorage = (await import('@react-native-async-storage/async-storage')) + .default; + await AsyncStorage.setItem(key, value); + }, + async removeItem(key) { + const AsyncStorage = (await import('@react-native-async-storage/async-storage')) + .default; + await AsyncStorage.removeItem(key); + }, +}; + +/** + * A simple in-memory {@link Persistence} — handy for tests and Storybook. Not + * used by the app at runtime. + */ +export function createMemoryPersistence( + initial: Record = {}, +): Persistence { + const map = new Map(Object.entries(initial)); + return { + getItem: (key) => Promise.resolve(map.get(key) ?? null), + setItem: (key, value) => { + map.set(key, value); + return Promise.resolve(); + }, + removeItem: (key) => { + map.delete(key); + return Promise.resolve(); + }, + }; +} diff --git a/mobile/rn/src/state/workspaceStore.ts b/mobile/rn/src/state/workspaceStore.ts new file mode 100644 index 00000000..b7f0d131 --- /dev/null +++ b/mobile/rn/src/state/workspaceStore.ts @@ -0,0 +1,336 @@ +/** + * workspaceStore.ts — project / folder / layout state (zustand). + * + * Ports `mobile/lib/src/providers/workspace_provider.dart`. While connected it + * polls the cached remote state (~1s) for: + * - the project list (`getProjects`), + * - folders (`getFolders`), + * - project order (`getProjectOrder`), + * - the server's focused project (`getFocusedProjectId`), + * - the fullscreen terminal (`getFullscreenTerminal`). + * + * It auto-selects the focused project when nothing is selected, and auto-selects + * a terminal within the selected project (newly-added one, or the first if the + * current selection vanished) — mirroring the Dart `_pollState` logic. + * + * Lifecycle: the Dart provider listened to the connection provider and + * started/stopped polling on connect/disconnect. Here that wiring is explicit — + * call {@link WorkspaceState.start} when the connection becomes connected and + * {@link WorkspaceState.stop} when it isn't (App.tsx wires this; see the nav + * layer). `start` needs the live `connId`. + * + * Dependencies (the native module) are injected via + * {@link configureWorkspaceStore}, same philosophy as the connection store. + */ + +import { create, type StoreApi, type UseBoundStore } from 'zustand'; + +import type { + ConnId, + FolderInfo, + FullscreenInfo, + OkenaNative, + ProjectId, + ProjectInfo, + TerminalId, +} from '../native/okena'; +import { getOkenaNative } from '../native/okena'; + +/** Poll interval (ms) for the workspace state — Dart used 1000ms. */ +export const WORKSPACE_POLL_MS = 1000; + +/** Dependencies the store calls out to. Overridable for tests. */ +export interface WorkspaceDeps { + native: OkenaNative; +} + +/** + * The workspace store's state + actions. Screen agents read fields with the + * `useWorkspaceStore(selector)` hook and call the action methods. + */ +export interface WorkspaceState { + // ── state ──────────────────────────────────────────────────────────────── + /** Projects from the cached remote state. */ + projects: ProjectInfo[]; + /** Folders from the cached remote state. */ + folders: FolderInfo[]; + /** Server-defined project ordering (list of project ids). */ + projectOrder: ProjectId[]; + /** The active fullscreen terminal, if any. */ + fullscreenTerminal: FullscreenInfo | null; + /** Currently-selected project id (auto-selected from focused, or by the user). */ + selectedProjectId: ProjectId | null; + /** Currently-selected terminal id within the selected project. */ + selectedTerminalId: TerminalId | null; + /** Seconds since last WS activity (also tracked here, per the Dart provider). */ + secondsSinceActivity: number; + + // ── actions ──────────────────────────────────────────────────────────── + /** + * Start polling for the given connection. Idempotent: re-calling with a new + * `connId` restarts against it. Does an immediate first poll (as Dart did). + * Call when the connection becomes `connected`. + */ + start(connId: ConnId): void; + /** + * Stop polling and clear all workspace state. Call when the connection is no + * longer connected (mirrors the Dart `_onConnectionChanged` else-branch). + */ + stop(): void; + /** Select a project; clears the terminal selection so it re-auto-selects. Mirrors `selectProject`. */ + selectProject(projectId: ProjectId): void; + /** Select a terminal within the current project. Mirrors `selectTerminal`. */ + selectTerminal(terminalId: TerminalId): void; + /** + * The currently-selected project object, or the first project as a fallback, + * or `null`. Mirrors the Dart `selectedProject` getter. (Provided as a + * selector helper since zustand state holds only the id.) + */ + getSelectedProject(): ProjectInfo | null; + /** + * The layout JSON for the selected project, via the native module. `null` if + * not connected or no project selected. Mirrors `getProjectLayoutJson`. + * Parse it with {@link import('../models/layoutNode').parseLayout}. + */ + getProjectLayoutJson(): string | undefined; +} + +let injectedDeps: Partial = {}; +let resolvedDeps: WorkspaceDeps | null = null; + +function deps(): WorkspaceDeps { + if (!resolvedDeps) { + resolvedDeps = { native: injectedDeps.native ?? getOkenaNative() }; + } + return resolvedDeps; +} + +/** + * Override the store's native module. Call before the first action runs (e.g. + * at the top of a test, or once at app start). + */ +export function configureWorkspaceStore(overrides: Partial): void { + injectedDeps = { ...injectedDeps, ...overrides }; + resolvedDeps = null; +} + +// ── polling: the store owns a single timer + the live connId ──────────────── + +let pollTimer: ReturnType | null = null; +let activeConnId: ConnId | null = null; +/** Previous terminal-id set for the selected project (newly-added detection). */ +let previousTerminalIds: Set | null = null; + +function stopPolling(): void { + if (pollTimer !== null) { + clearInterval(pollTimer); + pollTimer = null; + } + activeConnId = null; + previousTerminalIds = null; +} + +/** Shallow id+name+git+services equality for the project list (mirrors `_projectListEquals`). */ +function projectListEquals(a: ProjectInfo[], b: ProjectInfo[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + const pa = a[i]!; + const pb = b[i]!; + if (pa.id !== pb.id || pa.name !== pb.name) return false; + if (!arrayEquals(pa.terminalIds, pb.terminalIds)) return false; + if (pa.gitBranch !== pb.gitBranch) return false; + if (pa.gitLinesAdded !== pb.gitLinesAdded) return false; + if (pa.gitLinesRemoved !== pb.gitLinesRemoved) return false; + if (pa.services.length !== pb.services.length) return false; + for (let j = 0; j < pa.services.length; j++) { + if ( + pa.services[j]!.name !== pb.services[j]!.name || + pa.services[j]!.status !== pb.services[j]!.status + ) { + return false; + } + } + if (pa.folderColor !== pb.folderColor) return false; + } + return true; +} + +function arrayEquals(a: readonly T[], b: readonly T[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +/** Resolve the selected project from a project list + selected id (Dart `selectedProject`). */ +function resolveSelectedProject( + projects: ProjectInfo[], + selectedProjectId: ProjectId | null, +): ProjectInfo | null { + if (selectedProjectId === null) return projects[0] ?? null; + return projects.find((p) => p.id === selectedProjectId) ?? projects[0] ?? null; +} + +/** One poll tick — refresh remote state + run auto-selection. Mirrors `_pollState`. */ +function pollState( + get: StoreApi['getState'], + set: StoreApi['setState'], +): void { + const connId = activeConnId; + if (!connId) return; + const native = deps().native; + + const newProjects = native.getProjects(connId); + const focusedId = native.getFocusedProjectId(connId); + const newFolders = native.getFolders(connId); + const newProjectOrder = native.getProjectOrder(connId); + const newFullscreen = native.getFullscreenTerminal(connId) ?? null; + + const prev = get(); + const patch: Partial = {}; + let changed = false; + + if (!projectListEquals(newProjects, prev.projects)) { + patch.projects = newProjects; + changed = true; + } + if ( + !arrayEquals( + newFolders.map((f) => f.id), + prev.folders.map((f) => f.id), + ) + ) { + patch.folders = newFolders; + changed = true; + } + if (!arrayEquals(newProjectOrder, prev.projectOrder)) { + patch.projectOrder = newProjectOrder; + changed = true; + } + if (newFullscreen?.terminalId !== prev.fullscreenTerminal?.terminalId) { + patch.fullscreenTerminal = newFullscreen; + changed = true; + } + + // Work against the freshest values for the auto-select logic below. + const projects = patch.projects ?? prev.projects; + let selectedProjectId = prev.selectedProjectId; + let selectedTerminalId = prev.selectedTerminalId; + + // Auto-select the focused project if nothing is selected. + if (selectedProjectId === null && focusedId) { + selectedProjectId = focusedId; + patch.selectedProjectId = focusedId; + changed = true; + } + + // Auto-select a terminal: pick a newly-added one, or the first if the current + // selection is gone (matches the Dart logic + `_previousTerminalIds`). + const project = resolveSelectedProject(projects, selectedProjectId); + if (project && project.terminalIds.length > 0) { + if (selectedTerminalId === null || !project.terminalIds.includes(selectedTerminalId)) { + selectedTerminalId = project.terminalIds[0]!; + patch.selectedTerminalId = selectedTerminalId; + changed = true; + } else if (previousTerminalIds !== null) { + const newIds = project.terminalIds.filter((id) => !previousTerminalIds!.has(id)); + if (newIds.length > 0) { + selectedTerminalId = newIds[newIds.length - 1]!; + patch.selectedTerminalId = selectedTerminalId; + changed = true; + } + } + previousTerminalIds = new Set(project.terminalIds); + } else { + previousTerminalIds = null; + if (selectedTerminalId !== null) { + patch.selectedTerminalId = null; + changed = true; + } + } + + // Connection health (drives the staleness indicator's 3s / 10s thresholds). + const newActivity = native.secondsSinceActivity(connId); + const oldActivity = prev.secondsSinceActivity; + if ( + (oldActivity < 3) !== (newActivity < 3) || + (oldActivity < 10) !== (newActivity < 10) + ) { + changed = true; + } + patch.secondsSinceActivity = newActivity; + + if (changed) set(patch); + else if (patch.secondsSinceActivity !== undefined) { + // Always keep the raw activity number current, even when no UI-visible + // threshold crossed (cheap, avoids a stale value). + set({ secondsSinceActivity: newActivity }); + } +} + +/** + * The workspace store hook + bound store. Module-level (construction is + * side-effect-free w.r.t. the native module — see the connection store for the + * same reasoning). Use it like any zustand hook, plus `.getState()` for + * imperative access: + * + * ```ts + * const projects = useWorkspaceStore((s) => s.projects); + * const selectProject = useWorkspaceStore((s) => s.selectProject); + * ``` + */ +export const useWorkspaceStore: UseBoundStore> = + create((set, get) => ({ + projects: [], + folders: [], + projectOrder: [], + fullscreenTerminal: null, + selectedProjectId: null, + selectedTerminalId: null, + secondsSinceActivity: 0, + + start(connId) { + if (pollTimer !== null && activeConnId === connId) return; // already polling this conn + stopPolling(); + activeConnId = connId; + pollTimer = setInterval(() => pollState(get, set), WORKSPACE_POLL_MS); + pollState(get, set); // immediate first poll + }, + + stop() { + stopPolling(); + set({ + projects: [], + folders: [], + projectOrder: [], + fullscreenTerminal: null, + selectedProjectId: null, + selectedTerminalId: null, + }); + }, + + selectProject(projectId) { + previousTerminalIds = null; + set({ selectedProjectId: projectId, selectedTerminalId: null }); + }, + + selectTerminal(terminalId) { + set({ selectedTerminalId: terminalId }); + }, + + getSelectedProject() { + const { projects, selectedProjectId } = get(); + return resolveSelectedProject(projects, selectedProjectId); + }, + + getProjectLayoutJson() { + if (!activeConnId) return undefined; + const project = get().getSelectedProject(); + if (!project) return undefined; + return deps().native.getProjectLayoutJson(activeConnId, project.id); + }, + })); + +/** Imperative store handle for non-React consumers (e.g. App.tsx lifecycle wiring). */ +export function workspaceStore(): StoreApi { + return useWorkspaceStore; +} From b66a0a8df7355a3ef9ea9d9a733711217862f3b5 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 10 Jun 2026 13:21:20 +0200 Subject: [PATCH 31/37] refactor(mobile-ffi): make okena-mobile-ffi self-contained on uniffi 0.31 Move the plain-Rust engine (ConnectionManager, MobileConnectionHandler, TerminalHolder) and the api data structs out of the retired flutter_rust_bridge crate mobile/native into crates/okena-mobile-ffi, stripping the #[frb] attributes. Drop the okena_mobile_native path dependency (and the transitive flutter_rust_bridge build) so the crate depends only on okena-core + uniffi. Remove the now-dead execute_action/spawn helpers, and bump uniffi 0.29 -> 0.31 to match uniffi-bindgen-react-native 0.31 used by mobile/rn. Drop mobile/native from the workspace members. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 232 +- Cargo.toml | 2 +- crates/okena-core/src/client/connection.rs | 2 +- crates/okena-mobile-ffi/Cargo.toml | 28 +- crates/okena-mobile-ffi/src/api/connection.rs | 43 + .../okena-mobile-ffi}/src/api/mod.rs | 0 crates/okena-mobile-ffi/src/api/state.rs | 171 + crates/okena-mobile-ffi/src/api/terminal.rs | 52 + .../okena-mobile-ffi}/src/client/handler.rs | 0 .../okena-mobile-ffi}/src/client/manager.rs | 49 - .../okena-mobile-ffi}/src/client/mod.rs | 0 .../src/client/terminal_holder.rs | 5 + crates/okena-mobile-ffi/src/lib.rs | 35 +- crates/okena-mobile-ffi/src/types.rs | 13 +- mobile/native/Cargo.toml | 29 - mobile/native/src/api/connection.rs | 86 - mobile/native/src/api/state.rs | 733 ---- mobile/native/src/api/terminal.rs | 197 - mobile/native/src/frb_generated.rs | 3587 ----------------- mobile/native/src/lib.rs | 6 - 20 files changed, 353 insertions(+), 4917 deletions(-) create mode 100644 crates/okena-mobile-ffi/src/api/connection.rs rename {mobile/native => crates/okena-mobile-ffi}/src/api/mod.rs (100%) create mode 100644 crates/okena-mobile-ffi/src/api/state.rs create mode 100644 crates/okena-mobile-ffi/src/api/terminal.rs rename {mobile/native => crates/okena-mobile-ffi}/src/client/handler.rs (100%) rename {mobile/native => crates/okena-mobile-ffi}/src/client/manager.rs (89%) rename {mobile/native => crates/okena-mobile-ffi}/src/client/mod.rs (100%) rename {mobile/native => crates/okena-mobile-ffi}/src/client/terminal_holder.rs (97%) delete mode 100644 mobile/native/Cargo.toml delete mode 100644 mobile/native/src/api/connection.rs delete mode 100644 mobile/native/src/api/state.rs delete mode 100644 mobile/native/src/api/terminal.rs delete mode 100644 mobile/native/src/frb_generated.rs delete mode 100644 mobile/native/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index cac4ef51..5c756c74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,40 +94,12 @@ dependencies = [ "equator", ] -[[package]] -name = "allo-isolate" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" -dependencies = [ - "anyhow", - "atomic", - "backtrace", -] - [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" -dependencies = [ - "android_log-sys", - "env_filter 0.1.4", - "log", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -284,9 +256,9 @@ dependencies = [ [[package]] name = "askama" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" dependencies = [ "askama_derive", "itoa", @@ -297,9 +269,9 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" dependencies = [ "askama_parser", "basic-toml", @@ -314,9 +286,9 @@ dependencies = [ [[package]] name = "askama_parser" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" dependencies = [ "memchr", "serde", @@ -902,12 +874,6 @@ dependencies = [ "serde", ] -[[package]] -name = "build-target" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" - [[package]] name = "built" version = "0.8.0" @@ -1594,28 +1560,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "dart-sys" -version = "4.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" -dependencies = [ - "cc", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -1648,17 +1592,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" -[[package]] -name = "delegate-attr" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "deranged" version = "0.5.8" @@ -2218,48 +2151,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "flutter_rust_bridge" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde126295b2acc5f0a712e265e91b6fdc0ed38767496483e592ae7134db83725" -dependencies = [ - "allo-isolate", - "android_logger", - "anyhow", - "build-target", - "bytemuck", - "byteorder", - "console_error_panic_hook", - "dart-sys", - "delegate-attr", - "flutter_rust_bridge_macros", - "futures", - "js-sys", - "lazy_static", - "log", - "oslog", - "portable-atomic", - "threadpool", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "flutter_rust_bridge_macros" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f0420326b13675321b194928bb7830043b68cf8b810e1c651285c747abb080" -dependencies = [ - "hex", - "md-5", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "fnv" version = "1.0.7" @@ -3351,7 +3242,7 @@ version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "691ea1e31435c7e7d4d04705ec9d1c0d9482c46b2acf512bc723939d8f0af7fb" dependencies = [ - "dashmap 6.1.0", + "dashmap", "gix-fs", "libc", "parking_lot", @@ -5923,11 +5814,17 @@ dependencies = [ name = "okena-mobile-ffi" version = "0.1.0" dependencies = [ + "alacritty_terminal", "anyhow", + "async-channel 2.5.0", + "log", "okena-core", - "okena_mobile_native", + "parking_lot", + "reqwest", "serde_json", + "tokio", "uniffi", + "uuid", ] [[package]] @@ -6134,26 +6031,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "okena_mobile_native" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "async-channel 2.5.0", - "flutter_rust_bridge", - "futures", - "log", - "okena-core", - "parking_lot", - "reqwest", - "serde", - "serde_json", - "tokio", - "tokio-tungstenite 0.24.0", - "uuid", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -6265,17 +6142,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "oslog" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" -dependencies = [ - "cc", - "dashmap 5.5.3", - "log", -] - [[package]] name = "parking" version = "2.2.1" @@ -6413,7 +6279,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -7386,7 +7252,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "siphasher 1.0.2", + "siphasher", "toml 0.8.23", "triomphe", ] @@ -7982,12 +7848,6 @@ dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.2" @@ -8323,7 +8183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -8560,15 +8420,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "tiff" version = "0.11.3" @@ -8753,15 +8604,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.23" @@ -9219,9 +9061,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uniffi" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +checksum = "dc5f2297ee5b893405bed1a6929faec4713a061df158ecf5198089f23910d470" dependencies = [ "anyhow", "cargo_metadata", @@ -9233,9 +9075,9 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +checksum = "8bc0c60a9607e7ab77a2ad47ec5530178015014839db25af7512447d2238016c" dependencies = [ "anyhow", "askama", @@ -9250,7 +9092,7 @@ dependencies = [ "serde", "tempfile", "textwrap", - "toml 0.5.11", + "toml 0.9.12+spec-1.1.0", "uniffi_internal_macros", "uniffi_meta", "uniffi_pipeline", @@ -9259,9 +9101,9 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +checksum = "77baf5d539fe2e1ad6805e942dbc5dbdeb2b83eb5f2b3a6535d422ca4b02a12f" dependencies = [ "anyhow", "async-compat", @@ -9272,9 +9114,9 @@ dependencies = [ [[package]] name = "uniffi_internal_macros" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5" dependencies = [ "anyhow", "indexmap", @@ -9285,9 +9127,9 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +checksum = "d9273ec45330d8fe9a3701b7b983cea7a4e218503359831967cb95d26b873561" dependencies = [ "camino", "fs-err", @@ -9296,27 +9138,27 @@ dependencies = [ "quote", "serde", "syn", - "toml 0.5.11", + "toml 0.9.12+spec-1.1.0", "uniffi_meta", ] [[package]] name = "uniffi_meta" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +checksum = "431d2f443e7828a6c29d188de98b6771a6491ee98bba2d4372643bf93f988a18" dependencies = [ "anyhow", - "siphasher 0.3.11", + "siphasher", "uniffi_internal_macros", "uniffi_pipeline", ] [[package]] name = "uniffi_pipeline" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689" dependencies = [ "anyhow", "heck 0.5.0", @@ -9327,9 +9169,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +checksum = "68773ec0e1c067b6505a73bbf6a5782f31a7f9209333a0df97b87565c46bf370" dependencies = [ "anyhow", "textwrap", @@ -9378,7 +9220,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.2", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", diff --git a/Cargo.toml b/Cargo.toml index 32c84c57..9d37baf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "mobile/native", "crates/okena-mobile-ffi", "crates/okena-core", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-files", "crates/okena-markdown", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-views-remote", "crates/okena-theme"] +members = [".", "crates/okena-mobile-ffi", "crates/okena-core", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-files", "crates/okena-markdown", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-views-remote", "crates/okena-theme"] resolver = "2" [package] diff --git a/crates/okena-core/src/client/connection.rs b/crates/okena-core/src/client/connection.rs index aa0b318f..a449ad2b 100644 --- a/crates/okena-core/src/client/connection.rs +++ b/crates/okena-core/src/client/connection.rs @@ -13,7 +13,7 @@ use tokio_tungstenite::tungstenite; /// Platform-specific operations that the generic client delegates to. /// /// Desktop creates `Terminal` objects and inserts into `TerminalsRegistry`. -/// Mobile may create Flutter-side terminal state via FFI callbacks. +/// Mobile creates `TerminalHolder` state via the FFI binding crate. pub trait ConnectionHandler: Send + Sync + 'static { /// Terminal discovered — create platform terminal object. /// `ws_sender` is for constructing a transport that sends WS commands. diff --git a/crates/okena-mobile-ffi/Cargo.toml b/crates/okena-mobile-ffi/Cargo.toml index 59f63859..7f2e5d2a 100644 --- a/crates/okena-mobile-ffi/Cargo.toml +++ b/crates/okena-mobile-ffi/Cargo.toml @@ -10,20 +10,26 @@ edition = "2024" crate-type = ["cdylib", "staticlib", "lib"] [dependencies] -# Reuse the existing mobile binding logic verbatim — ConnectionManager, -# MobileConnectionHandler, TerminalHolder and the api data structs all live -# here. We do NOT duplicate any of it; this crate only re-expresses the FFI -# surface via uniffi instead of flutter_rust_bridge. -okena_mobile_native = { path = "../../mobile/native" } - -# Shared protocol / TLS / WS / terminal-emulation core. Needed for the request -# enums (ActionRequest, WsClientMessage, SpecialKey, SplitDirection, DiffMode, -# FolderColor) and the DARK_THEME used when extracting cells. +# Shared protocol / TLS / WS / terminal-emulation core. The `client` feature +# pulls reqwest + tokio-tungstenite with the rustls backends (NDK-friendly — no +# OpenSSL cross-compile), so we inherit that backend selection via unification. okena-core = { path = "../okena-core", features = ["client"] } # uniffi in proc-macro mode (no UDL). `tokio` enables async export via -# `#[uniffi::export(async_runtime = "tokio")]`. -uniffi = { version = "0.29", features = ["tokio"] } +# `#[uniffi::export(async_runtime = "tokio")]`. Pinned to 0.31 to match the +# `uniffi-bindgen-react-native` (ubrn) version in mobile/rn (ubrn 0.31.0-3 pins +# `uniffi =0.31.0`, and the metadata contract version must match the bindgen). +uniffi = { version = "0.31", features = ["tokio"] } +# The connection/terminal engine carried over from the retired `mobile/native` +# crate (ConnectionManager, MobileConnectionHandler, TerminalHolder). These are +# its direct dependencies — no flutter_rust_bridge. +alacritty_terminal = "0.25" +tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "time"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +async-channel = "2.3" +parking_lot = "0.12" +uuid = { version = "1.10", features = ["v4"] } +log = "0.4" serde_json = "1.0" anyhow = "1.0" diff --git a/crates/okena-mobile-ffi/src/api/connection.rs b/crates/okena-mobile-ffi/src/api/connection.rs new file mode 100644 index 00000000..b414457f --- /dev/null +++ b/crates/okena-mobile-ffi/src/api/connection.rs @@ -0,0 +1,43 @@ +//! Connection status extraction. +//! +//! The lifecycle entry points (`init_app`, `connect`, `pair`, …) are exported +//! directly from `crate::lib` via uniffi; this module only holds the plain +//! `ConnectionStatus` enum (mirrored as a uniffi enum in `crate::types`) and the +//! one accessor `lib.rs` delegates to. + +use crate::client::manager::ConnectionManager; + +/// Connection status returned via FFI. +/// +/// Simplified version of okena_core's ConnectionStatus — collapses `Reconnecting { attempt }` +/// into `Connecting` since mobile UI doesn't need the attempt count. +#[derive(Debug, Clone)] +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Pairing, + Error { message: String }, +} + +impl From for ConnectionStatus { + fn from(status: okena_core::client::ConnectionStatus) -> Self { + match status { + okena_core::client::ConnectionStatus::Disconnected => ConnectionStatus::Disconnected, + okena_core::client::ConnectionStatus::Connecting => ConnectionStatus::Connecting, + okena_core::client::ConnectionStatus::Connected => ConnectionStatus::Connected, + okena_core::client::ConnectionStatus::Pairing => ConnectionStatus::Pairing, + okena_core::client::ConnectionStatus::Reconnecting { .. } => { + ConnectionStatus::Connecting + } + okena_core::client::ConnectionStatus::Error(msg) => { + ConnectionStatus::Error { message: msg } + } + } + } +} + +/// Get current connection status. +pub fn connection_status(conn_id: String) -> ConnectionStatus { + ConnectionManager::get().get_status(&conn_id).into() +} diff --git a/mobile/native/src/api/mod.rs b/crates/okena-mobile-ffi/src/api/mod.rs similarity index 100% rename from mobile/native/src/api/mod.rs rename to crates/okena-mobile-ffi/src/api/mod.rs diff --git a/crates/okena-mobile-ffi/src/api/state.rs b/crates/okena-mobile-ffi/src/api/state.rs new file mode 100644 index 00000000..699593a1 --- /dev/null +++ b/crates/okena-mobile-ffi/src/api/state.rs @@ -0,0 +1,171 @@ +//! Cached-state extraction into plain, FFI-friendly structs. +//! +//! Only the read-side accessors that `lib.rs` delegates to live here. Every +//! mutating action (terminal / git / service / project / layout) is exported +//! directly from `lib.rs` via uniffi against `ConnectionManager`, so it is not +//! duplicated here. The uniffi mirrors of these structs (with +//! `#[derive(uniffi::Record)]`) live in `crate::types`. + +use std::collections::HashMap; + +use crate::client::manager::ConnectionManager; +use okena_core::api::ApiLayoutNode; + +/// Flat FFI-friendly project info. +#[derive(Debug, Clone)] +pub struct ProjectInfo { + pub id: String, + pub name: String, + pub path: String, + pub show_in_overview: bool, + pub terminal_ids: Vec, + pub terminal_names: HashMap, + pub git_branch: Option, + pub git_lines_added: u32, + pub git_lines_removed: u32, + pub services: Vec, + pub folder_color: String, +} + +/// FFI-friendly service info. +#[derive(Debug, Clone)] +pub struct ServiceInfo { + pub name: String, + pub status: String, + pub terminal_id: Option, + pub ports: Vec, + pub exit_code: Option, + pub kind: String, + pub is_extra: bool, +} + +/// FFI-friendly folder info. +#[derive(Debug, Clone)] +pub struct FolderInfo { + pub id: String, + pub name: String, + pub project_ids: Vec, + pub folder_color: String, +} + +/// FFI-friendly fullscreen info. +#[derive(Debug, Clone)] +pub struct FullscreenInfo { + pub project_id: String, + pub terminal_id: String, +} + +/// Get all projects from the cached remote state. +pub fn get_projects(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + let state = match mgr.get_state(&conn_id) { + Some(s) => s, + None => return Vec::new(), + }; + + state + .projects + .iter() + .map(|p| { + let terminal_ids = if let Some(ref layout) = p.layout { + let mut ids = Vec::new(); + collect_layout_ids_vec(layout, &mut ids); + ids + } else { + Vec::new() + }; + let (git_branch, git_lines_added, git_lines_removed) = + if let Some(ref gs) = p.git_status { + (gs.branch.clone(), gs.lines_added as u32, gs.lines_removed as u32) + } else { + (None, 0, 0) + }; + let services = p + .services + .iter() + .map(|s| ServiceInfo { + name: s.name.clone(), + status: s.status.clone(), + terminal_id: s.terminal_id.clone(), + ports: s.ports.clone(), + exit_code: s.exit_code, + kind: s.kind.clone(), + is_extra: s.is_extra, + }) + .collect(); + ProjectInfo { + id: p.id.clone(), + name: p.name.clone(), + path: p.path.clone(), + show_in_overview: p.show_in_overview, + terminal_ids, + terminal_names: p.terminal_names.clone(), + git_branch, + git_lines_added, + git_lines_removed, + services, + folder_color: format!("{:?}", p.folder_color).to_lowercase(), + } + }) + .collect() +} + +/// Get the focused project ID from the cached remote state. +pub fn get_focused_project_id(conn_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id) + .and_then(|s| s.focused_project_id.clone()) +} + +/// Get folders from the cached remote state. +pub fn get_folders(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + let state = match mgr.get_state(&conn_id) { + Some(s) => s, + None => return Vec::new(), + }; + state + .folders + .iter() + .map(|f| FolderInfo { + id: f.id.clone(), + name: f.name.clone(), + project_ids: f.project_ids.clone(), + folder_color: format!("{:?}", f.folder_color).to_lowercase(), + }) + .collect() +} + +/// Get the project order from the cached remote state. +pub fn get_project_order(conn_id: String) -> Vec { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id) + .map(|s| s.project_order.clone()) + .unwrap_or_default() +} + +/// Get fullscreen terminal info. +pub fn get_fullscreen_terminal(conn_id: String) -> Option { + let mgr = ConnectionManager::get(); + mgr.get_state(&conn_id).and_then(|s| { + s.fullscreen_terminal.as_ref().map(|f| FullscreenInfo { + project_id: f.project_id.clone(), + terminal_id: f.terminal_id.clone(), + }) + }) +} + +fn collect_layout_ids_vec(node: &ApiLayoutNode, ids: &mut Vec) { + match node { + ApiLayoutNode::Terminal { terminal_id, .. } => { + if let Some(id) = terminal_id { + ids.push(id.clone()); + } + } + ApiLayoutNode::Split { children, .. } | ApiLayoutNode::Tabs { children, .. } => { + for child in children { + collect_layout_ids_vec(child, ids); + } + } + } +} diff --git a/crates/okena-mobile-ffi/src/api/terminal.rs b/crates/okena-mobile-ffi/src/api/terminal.rs new file mode 100644 index 00000000..c4dd160a --- /dev/null +++ b/crates/okena-mobile-ffi/src/api/terminal.rs @@ -0,0 +1,52 @@ +//! Plain data structs extracted from the terminal grid. +//! +//! `TerminalHolder` (`crate::client::terminal_holder`) produces these; the +//! uniffi-facing equivalents (`#[derive(uniffi::Record)]`) live in +//! `crate::types` and convert from these via `From`. + +/// Cell data for FFI transfer (flat, no pointers). +#[derive(Debug, Clone)] +pub struct CellData { + /// The character in this cell. + pub character: String, + /// Foreground color as ARGB packed u32. + pub fg: u32, + /// Background color as ARGB packed u32. + pub bg: u32, + /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). + pub flags: u8, +} + +/// Cursor shape variants. +#[derive(Debug, Clone)] +pub enum CursorShape { + Block, + Underline, + Beam, +} + +/// Cursor state for FFI transfer. +#[derive(Debug, Clone)] +pub struct CursorState { + pub col: u16, + pub row: u16, + pub shape: CursorShape, + pub visible: bool, +} + +/// Scroll info for FFI transfer. +#[derive(Debug, Clone)] +pub struct ScrollInfo { + pub total_lines: u32, + pub visible_lines: u32, + pub display_offset: u32, +} + +/// Selection bounds for FFI transfer. +#[derive(Debug, Clone)] +pub struct SelectionBounds { + pub start_col: u16, + pub start_row: i32, + pub end_col: u16, + pub end_row: i32, +} diff --git a/mobile/native/src/client/handler.rs b/crates/okena-mobile-ffi/src/client/handler.rs similarity index 100% rename from mobile/native/src/client/handler.rs rename to crates/okena-mobile-ffi/src/client/handler.rs diff --git a/mobile/native/src/client/manager.rs b/crates/okena-mobile-ffi/src/client/manager.rs similarity index 89% rename from mobile/native/src/client/manager.rs rename to crates/okena-mobile-ffi/src/client/manager.rs index 23484e18..9544eff8 100644 --- a/mobile/native/src/client/manager.rs +++ b/crates/okena-mobile-ffi/src/client/manager.rs @@ -261,55 +261,6 @@ impl ConnectionManager { } } - /// Execute an action on the remote server via HTTP POST /v1/actions. - /// Returns the response body as a string. - pub async fn execute_action( - &self, - conn_id: &str, - action: okena_core::api::ActionRequest, - ) -> anyhow::Result { - let (host, port, token) = { - let connections = self.connections.read(); - let conn = connections - .get(conn_id) - .ok_or_else(|| anyhow::anyhow!("Connection not found: {}", conn_id))?; - let client = conn.client.read(); - let config = client.config(); - let token = config - .saved_token - .clone() - .ok_or_else(|| anyhow::anyhow!("No auth token for connection {}", conn_id))?; - (config.host.clone(), config.port, token) - }; - - let url = format!("http://{}:{}/v1/actions", host, port); - let client = reqwest::Client::new(); - let resp = client - .post(&url) - .header("Authorization", format!("Bearer {}", token)) - .json(&action) - .timeout(std::time::Duration::from_secs(10)) - .send() - .await?; - - if !resp.status().is_success() { - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - anyhow::bail!("Action failed: HTTP {} - {}", status, body); - } - - Ok(resp.text().await.unwrap_or_default()) - } - - /// Spawn an async task on the connection manager's runtime. - pub fn spawn(&self, future: F) -> tokio::task::JoinHandle - where - F: std::future::Future + Send + 'static, - F::Output: Send + 'static, - { - self.runtime.spawn(future) - } - /// Resize a terminal holder and send the resize message to the server. pub fn resize_terminal(&self, conn_id: &str, terminal_id: &str, cols: u16, rows: u16) { let connections = self.connections.read(); diff --git a/mobile/native/src/client/mod.rs b/crates/okena-mobile-ffi/src/client/mod.rs similarity index 100% rename from mobile/native/src/client/mod.rs rename to crates/okena-mobile-ffi/src/client/mod.rs diff --git a/mobile/native/src/client/terminal_holder.rs b/crates/okena-mobile-ffi/src/client/terminal_holder.rs similarity index 97% rename from mobile/native/src/client/terminal_holder.rs rename to crates/okena-mobile-ffi/src/client/terminal_holder.rs index 5eca7281..03bd5497 100644 --- a/mobile/native/src/client/terminal_holder.rs +++ b/crates/okena-mobile-ffi/src/client/terminal_holder.rs @@ -247,6 +247,11 @@ impl TerminalHolder { } /// Take the dirty flag (returns true if it was dirty, resets to false). + /// + /// Reserved for the RN render loop's `is_dirty()`-gated repaint (migration + /// plan Decision C): the canvas will call this once per frame to consume the + /// flag. Not yet wired into the uniffi surface, hence `allow(dead_code)`. + #[allow(dead_code)] pub fn take_dirty(&self) -> bool { self.dirty.swap(false, Ordering::Relaxed) } diff --git a/crates/okena-mobile-ffi/src/lib.rs b/crates/okena-mobile-ffi/src/lib.rs index af6f518c..aa3922a5 100644 --- a/crates/okena-mobile-ffi/src/lib.rs +++ b/crates/okena-mobile-ffi/src/lib.rs @@ -1,15 +1,16 @@ //! uniffi FFI surface for the React Native mobile app. //! -//! This crate is the RN-facing equivalent of `mobile/native`'s -//! `flutter_rust_bridge` `api/` layer. It re-expresses the same ~60 functions -//! via uniffi proc-macros (`#[uniffi::export]`, `#[derive(uniffi::Record)]`, -//! `#[derive(uniffi::Enum)]`) so `uniffi-bindgen-react-native` (ubrn) can emit -//! a JSI TurboModule. +//! Exposes ~60 functions via uniffi proc-macros (`#[uniffi::export]`, +//! `#[derive(uniffi::Record)]`, `#[derive(uniffi::Enum)]`) so +//! `uniffi-bindgen-react-native` (ubrn) can emit a JSI TurboModule. This +//! replaces the `flutter_rust_bridge` `api/` layer of the retired `mobile/native` +//! crate, whose plain-Rust engine now lives here directly (see below). //! -//! It does NOT reimplement any logic: every function delegates to -//! `okena_mobile_native::client::manager::ConnectionManager`, exactly like the -//! Flutter `api/` functions do. The `ConnectionManager`, `MobileConnectionHandler` -//! and `TerminalHolder` are reused verbatim from `mobile/native`. +//! The networking/emulation engine lives in `crate::client` +//! (`ConnectionManager`, `MobileConnectionHandler`, `TerminalHolder`) and the +//! plain state-extraction structs in `crate::api` — both carried over from the +//! retired `mobile/native` Flutter crate (frb attributes stripped). This crate +//! is self-contained: it does not depend on any Flutter tooling. //! //! ## Async strategy //! The frb api split sync vs. async based on whether the body actually awaits: @@ -24,6 +25,8 @@ #![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))] +mod api; +mod client; mod types; use okena_core::api::ActionRequest; @@ -32,7 +35,7 @@ use okena_core::keys::SpecialKey; use okena_core::theme::DARK_THEME; use okena_core::types::{DiffMode, SplitDirection}; -use okena_mobile_native::client::manager::ConnectionManager; +use crate::client::manager::ConnectionManager; pub use types::{ CellData, ConnectionStatus, CursorShape, CursorState, FolderInfo, FullscreenInfo, ProjectInfo, @@ -103,7 +106,7 @@ pub fn connection_status(conn_id: String) -> ConnectionStatus { // Delegate to the native api fn, which already maps okena-core's status // (collapsing `Reconnecting` into `Connecting`) into its own enum; we then // convert that into our uniffi enum. - okena_mobile_native::api::connection::connection_status(conn_id).into() + crate::api::connection::connection_status(conn_id).into() } /// Seconds since last WS activity (terminal output). Large value if missing. @@ -328,7 +331,7 @@ pub fn get_selection_bounds(conn_id: String, terminal_id: String) -> Option Vec { - okena_mobile_native::api::state::get_projects(conn_id) + crate::api::state::get_projects(conn_id) .into_iter() .map(Into::into) .collect() @@ -337,13 +340,13 @@ pub fn get_projects(conn_id: String) -> Vec { /// Get the focused project ID from the cached remote state. #[uniffi::export] pub fn get_focused_project_id(conn_id: String) -> Option { - okena_mobile_native::api::state::get_focused_project_id(conn_id) + crate::api::state::get_focused_project_id(conn_id) } /// Get folders from the cached remote state. #[uniffi::export] pub fn get_folders(conn_id: String) -> Vec { - okena_mobile_native::api::state::get_folders(conn_id) + crate::api::state::get_folders(conn_id) .into_iter() .map(Into::into) .collect() @@ -352,13 +355,13 @@ pub fn get_folders(conn_id: String) -> Vec { /// Get the project order from the cached remote state. #[uniffi::export] pub fn get_project_order(conn_id: String) -> Vec { - okena_mobile_native::api::state::get_project_order(conn_id) + crate::api::state::get_project_order(conn_id) } /// Get fullscreen terminal info. #[uniffi::export] pub fn get_fullscreen_terminal(conn_id: String) -> Option { - okena_mobile_native::api::state::get_fullscreen_terminal(conn_id).map(Into::into) + crate::api::state::get_fullscreen_terminal(conn_id).map(Into::into) } /// Get layout JSON for a project. diff --git a/crates/okena-mobile-ffi/src/types.rs b/crates/okena-mobile-ffi/src/types.rs index 848d6e97..89cb9e78 100644 --- a/crates/okena-mobile-ffi/src/types.rs +++ b/crates/okena-mobile-ffi/src/types.rs @@ -1,12 +1,13 @@ //! uniffi `Record` / `Enum` types mirroring the data the FFI returns. //! -//! uniffi derive macros cannot be placed on types from another crate, so we -//! define our own here and convert from `okena_mobile_native`'s api/client -//! types via `From`. The shapes match 1:1 so the conversions are mechanical. +//! uniffi derive macros are kept off the internal `crate::api` data structs +//! (which `TerminalHolder` and the state accessors produce), so we define the +//! uniffi-facing mirrors here and convert from the internal ones via `From`. +//! The shapes match 1:1 so the conversions are mechanical. use std::collections::HashMap; -use okena_mobile_native::api::{ +use crate::api::{ connection::ConnectionStatus as NativeConnectionStatus, state::{ FolderInfo as NativeFolderInfo, FullscreenInfo as NativeFullscreenInfo, @@ -21,8 +22,8 @@ use okena_mobile_native::api::{ /// Connection status surfaced to the RN layer. /// -/// Mirrors `okena_mobile_native::api::connection::ConnectionStatus` (which -/// itself collapses core's `Reconnecting { attempt }` into `Connecting`). +/// Mirrors `crate::api::connection::ConnectionStatus` (which itself collapses +/// core's `Reconnecting { attempt }` into `Connecting`). #[derive(Debug, Clone, uniffi::Enum)] pub enum ConnectionStatus { Disconnected, diff --git a/mobile/native/Cargo.toml b/mobile/native/Cargo.toml deleted file mode 100644 index 71d73f0d..00000000 --- a/mobile/native/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "okena_mobile_native" -version = "0.1.0" -edition = "2024" - -[lib] -# `lib` is additive: the cdylib/staticlib still build for Flutter, while `lib` -# lets the new `okena-mobile-ffi` crate depend on this crate as a Rust library -# and reuse its ConnectionManager / TerminalHolder logic directly. -crate-type = ["cdylib", "staticlib", "lib"] - -[dependencies] -okena-core = { path = "../../crates/okena-core", features = ["client"] } -flutter_rust_bridge = "=2.11.1" -alacritty_terminal = "0.25" -tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "time"] } -tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } -log = "0.4" -anyhow = "1.0" -async-channel = "2.3" -futures = "0.3" -parking_lot = "0.12" -uuid = { version = "1.10", features = ["v4"] } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/mobile/native/src/api/connection.rs b/mobile/native/src/api/connection.rs deleted file mode 100644 index 76a62193..00000000 --- a/mobile/native/src/api/connection.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::client::manager::ConnectionManager; - -/// Connection status returned via FFI. -/// -/// Simplified version of okena_core's ConnectionStatus — collapses `Reconnecting { attempt }` -/// into `Connecting` since mobile UI doesn't need the attempt count. -#[derive(Debug, Clone)] -pub enum ConnectionStatus { - Disconnected, - Connecting, - Connected, - Pairing, - Error { message: String }, -} - -impl From for ConnectionStatus { - fn from(status: okena_core::client::ConnectionStatus) -> Self { - match status { - okena_core::client::ConnectionStatus::Disconnected => ConnectionStatus::Disconnected, - okena_core::client::ConnectionStatus::Connecting => ConnectionStatus::Connecting, - okena_core::client::ConnectionStatus::Connected => ConnectionStatus::Connected, - okena_core::client::ConnectionStatus::Pairing => ConnectionStatus::Pairing, - okena_core::client::ConnectionStatus::Reconnecting { .. } => { - ConnectionStatus::Connecting - } - okena_core::client::ConnectionStatus::Error(msg) => { - ConnectionStatus::Error { message: msg } - } - } - } -} - -/// Initialize the app (called once at startup). -#[flutter_rust_bridge::frb(init)] -pub fn init_app() { - flutter_rust_bridge::setup_default_user_utils(); - ConnectionManager::init(); -} - -/// Connect to an Okena remote server. Returns a connection ID. -/// If a saved token is provided, it will be used to skip pairing. -/// -/// TODO(mobile-tls): expose `tls: bool` here (and a Dart toggle + -/// fingerprint-verification UI) to let the app opt into TLS, then run -/// `flutter_rust_bridge_codegen generate`. The whole Rust stack below already -/// supports it — `add_connection` takes `tls` and the client pins the cert on -/// first connect — this FFI just hardcodes `false` until the codegen pass lands. -#[flutter_rust_bridge::frb(sync)] -pub fn connect(host: String, port: u16, saved_token: Option) -> String { - let mgr = ConnectionManager::get(); - let conn_id = mgr.add_connection(&host, port, saved_token, false); - mgr.connect(&conn_id); - conn_id -} - -/// Get the current auth token for a connection (if paired). -#[flutter_rust_bridge::frb(sync)] -pub fn get_token(conn_id: String) -> Option { - ConnectionManager::get().get_token(&conn_id) -} - -/// Pair with the server using a pairing code. -pub async fn pair(conn_id: String, code: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.pair(&conn_id, &code); - Ok(()) -} - -/// Disconnect from a server. -#[flutter_rust_bridge::frb(sync)] -pub fn disconnect(conn_id: String) { - ConnectionManager::get().disconnect(&conn_id); -} - -/// Get current connection status. -#[flutter_rust_bridge::frb(sync)] -pub fn connection_status(conn_id: String) -> ConnectionStatus { - ConnectionManager::get().get_status(&conn_id).into() -} - -/// Get seconds since last WS activity (terminal output). -/// Returns a large value if the connection doesn't exist. -#[flutter_rust_bridge::frb(sync)] -pub fn seconds_since_activity(conn_id: String) -> f64 { - ConnectionManager::get().seconds_since_activity(&conn_id) -} diff --git a/mobile/native/src/api/state.rs b/mobile/native/src/api/state.rs deleted file mode 100644 index 6bfe6e8c..00000000 --- a/mobile/native/src/api/state.rs +++ /dev/null @@ -1,733 +0,0 @@ -use std::collections::HashMap; - -use crate::client::manager::ConnectionManager; -use okena_core::api::{ActionRequest, ApiLayoutNode}; -use okena_core::client::{collect_state_terminal_ids, WsClientMessage}; -use okena_core::keys::SpecialKey; -use okena_core::types::SplitDirection; - -/// Flat FFI-friendly project info. -#[derive(Debug, Clone)] -pub struct ProjectInfo { - pub id: String, - pub name: String, - pub path: String, - pub show_in_overview: bool, - pub terminal_ids: Vec, - pub terminal_names: HashMap, - pub git_branch: Option, - pub git_lines_added: u32, - pub git_lines_removed: u32, - pub services: Vec, - pub folder_color: String, -} - -/// FFI-friendly service info. -#[derive(Debug, Clone)] -pub struct ServiceInfo { - pub name: String, - pub status: String, - pub terminal_id: Option, - pub ports: Vec, - pub exit_code: Option, - pub kind: String, - pub is_extra: bool, -} - -/// FFI-friendly folder info. -#[derive(Debug, Clone)] -pub struct FolderInfo { - pub id: String, - pub name: String, - pub project_ids: Vec, - pub folder_color: String, -} - -/// FFI-friendly fullscreen info. -#[derive(Debug, Clone)] -pub struct FullscreenInfo { - pub project_id: String, - pub terminal_id: String, -} - -/// Get all projects from the cached remote state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_projects(conn_id: String) -> Vec { - let mgr = ConnectionManager::get(); - let state = match mgr.get_state(&conn_id) { - Some(s) => s, - None => return Vec::new(), - }; - - state - .projects - .iter() - .map(|p| { - let terminal_ids = if let Some(ref layout) = p.layout { - let mut ids = Vec::new(); - collect_layout_ids_vec(layout, &mut ids); - ids - } else { - Vec::new() - }; - let (git_branch, git_lines_added, git_lines_removed) = - if let Some(ref gs) = p.git_status { - (gs.branch.clone(), gs.lines_added as u32, gs.lines_removed as u32) - } else { - (None, 0, 0) - }; - let services = p - .services - .iter() - .map(|s| ServiceInfo { - name: s.name.clone(), - status: s.status.clone(), - terminal_id: s.terminal_id.clone(), - ports: s.ports.clone(), - exit_code: s.exit_code, - kind: s.kind.clone(), - is_extra: s.is_extra, - }) - .collect(); - ProjectInfo { - id: p.id.clone(), - name: p.name.clone(), - path: p.path.clone(), - show_in_overview: p.show_in_overview, - terminal_ids, - terminal_names: p.terminal_names.clone(), - git_branch, - git_lines_added, - git_lines_removed, - services, - folder_color: format!("{:?}", p.folder_color).to_lowercase(), - } - }) - .collect() -} - -/// Get the focused project ID from the cached remote state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_focused_project_id(conn_id: String) -> Option { - let mgr = ConnectionManager::get(); - mgr.get_state(&conn_id) - .and_then(|s| s.focused_project_id.clone()) -} - -/// Get folders from the cached remote state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_folders(conn_id: String) -> Vec { - let mgr = ConnectionManager::get(); - let state = match mgr.get_state(&conn_id) { - Some(s) => s, - None => return Vec::new(), - }; - state - .folders - .iter() - .map(|f| FolderInfo { - id: f.id.clone(), - name: f.name.clone(), - project_ids: f.project_ids.clone(), - folder_color: format!("{:?}", f.folder_color).to_lowercase(), - }) - .collect() -} - -/// Get the project order from the cached remote state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_project_order(conn_id: String) -> Vec { - let mgr = ConnectionManager::get(); - mgr.get_state(&conn_id) - .map(|s| s.project_order.clone()) - .unwrap_or_default() -} - -/// Get fullscreen terminal info. -#[flutter_rust_bridge::frb(sync)] -pub fn get_fullscreen_terminal(conn_id: String) -> Option { - let mgr = ConnectionManager::get(); - mgr.get_state(&conn_id) - .and_then(|s| { - s.fullscreen_terminal.as_ref().map(|f| FullscreenInfo { - project_id: f.project_id.clone(), - terminal_id: f.terminal_id.clone(), - }) - }) -} - -/// Get layout JSON for a project. -#[flutter_rust_bridge::frb(sync)] -pub fn get_project_layout_json(conn_id: String, project_id: String) -> Option { - let mgr = ConnectionManager::get(); - let state = mgr.get_state(&conn_id)?; - let project = state.projects.iter().find(|p| p.id == project_id)?; - let layout = project.layout.as_ref()?; - serde_json::to_string(layout).ok() -} - -/// Check if a terminal has unprocessed output (dirty flag). -#[flutter_rust_bridge::frb(sync)] -pub fn is_dirty(conn_id: String, terminal_id: String) -> bool { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.is_dirty()) - .unwrap_or(false) -} - -/// Send a special key (e.g. "Enter", "Tab", "Escape") to a terminal. -/// -/// The key name is deserialized from JSON (e.g. `"Enter"`, `"CtrlC"`, `"ArrowUp"`). -pub async fn send_special_key( - conn_id: String, - terminal_id: String, - key: String, -) -> anyhow::Result<()> { - let special_key: SpecialKey = serde_json::from_value(serde_json::Value::String(key.clone())) - .map_err(|_| anyhow::anyhow!("Unknown special key: {}", key))?; - let text = String::from_utf8_lossy(special_key.to_bytes()).to_string(); - let mgr = ConnectionManager::get(); - mgr.send_ws_message( - &conn_id, - WsClientMessage::SendText { - terminal_id, - text, - }, - ); - Ok(()) -} - -fn collect_layout_ids_vec(node: &ApiLayoutNode, ids: &mut Vec) { - match node { - ApiLayoutNode::Terminal { terminal_id, .. } => { - if let Some(id) = terminal_id { - ids.push(id.clone()); - } - } - ApiLayoutNode::Split { children, .. } - | ApiLayoutNode::Tabs { children, .. } => { - for child in children { - collect_layout_ids_vec(child, ids); - } - } - } -} - -/// Get all terminal IDs from the cached remote state (flat list). -#[flutter_rust_bridge::frb(sync)] -pub fn get_all_terminal_ids(conn_id: String) -> Vec { - let mgr = ConnectionManager::get(); - match mgr.get_state(&conn_id) { - Some(state) => collect_state_terminal_ids(&state), - None => Vec::new(), - } -} - -// ── Terminal actions ──────────────────────────────────────────────── - -/// Create a new terminal in the given project. -pub async fn create_terminal(conn_id: String, project_id: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action(&conn_id, ActionRequest::CreateTerminal { project_id }) - .await -} - -/// Close a terminal in the given project. -pub async fn close_terminal( - conn_id: String, - project_id: String, - terminal_id: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::CloseTerminal { - project_id, - terminal_id, - }, - ) - .await -} - -/// Close multiple terminals in a project. -pub async fn close_terminals( - conn_id: String, - project_id: String, - terminal_ids: Vec, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::CloseTerminals { - project_id, - terminal_ids, - }, - ) - .await -} - -/// Rename a terminal. -pub async fn rename_terminal( - conn_id: String, - project_id: String, - terminal_id: String, - name: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::RenameTerminal { - project_id, - terminal_id, - name, - }, - ) - .await -} - -/// Focus a terminal. -pub async fn focus_terminal( - conn_id: String, - project_id: String, - terminal_id: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::FocusTerminal { - project_id, - terminal_id, - }, - ) - .await -} - -/// Toggle minimized state of a terminal. -pub async fn toggle_minimized( - conn_id: String, - project_id: String, - terminal_id: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::ToggleMinimized { - project_id, - terminal_id, - }, - ) - .await -} - -/// Set/clear fullscreen terminal. -pub async fn set_fullscreen( - conn_id: String, - project_id: String, - terminal_id: Option, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::SetFullscreen { - project_id, - terminal_id, - }, - ) - .await -} - -/// Split a terminal pane. -pub async fn split_terminal( - conn_id: String, - project_id: String, - path: Vec, - direction: String, -) -> anyhow::Result<()> { - let dir = match direction.as_str() { - "vertical" => SplitDirection::Vertical, - _ => SplitDirection::Horizontal, - }; - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::SplitTerminal { - project_id, - path, - direction: dir, - }, - ) - .await -} - -/// Run a command in a terminal (presses Enter automatically). -pub async fn run_command( - conn_id: String, - terminal_id: String, - command: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::RunCommand { - terminal_id, - command, - }, - ) - .await -} - -/// Read terminal content as text. -pub async fn read_content(conn_id: String, terminal_id: String) -> anyhow::Result { - let mgr = ConnectionManager::get(); - mgr.send_action_with_response( - &conn_id, - ActionRequest::ReadContent { terminal_id }, - ) - .await -} - -// ── Git actions ───────────────────────────────────────────────────── - -/// Get detailed git status for a project. -pub async fn git_status(conn_id: String, project_id: String) -> anyhow::Result { - let mgr = ConnectionManager::get(); - mgr.send_action_with_response( - &conn_id, - ActionRequest::GitStatus { project_id }, - ) - .await -} - -/// Get git diff summary for a project. -pub async fn git_diff_summary(conn_id: String, project_id: String) -> anyhow::Result { - let mgr = ConnectionManager::get(); - mgr.send_action_with_response( - &conn_id, - ActionRequest::GitDiffSummary { project_id }, - ) - .await -} - -/// Get git diff for a project. Mode: "working_tree", "staged". -pub async fn git_diff( - conn_id: String, - project_id: String, - mode: String, -) -> anyhow::Result { - let diff_mode = match mode.as_str() { - "staged" => okena_core::types::DiffMode::Staged, - _ => okena_core::types::DiffMode::WorkingTree, - }; - let mgr = ConnectionManager::get(); - mgr.send_action_with_response( - &conn_id, - ActionRequest::GitDiff { - project_id, - mode: diff_mode, - ignore_whitespace: false, - }, - ) - .await -} - -/// Get git branches for a project. -pub async fn git_branches(conn_id: String, project_id: String) -> anyhow::Result { - let mgr = ConnectionManager::get(); - mgr.send_action_with_response( - &conn_id, - ActionRequest::GitBranches { project_id }, - ) - .await -} - -// ── Service actions ───────────────────────────────────────────────── - -/// Start a service. -pub async fn start_service( - conn_id: String, - project_id: String, - service_name: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::StartService { - project_id, - service_name, - }, - ) - .await -} - -/// Stop a service. -pub async fn stop_service( - conn_id: String, - project_id: String, - service_name: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::StopService { - project_id, - service_name, - }, - ) - .await -} - -/// Restart a service. -pub async fn restart_service( - conn_id: String, - project_id: String, - service_name: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::RestartService { - project_id, - service_name, - }, - ) - .await -} - -/// Start all services in a project. -pub async fn start_all_services(conn_id: String, project_id: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action(&conn_id, ActionRequest::StartAllServices { project_id }) - .await -} - -/// Stop all services in a project. -pub async fn stop_all_services(conn_id: String, project_id: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action(&conn_id, ActionRequest::StopAllServices { project_id }) - .await -} - -/// Reload services config for a project. -pub async fn reload_services(conn_id: String, project_id: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action(&conn_id, ActionRequest::ReloadServices { project_id }) - .await -} - -// ── Project management ────────────────────────────────────────────── - -/// Add a new project. -pub async fn add_project(conn_id: String, name: String, path: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action(&conn_id, ActionRequest::AddProject { name, path }) - .await -} - -/// Set project color. -pub async fn set_project_color( - conn_id: String, - project_id: String, - color: String, -) -> anyhow::Result<()> { - let folder_color: okena_core::theme::FolderColor = - serde_json::from_value(serde_json::Value::String(color.clone())) - .unwrap_or_default(); - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::SetProjectColor { - project_id, - color: folder_color, - }, - ) - .await -} - -/// Set folder color. -pub async fn set_folder_color( - conn_id: String, - folder_id: String, - color: String, -) -> anyhow::Result<()> { - let folder_color: okena_core::theme::FolderColor = - serde_json::from_value(serde_json::Value::String(color.clone())) - .unwrap_or_default(); - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::SetFolderColor { - folder_id, - color: folder_color, - }, - ) - .await -} - -/// Reorder a project within a folder. -pub async fn reorder_project_in_folder( - conn_id: String, - folder_id: String, - project_id: String, - new_index: usize, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::ReorderProjectInFolder { - folder_id, - project_id, - new_index, - }, - ) - .await -} - -// ── Layout actions ───────────────────────────────────────────────── - -/// Update split sizes for a split pane. -pub async fn update_split_sizes( - conn_id: String, - project_id: String, - path: Vec, - sizes: Vec, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::UpdateSplitSizes { - project_id, - path, - sizes, - }, - ) - .await -} - -/// Add a new tab to a tab group. -pub async fn add_tab( - conn_id: String, - project_id: String, - path: Vec, - in_group: bool, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::AddTab { - project_id, - path, - in_group, - }, - ) - .await -} - -/// Set the active tab in a tab group. -pub async fn set_active_tab( - conn_id: String, - project_id: String, - path: Vec, - index: usize, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::SetActiveTab { - project_id, - path, - index, - }, - ) - .await -} - -/// Move a tab within a tab group. -pub async fn move_tab( - conn_id: String, - project_id: String, - path: Vec, - from_index: usize, - to_index: usize, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::MoveTab { - project_id, - path, - from_index, - to_index, - }, - ) - .await -} - -/// Move a terminal into a tab group. -pub async fn move_terminal_to_tab_group( - conn_id: String, - project_id: String, - terminal_id: String, - target_path: Vec, - position: Option, - target_project_id: Option, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::MoveTerminalToTabGroup { - project_id, - terminal_id, - target_path, - position, - target_project_id, - }, - ) - .await -} - -/// Move a pane to a drop zone relative to another terminal. -pub async fn move_pane_to( - conn_id: String, - project_id: String, - terminal_id: String, - target_project_id: String, - target_terminal_id: String, - zone: String, -) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_action( - &conn_id, - ActionRequest::MovePaneTo { - project_id, - terminal_id, - target_project_id, - target_terminal_id, - zone, - }, - ) - .await -} - -// ── Additional git actions ───────────────────────────────────────── - -/// Get file contents from git (working tree or staged). -pub async fn git_file_contents( - conn_id: String, - project_id: String, - file_path: String, - mode: String, -) -> anyhow::Result { - let diff_mode = match mode.as_str() { - "staged" => okena_core::types::DiffMode::Staged, - _ => okena_core::types::DiffMode::WorkingTree, - }; - let mgr = ConnectionManager::get(); - mgr.send_action_with_response( - &conn_id, - ActionRequest::GitFileContents { - project_id, - file_path, - mode: diff_mode, - }, - ) - .await -} diff --git a/mobile/native/src/api/terminal.rs b/mobile/native/src/api/terminal.rs deleted file mode 100644 index 0996e19f..00000000 --- a/mobile/native/src/api/terminal.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::client::manager::ConnectionManager; -use okena_core::client::WsClientMessage; -use okena_core::theme::DARK_THEME; - -/// Cell data for FFI transfer (flat, no pointers). -#[derive(Debug, Clone)] -pub struct CellData { - /// The character in this cell. - pub character: String, - /// Foreground color as ARGB packed u32. - pub fg: u32, - /// Background color as ARGB packed u32. - pub bg: u32, - /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). - pub flags: u8, -} - -/// Cursor shape variants. -#[derive(Debug, Clone)] -pub enum CursorShape { - Block, - Underline, - Beam, -} - -/// Cursor state for FFI transfer. -#[derive(Debug, Clone)] -pub struct CursorState { - pub col: u16, - pub row: u16, - pub shape: CursorShape, - pub visible: bool, -} - -/// Get the visible terminal cells for rendering. -#[flutter_rust_bridge::frb(sync)] -pub fn get_visible_cells(conn_id: String, terminal_id: String) -> Vec { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.get_visible_cells(&DARK_THEME) - }) - .unwrap_or_default() -} - -/// Get the current cursor state. -#[flutter_rust_bridge::frb(sync)] -pub fn get_cursor(conn_id: String, terminal_id: String) -> CursorState { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| holder.get_cursor()) - .unwrap_or(CursorState { - col: 0, - row: 0, - shape: CursorShape::Block, - visible: true, - }) -} - -/// Scroll info for FFI transfer. -#[derive(Debug, Clone)] -pub struct ScrollInfo { - pub total_lines: u32, - pub visible_lines: u32, - pub display_offset: u32, -} - -/// Selection bounds for FFI transfer. -#[derive(Debug, Clone)] -pub struct SelectionBounds { - pub start_col: u16, - pub start_row: i32, - pub end_col: u16, - pub end_row: i32, -} - -/// Scroll the terminal display (positive = up, negative = down). -#[flutter_rust_bridge::frb(sync)] -pub fn scroll(conn_id: String, terminal_id: String, delta: i32) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.scroll(delta); - }); -} - -/// Get scroll info: total lines, visible lines, display offset. -#[flutter_rust_bridge::frb(sync)] -pub fn get_scroll_info(conn_id: String, terminal_id: String) -> ScrollInfo { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - let (total, visible, offset) = holder.scroll_info(); - ScrollInfo { - total_lines: total as u32, - visible_lines: visible as u32, - display_offset: offset as u32, - } - }) - .unwrap_or(ScrollInfo { - total_lines: 0, - visible_lines: 0, - display_offset: 0, - }) -} - -/// Start a character-level selection at col/row. -#[flutter_rust_bridge::frb(sync)] -pub fn start_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.start_selection(col as usize, row as usize); - }); -} - -/// Start a word (semantic) selection at col/row. -#[flutter_rust_bridge::frb(sync)] -pub fn start_word_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.start_word_selection(col as usize, row as usize); - }); -} - -/// Extend the current selection to col/row. -#[flutter_rust_bridge::frb(sync)] -pub fn update_selection(conn_id: String, terminal_id: String, col: u16, row: u16) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.update_selection(col as usize, row as usize); - }); -} - -/// Clear the current selection. -#[flutter_rust_bridge::frb(sync)] -pub fn clear_selection(conn_id: String, terminal_id: String) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.clear_selection(); - }); -} - -/// Get the selected text, if any. -#[flutter_rust_bridge::frb(sync)] -pub fn get_selected_text(conn_id: String, terminal_id: String) -> Option { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.get_selected_text() - }) - .flatten() -} - -/// Get selection bounds for rendering. -#[flutter_rust_bridge::frb(sync)] -pub fn get_selection_bounds(conn_id: String, terminal_id: String) -> Option { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.selection_bounds().map(|((sc, sr), (ec, er))| SelectionBounds { - start_col: sc as u16, - start_row: sr, - end_col: ec as u16, - end_row: er, - }) - }) - .flatten() -} - -/// Send text input to a terminal. -pub async fn send_text(conn_id: String, terminal_id: String, text: String) -> anyhow::Result<()> { - let mgr = ConnectionManager::get(); - mgr.send_ws_message( - &conn_id, - WsClientMessage::SendText { - terminal_id, - text, - }, - ); - Ok(()) -} - -/// Resize a terminal. -#[flutter_rust_bridge::frb(sync)] -pub fn resize_terminal( - conn_id: String, - terminal_id: String, - cols: u16, - rows: u16, -) { - let mgr = ConnectionManager::get(); - mgr.resize_terminal(&conn_id, &terminal_id, cols, rows); -} - -/// Resize only the local alacritty terminal — does NOT send a WS resize message to the server. -/// Used when mobile adapts to the server's terminal size. -#[flutter_rust_bridge::frb(sync)] -pub fn resize_local(conn_id: String, terminal_id: String, cols: u16, rows: u16) { - let mgr = ConnectionManager::get(); - mgr.with_terminal(&conn_id, &terminal_id, |holder| { - holder.resize(cols, rows); - }); -} diff --git a/mobile/native/src/frb_generated.rs b/mobile/native/src/frb_generated.rs deleted file mode 100644 index 754b738e..00000000 --- a/mobile/native/src/frb_generated.rs +++ /dev/null @@ -1,3587 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -#![allow( - non_camel_case_types, - unused, - non_snake_case, - clippy::needless_return, - clippy::redundant_closure_call, - clippy::redundant_closure, - clippy::useless_conversion, - clippy::unit_arg, - clippy::unused_unit, - clippy::double_parens, - clippy::let_and_return, - clippy::too_many_arguments, - clippy::match_single_binding, - clippy::clone_on_copy, - clippy::let_unit_value, - clippy::deref_addrof, - clippy::explicit_auto_deref, - clippy::borrow_deref_ref, - clippy::needless_borrow -)] - -// Section: imports - -use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; -use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; -use flutter_rust_bridge::{Handler, IntoIntoDart}; - -// Section: boilerplate - -flutter_rust_bridge::frb_generated_boilerplate!( - default_stream_sink_codec = SseCodec, - default_rust_opaque = RustOpaqueMoi, - default_rust_auto_opaque = RustAutoOpaqueMoi, -); -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 632182563; - -// Section: executor - -flutter_rust_bridge::frb_generated_default_handler!(); - -// Section: wire_funcs - -fn wire__crate__api__state__add_project_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "add_project", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_name = ::sse_decode(&mut deserializer); - let api_path = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::add_project(api_conn_id, api_name, api_path).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__add_tab_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "add_tab", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_path = >::sse_decode(&mut deserializer); - let api_in_group = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::add_tab( - api_conn_id, - api_project_id, - api_path, - api_in_group, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__clear_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "clear_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::clear_selection(api_conn_id, api_terminal_id); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__close_terminal_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "close_terminal", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::close_terminal( - api_conn_id, - api_project_id, - api_terminal_id, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__close_terminals_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "close_terminals", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_ids = >::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::close_terminals( - api_conn_id, - api_project_id, - api_terminal_ids, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__connection__connect_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "connect", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_host = ::sse_decode(&mut deserializer); - let api_port = ::sse_decode(&mut deserializer); - let api_saved_token = >::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::connection::connect( - api_host, - api_port, - api_saved_token, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__connection_status_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "connection_status", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::connection::connection_status(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__create_terminal_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "create_terminal", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::create_terminal(api_conn_id, api_project_id).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__connection__disconnect_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "disconnect", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::connection::disconnect(api_conn_id); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__focus_terminal_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "focus_terminal", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::focus_terminal( - api_conn_id, - api_project_id, - api_terminal_id, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__get_all_terminal_ids_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_all_terminal_ids", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::get_all_terminal_ids(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_cursor_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_cursor", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_cursor( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_focused_project_id_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_focused_project_id", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::get_focused_project_id(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_folders_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_folders", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::state::get_folders(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_fullscreen_terminal_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_fullscreen_terminal", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::get_fullscreen_terminal(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_project_layout_json_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_project_layout_json", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::state::get_project_layout_json( - api_conn_id, - api_project_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_project_order_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_project_order", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::get_project_order(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__get_projects_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_projects", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::state::get_projects(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_scroll_info_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_scroll_info", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_scroll_info( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_selected_text_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_selected_text", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selected_text( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_selection_bounds_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_selection_bounds", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_selection_bounds( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__get_token_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_token", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::connection::get_token(api_conn_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__get_visible_cells_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "get_visible_cells", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::terminal::get_visible_cells( - api_conn_id, - api_terminal_id, - ))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__git_branches_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "git_branches", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::git_branches(api_conn_id, api_project_id).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__git_diff_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "git_diff", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_mode = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::git_diff(api_conn_id, api_project_id, api_mode) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__git_diff_summary_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "git_diff_summary", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::git_diff_summary(api_conn_id, api_project_id) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__git_file_contents_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "git_file_contents", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_file_path = ::sse_decode(&mut deserializer); - let api_mode = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::git_file_contents( - api_conn_id, - api_project_id, - api_file_path, - api_mode, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__git_status_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "git_status", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::git_status(api_conn_id, api_project_id).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__connection__init_app_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "init_app", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - deserializer.end(); - move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::connection::init_app(); - })?; - Ok(output_ok) - })()) - } - }, - ) -} -fn wire__crate__api__state__is_dirty_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "is_dirty", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::state::is_dirty(api_conn_id, api_terminal_id))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__move_pane_to_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "move_pane_to", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_target_project_id = ::sse_decode(&mut deserializer); - let api_target_terminal_id = ::sse_decode(&mut deserializer); - let api_zone = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::move_pane_to( - api_conn_id, - api_project_id, - api_terminal_id, - api_target_project_id, - api_target_terminal_id, - api_zone, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__move_tab_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "move_tab", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_path = >::sse_decode(&mut deserializer); - let api_from_index = ::sse_decode(&mut deserializer); - let api_to_index = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::move_tab( - api_conn_id, - api_project_id, - api_path, - api_from_index, - api_to_index, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__move_terminal_to_tab_group_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "move_terminal_to_tab_group", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_target_path = >::sse_decode(&mut deserializer); - let api_position = >::sse_decode(&mut deserializer); - let api_target_project_id = >::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::move_terminal_to_tab_group( - api_conn_id, - api_project_id, - api_terminal_id, - api_target_path, - api_position, - api_target_project_id, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__connection__pair_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "pair", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_code = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::connection::pair(api_conn_id, api_code).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__read_content_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "read_content", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::read_content(api_conn_id, api_terminal_id).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__reload_services_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "reload_services", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::reload_services(api_conn_id, api_project_id).await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__rename_terminal_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "rename_terminal", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_name = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::rename_terminal( - api_conn_id, - api_project_id, - api_terminal_id, - api_name, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__reorder_project_in_folder_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "reorder_project_in_folder", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_folder_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_new_index = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::reorder_project_in_folder( - api_conn_id, - api_folder_id, - api_project_id, - api_new_index, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__resize_local_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "resize_local", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_cols = ::sse_decode(&mut deserializer); - let api_rows = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::resize_local( - api_conn_id, - api_terminal_id, - api_cols, - api_rows, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__terminal__resize_terminal_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "resize_terminal", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_cols = ::sse_decode(&mut deserializer); - let api_rows = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::resize_terminal( - api_conn_id, - api_terminal_id, - api_cols, - api_rows, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__restart_service_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "restart_service", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_service_name = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::restart_service( - api_conn_id, - api_project_id, - api_service_name, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__run_command_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "run_command", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_command = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::run_command( - api_conn_id, - api_terminal_id, - api_command, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__scroll_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "scroll", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_delta = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::scroll(api_conn_id, api_terminal_id, api_delta); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__connection__seconds_since_activity_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "seconds_since_activity", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok( - crate::api::connection::seconds_since_activity(api_conn_id), - )?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__send_special_key_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "send_special_key", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_key = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::send_special_key( - api_conn_id, - api_terminal_id, - api_key, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__send_text_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "send_text", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_text = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::terminal::send_text(api_conn_id, api_terminal_id, api_text) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__set_active_tab_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "set_active_tab", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_path = >::sse_decode(&mut deserializer); - let api_index = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::set_active_tab( - api_conn_id, - api_project_id, - api_path, - api_index, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__set_folder_color_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "set_folder_color", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_folder_id = ::sse_decode(&mut deserializer); - let api_color = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::set_folder_color( - api_conn_id, - api_folder_id, - api_color, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__set_fullscreen_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "set_fullscreen", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = >::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::set_fullscreen( - api_conn_id, - api_project_id, - api_terminal_id, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__set_project_color_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "set_project_color", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_color = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::set_project_color( - api_conn_id, - api_project_id, - api_color, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__split_terminal_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "split_terminal", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_path = >::sse_decode(&mut deserializer); - let api_direction = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::split_terminal( - api_conn_id, - api_project_id, - api_path, - api_direction, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__start_all_services_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "start_all_services", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::start_all_services(api_conn_id, api_project_id) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__start_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "start_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_col = ::sse_decode(&mut deserializer); - let api_row = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::start_selection( - api_conn_id, - api_terminal_id, - api_col, - api_row, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__start_service_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "start_service", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_service_name = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::start_service( - api_conn_id, - api_project_id, - api_service_name, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__start_word_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "start_word_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_col = ::sse_decode(&mut deserializer); - let api_row = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::start_word_selection( - api_conn_id, - api_terminal_id, - api_col, - api_row, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__stop_all_services_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "stop_all_services", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = - crate::api::state::stop_all_services(api_conn_id, api_project_id) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__stop_service_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "stop_service", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_service_name = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::stop_service( - api_conn_id, - api_project_id, - api_service_name, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__state__toggle_minimized_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "toggle_minimized", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::toggle_minimized( - api_conn_id, - api_project_id, - api_terminal_id, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} -fn wire__crate__api__terminal__update_selection_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "update_selection", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_terminal_id = ::sse_decode(&mut deserializer); - let api_col = ::sse_decode(&mut deserializer); - let api_row = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::terminal::update_selection( - api_conn_id, - api_terminal_id, - api_col, - api_row, - ); - })?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__state__update_split_sizes_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "update_split_sizes", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_conn_id = ::sse_decode(&mut deserializer); - let api_project_id = ::sse_decode(&mut deserializer); - let api_path = >::sse_decode(&mut deserializer); - let api_sizes = >::sse_decode(&mut deserializer); - deserializer.end(); - move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || async move { - let output_ok = crate::api::state::update_split_sizes( - api_conn_id, - api_project_id, - api_path, - api_sizes, - ) - .await?; - Ok(output_ok) - })() - .await, - ) - } - }, - ) -} - -// Section: dart2rust - -impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); - } -} - -impl SseDecode for std::collections::HashMap { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = >::sse_decode(deserializer); - return inner.into_iter().collect(); - } -} - -impl SseDecode for String { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = >::sse_decode(deserializer); - return String::from_utf8(inner).unwrap(); - } -} - -impl SseDecode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() != 0 - } -} - -impl SseDecode for crate::api::terminal::CellData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_character = ::sse_decode(deserializer); - let mut var_fg = ::sse_decode(deserializer); - let mut var_bg = ::sse_decode(deserializer); - let mut var_flags = ::sse_decode(deserializer); - return crate::api::terminal::CellData { - character: var_character, - fg: var_fg, - bg: var_bg, - flags: var_flags, - }; - } -} - -impl SseDecode for crate::api::connection::ConnectionStatus { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut tag_ = ::sse_decode(deserializer); - match tag_ { - 0 => { - return crate::api::connection::ConnectionStatus::Disconnected; - } - 1 => { - return crate::api::connection::ConnectionStatus::Connecting; - } - 2 => { - return crate::api::connection::ConnectionStatus::Connected; - } - 3 => { - return crate::api::connection::ConnectionStatus::Pairing; - } - 4 => { - let mut var_message = ::sse_decode(deserializer); - return crate::api::connection::ConnectionStatus::Error { - message: var_message, - }; - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseDecode for crate::api::terminal::CursorShape { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return match inner { - 0 => crate::api::terminal::CursorShape::Block, - 1 => crate::api::terminal::CursorShape::Underline, - 2 => crate::api::terminal::CursorShape::Beam, - _ => unreachable!("Invalid variant for CursorShape: {}", inner), - }; - } -} - -impl SseDecode for crate::api::terminal::CursorState { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_col = ::sse_decode(deserializer); - let mut var_row = ::sse_decode(deserializer); - let mut var_shape = ::sse_decode(deserializer); - let mut var_visible = ::sse_decode(deserializer); - return crate::api::terminal::CursorState { - col: var_col, - row: var_row, - shape: var_shape, - visible: var_visible, - }; - } -} - -impl SseDecode for f32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_f32::().unwrap() - } -} - -impl SseDecode for f64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_f64::().unwrap() - } -} - -impl SseDecode for crate::api::state::FolderInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_id = ::sse_decode(deserializer); - let mut var_name = ::sse_decode(deserializer); - let mut var_projectIds = >::sse_decode(deserializer); - let mut var_folderColor = ::sse_decode(deserializer); - return crate::api::state::FolderInfo { - id: var_id, - name: var_name, - project_ids: var_projectIds, - folder_color: var_folderColor, - }; - } -} - -impl SseDecode for crate::api::state::FullscreenInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_projectId = ::sse_decode(deserializer); - let mut var_terminalId = ::sse_decode(deserializer); - return crate::api::state::FullscreenInfo { - project_id: var_projectId, - terminal_id: var_terminalId, - }; - } -} - -impl SseDecode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_i32::().unwrap() - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec<(String, String)> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(<(String, String)>::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = vec![]; - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } - } -} - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode( - deserializer, - )); - } else { - return None; - } - } -} - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode( - deserializer, - )); - } else { - return None; - } - } -} - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } - } -} - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } - } -} - -impl SseDecode for crate::api::state::ProjectInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_id = ::sse_decode(deserializer); - let mut var_name = ::sse_decode(deserializer); - let mut var_path = ::sse_decode(deserializer); - let mut var_showInOverview = ::sse_decode(deserializer); - let mut var_terminalIds = >::sse_decode(deserializer); - let mut var_terminalNames = - >::sse_decode(deserializer); - let mut var_gitBranch = >::sse_decode(deserializer); - let mut var_gitLinesAdded = ::sse_decode(deserializer); - let mut var_gitLinesRemoved = ::sse_decode(deserializer); - let mut var_services = >::sse_decode(deserializer); - let mut var_folderColor = ::sse_decode(deserializer); - return crate::api::state::ProjectInfo { - id: var_id, - name: var_name, - path: var_path, - show_in_overview: var_showInOverview, - terminal_ids: var_terminalIds, - terminal_names: var_terminalNames, - git_branch: var_gitBranch, - git_lines_added: var_gitLinesAdded, - git_lines_removed: var_gitLinesRemoved, - services: var_services, - folder_color: var_folderColor, - }; - } -} - -impl SseDecode for (String, String) { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_field0 = ::sse_decode(deserializer); - let mut var_field1 = ::sse_decode(deserializer); - return (var_field0, var_field1); - } -} - -impl SseDecode for crate::api::terminal::ScrollInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_totalLines = ::sse_decode(deserializer); - let mut var_visibleLines = ::sse_decode(deserializer); - let mut var_displayOffset = ::sse_decode(deserializer); - return crate::api::terminal::ScrollInfo { - total_lines: var_totalLines, - visible_lines: var_visibleLines, - display_offset: var_displayOffset, - }; - } -} - -impl SseDecode for crate::api::terminal::SelectionBounds { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_startCol = ::sse_decode(deserializer); - let mut var_startRow = ::sse_decode(deserializer); - let mut var_endCol = ::sse_decode(deserializer); - let mut var_endRow = ::sse_decode(deserializer); - return crate::api::terminal::SelectionBounds { - start_col: var_startCol, - start_row: var_startRow, - end_col: var_endCol, - end_row: var_endRow, - }; - } -} - -impl SseDecode for crate::api::state::ServiceInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_name = ::sse_decode(deserializer); - let mut var_status = ::sse_decode(deserializer); - let mut var_terminalId = >::sse_decode(deserializer); - let mut var_ports = >::sse_decode(deserializer); - let mut var_exitCode = >::sse_decode(deserializer); - let mut var_kind = ::sse_decode(deserializer); - let mut var_isExtra = ::sse_decode(deserializer); - return crate::api::state::ServiceInfo { - name: var_name, - status: var_status, - terminal_id: var_terminalId, - ports: var_ports, - exit_code: var_exitCode, - kind: var_kind, - is_extra: var_isExtra, - }; - } -} - -impl SseDecode for u16 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u16::().unwrap() - } -} - -impl SseDecode for u32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u32::().unwrap() - } -} - -impl SseDecode for u8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() - } -} - -impl SseDecode for () { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} -} - -impl SseDecode for usize { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u64::().unwrap() as _ - } -} - -fn pde_ffi_dispatcher_primary_impl( - func_id: i32, - port: flutter_rust_bridge::for_generated::MessagePort, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - 1 => wire__crate__api__state__add_project_impl(port, ptr, rust_vec_len, data_len), - 2 => wire__crate__api__state__add_tab_impl(port, ptr, rust_vec_len, data_len), - 4 => wire__crate__api__state__close_terminal_impl(port, ptr, rust_vec_len, data_len), - 5 => wire__crate__api__state__close_terminals_impl(port, ptr, rust_vec_len, data_len), - 8 => wire__crate__api__state__create_terminal_impl(port, ptr, rust_vec_len, data_len), - 10 => wire__crate__api__state__focus_terminal_impl(port, ptr, rust_vec_len, data_len), - 24 => wire__crate__api__state__git_branches_impl(port, ptr, rust_vec_len, data_len), - 25 => wire__crate__api__state__git_diff_impl(port, ptr, rust_vec_len, data_len), - 26 => wire__crate__api__state__git_diff_summary_impl(port, ptr, rust_vec_len, data_len), - 27 => wire__crate__api__state__git_file_contents_impl(port, ptr, rust_vec_len, data_len), - 28 => wire__crate__api__state__git_status_impl(port, ptr, rust_vec_len, data_len), - 29 => wire__crate__api__connection__init_app_impl(port, ptr, rust_vec_len, data_len), - 31 => wire__crate__api__state__move_pane_to_impl(port, ptr, rust_vec_len, data_len), - 32 => wire__crate__api__state__move_tab_impl(port, ptr, rust_vec_len, data_len), - 33 => wire__crate__api__state__move_terminal_to_tab_group_impl( - port, - ptr, - rust_vec_len, - data_len, - ), - 34 => wire__crate__api__connection__pair_impl(port, ptr, rust_vec_len, data_len), - 35 => wire__crate__api__state__read_content_impl(port, ptr, rust_vec_len, data_len), - 36 => wire__crate__api__state__reload_services_impl(port, ptr, rust_vec_len, data_len), - 37 => wire__crate__api__state__rename_terminal_impl(port, ptr, rust_vec_len, data_len), - 38 => wire__crate__api__state__reorder_project_in_folder_impl( - port, - ptr, - rust_vec_len, - data_len, - ), - 41 => wire__crate__api__state__restart_service_impl(port, ptr, rust_vec_len, data_len), - 42 => wire__crate__api__state__run_command_impl(port, ptr, rust_vec_len, data_len), - 45 => wire__crate__api__state__send_special_key_impl(port, ptr, rust_vec_len, data_len), - 46 => wire__crate__api__terminal__send_text_impl(port, ptr, rust_vec_len, data_len), - 47 => wire__crate__api__state__set_active_tab_impl(port, ptr, rust_vec_len, data_len), - 48 => wire__crate__api__state__set_folder_color_impl(port, ptr, rust_vec_len, data_len), - 49 => wire__crate__api__state__set_fullscreen_impl(port, ptr, rust_vec_len, data_len), - 50 => wire__crate__api__state__set_project_color_impl(port, ptr, rust_vec_len, data_len), - 51 => wire__crate__api__state__split_terminal_impl(port, ptr, rust_vec_len, data_len), - 52 => wire__crate__api__state__start_all_services_impl(port, ptr, rust_vec_len, data_len), - 54 => wire__crate__api__state__start_service_impl(port, ptr, rust_vec_len, data_len), - 56 => wire__crate__api__state__stop_all_services_impl(port, ptr, rust_vec_len, data_len), - 57 => wire__crate__api__state__stop_service_impl(port, ptr, rust_vec_len, data_len), - 58 => wire__crate__api__state__toggle_minimized_impl(port, ptr, rust_vec_len, data_len), - 60 => wire__crate__api__state__update_split_sizes_impl(port, ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} - -fn pde_ffi_dispatcher_sync_impl( - func_id: i32, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - 3 => wire__crate__api__terminal__clear_selection_impl(ptr, rust_vec_len, data_len), - 6 => wire__crate__api__connection__connect_impl(ptr, rust_vec_len, data_len), - 7 => wire__crate__api__connection__connection_status_impl(ptr, rust_vec_len, data_len), - 9 => wire__crate__api__connection__disconnect_impl(ptr, rust_vec_len, data_len), - 11 => wire__crate__api__state__get_all_terminal_ids_impl(ptr, rust_vec_len, data_len), - 12 => wire__crate__api__terminal__get_cursor_impl(ptr, rust_vec_len, data_len), - 13 => wire__crate__api__state__get_focused_project_id_impl(ptr, rust_vec_len, data_len), - 14 => wire__crate__api__state__get_folders_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__api__state__get_fullscreen_terminal_impl(ptr, rust_vec_len, data_len), - 16 => wire__crate__api__state__get_project_layout_json_impl(ptr, rust_vec_len, data_len), - 17 => wire__crate__api__state__get_project_order_impl(ptr, rust_vec_len, data_len), - 18 => wire__crate__api__state__get_projects_impl(ptr, rust_vec_len, data_len), - 19 => wire__crate__api__terminal__get_scroll_info_impl(ptr, rust_vec_len, data_len), - 20 => wire__crate__api__terminal__get_selected_text_impl(ptr, rust_vec_len, data_len), - 21 => wire__crate__api__terminal__get_selection_bounds_impl(ptr, rust_vec_len, data_len), - 22 => wire__crate__api__connection__get_token_impl(ptr, rust_vec_len, data_len), - 23 => wire__crate__api__terminal__get_visible_cells_impl(ptr, rust_vec_len, data_len), - 30 => wire__crate__api__state__is_dirty_impl(ptr, rust_vec_len, data_len), - 39 => wire__crate__api__terminal__resize_local_impl(ptr, rust_vec_len, data_len), - 40 => wire__crate__api__terminal__resize_terminal_impl(ptr, rust_vec_len, data_len), - 43 => wire__crate__api__terminal__scroll_impl(ptr, rust_vec_len, data_len), - 44 => { - wire__crate__api__connection__seconds_since_activity_impl(ptr, rust_vec_len, data_len) - } - 53 => wire__crate__api__terminal__start_selection_impl(ptr, rust_vec_len, data_len), - 55 => wire__crate__api__terminal__start_word_selection_impl(ptr, rust_vec_len, data_len), - 59 => wire__crate__api__terminal__update_selection_impl(ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} - -// Section: rust2dart - -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::CellData { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.character.into_into_dart().into_dart(), - self.fg.into_into_dart().into_dart(), - self.bg.into_into_dart().into_dart(), - self.flags.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::CellData -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::CellData -{ - fn into_into_dart(self) -> crate::api::terminal::CellData { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::connection::ConnectionStatus { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - crate::api::connection::ConnectionStatus::Disconnected => [0.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Connecting => [1.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Connected => [2.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Pairing => [3.into_dart()].into_dart(), - crate::api::connection::ConnectionStatus::Error { message } => { - [4.into_dart(), message.into_into_dart().into_dart()].into_dart() - } - _ => { - unimplemented!(""); - } - } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::connection::ConnectionStatus -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::connection::ConnectionStatus -{ - fn into_into_dart(self) -> crate::api::connection::ConnectionStatus { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::CursorShape { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - Self::Block => 0.into_dart(), - Self::Underline => 1.into_dart(), - Self::Beam => 2.into_dart(), - _ => unreachable!(), - } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::CursorShape -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::CursorShape -{ - fn into_into_dart(self) -> crate::api::terminal::CursorShape { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::CursorState { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.col.into_into_dart().into_dart(), - self.row.into_into_dart().into_dart(), - self.shape.into_into_dart().into_dart(), - self.visible.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::CursorState -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::CursorState -{ - fn into_into_dart(self) -> crate::api::terminal::CursorState { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::state::FolderInfo { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.id.into_into_dart().into_dart(), - self.name.into_into_dart().into_dart(), - self.project_ids.into_into_dart().into_dart(), - self.folder_color.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::state::FolderInfo {} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::state::FolderInfo -{ - fn into_into_dart(self) -> crate::api::state::FolderInfo { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::state::FullscreenInfo { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.project_id.into_into_dart().into_dart(), - self.terminal_id.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::state::FullscreenInfo -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::state::FullscreenInfo -{ - fn into_into_dart(self) -> crate::api::state::FullscreenInfo { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::state::ProjectInfo { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.id.into_into_dart().into_dart(), - self.name.into_into_dart().into_dart(), - self.path.into_into_dart().into_dart(), - self.show_in_overview.into_into_dart().into_dart(), - self.terminal_ids.into_into_dart().into_dart(), - self.terminal_names.into_into_dart().into_dart(), - self.git_branch.into_into_dart().into_dart(), - self.git_lines_added.into_into_dart().into_dart(), - self.git_lines_removed.into_into_dart().into_dart(), - self.services.into_into_dart().into_dart(), - self.folder_color.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::state::ProjectInfo -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::state::ProjectInfo -{ - fn into_into_dart(self) -> crate::api::state::ProjectInfo { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::ScrollInfo { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.total_lines.into_into_dart().into_dart(), - self.visible_lines.into_into_dart().into_dart(), - self.display_offset.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::ScrollInfo -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::ScrollInfo -{ - fn into_into_dart(self) -> crate::api::terminal::ScrollInfo { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::terminal::SelectionBounds { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.start_col.into_into_dart().into_dart(), - self.start_row.into_into_dart().into_dart(), - self.end_col.into_into_dart().into_dart(), - self.end_row.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::terminal::SelectionBounds -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::terminal::SelectionBounds -{ - fn into_into_dart(self) -> crate::api::terminal::SelectionBounds { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::state::ServiceInfo { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.name.into_into_dart().into_dart(), - self.status.into_into_dart().into_dart(), - self.terminal_id.into_into_dart().into_dart(), - self.ports.into_into_dart().into_dart(), - self.exit_code.into_into_dart().into_dart(), - self.kind.into_into_dart().into_dart(), - self.is_extra.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::state::ServiceInfo -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::state::ServiceInfo -{ - fn into_into_dart(self) -> crate::api::state::ServiceInfo { - self - } -} - -impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(format!("{:?}", self), serializer); - } -} - -impl SseEncode for std::collections::HashMap { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >::sse_encode(self.into_iter().collect(), serializer); - } -} - -impl SseEncode for String { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >::sse_encode(self.into_bytes(), serializer); - } -} - -impl SseEncode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u8(self as _).unwrap(); - } -} - -impl SseEncode for crate::api::terminal::CellData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.character, serializer); - ::sse_encode(self.fg, serializer); - ::sse_encode(self.bg, serializer); - ::sse_encode(self.flags, serializer); - } -} - -impl SseEncode for crate::api::connection::ConnectionStatus { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - match self { - crate::api::connection::ConnectionStatus::Disconnected => { - ::sse_encode(0, serializer); - } - crate::api::connection::ConnectionStatus::Connecting => { - ::sse_encode(1, serializer); - } - crate::api::connection::ConnectionStatus::Connected => { - ::sse_encode(2, serializer); - } - crate::api::connection::ConnectionStatus::Pairing => { - ::sse_encode(3, serializer); - } - crate::api::connection::ConnectionStatus::Error { message } => { - ::sse_encode(4, serializer); - ::sse_encode(message, serializer); - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseEncode for crate::api::terminal::CursorShape { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode( - match self { - crate::api::terminal::CursorShape::Block => 0, - crate::api::terminal::CursorShape::Underline => 1, - crate::api::terminal::CursorShape::Beam => 2, - _ => { - unimplemented!(""); - } - }, - serializer, - ); - } -} - -impl SseEncode for crate::api::terminal::CursorState { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.col, serializer); - ::sse_encode(self.row, serializer); - ::sse_encode(self.shape, serializer); - ::sse_encode(self.visible, serializer); - } -} - -impl SseEncode for f32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_f32::(self).unwrap(); - } -} - -impl SseEncode for f64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_f64::(self).unwrap(); - } -} - -impl SseEncode for crate::api::state::FolderInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.id, serializer); - ::sse_encode(self.name, serializer); - >::sse_encode(self.project_ids, serializer); - ::sse_encode(self.folder_color, serializer); - } -} - -impl SseEncode for crate::api::state::FullscreenInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.project_id, serializer); - ::sse_encode(self.terminal_id, serializer); - } -} - -impl SseEncode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_i32::(self).unwrap(); - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec<(String, String)> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - <(String, String)>::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} - -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - -impl SseEncode for crate::api::state::ProjectInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.id, serializer); - ::sse_encode(self.name, serializer); - ::sse_encode(self.path, serializer); - ::sse_encode(self.show_in_overview, serializer); - >::sse_encode(self.terminal_ids, serializer); - >::sse_encode(self.terminal_names, serializer); - >::sse_encode(self.git_branch, serializer); - ::sse_encode(self.git_lines_added, serializer); - ::sse_encode(self.git_lines_removed, serializer); - >::sse_encode(self.services, serializer); - ::sse_encode(self.folder_color, serializer); - } -} - -impl SseEncode for (String, String) { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.0, serializer); - ::sse_encode(self.1, serializer); - } -} - -impl SseEncode for crate::api::terminal::ScrollInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.total_lines, serializer); - ::sse_encode(self.visible_lines, serializer); - ::sse_encode(self.display_offset, serializer); - } -} - -impl SseEncode for crate::api::terminal::SelectionBounds { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.start_col, serializer); - ::sse_encode(self.start_row, serializer); - ::sse_encode(self.end_col, serializer); - ::sse_encode(self.end_row, serializer); - } -} - -impl SseEncode for crate::api::state::ServiceInfo { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.name, serializer); - ::sse_encode(self.status, serializer); - >::sse_encode(self.terminal_id, serializer); - >::sse_encode(self.ports, serializer); - >::sse_encode(self.exit_code, serializer); - ::sse_encode(self.kind, serializer); - ::sse_encode(self.is_extra, serializer); - } -} - -impl SseEncode for u16 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u16::(self).unwrap(); - } -} - -impl SseEncode for u32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u32::(self).unwrap(); - } -} - -impl SseEncode for u8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u8(self).unwrap(); - } -} - -impl SseEncode for () { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} -} - -impl SseEncode for usize { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer - .cursor - .write_u64::(self as _) - .unwrap(); - } -} - -#[cfg(not(target_family = "wasm"))] -mod io { - // This file is automatically generated, so please do not edit it. - // @generated by `flutter_rust_bridge`@ 2.11.1. - - // Section: imports - - use super::*; - use flutter_rust_bridge::for_generated::byteorder::{ - NativeEndian, ReadBytesExt, WriteBytesExt, - }; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; - use flutter_rust_bridge::{Handler, IntoIntoDart}; - - // Section: boilerplate - - flutter_rust_bridge::frb_generated_boilerplate_io!(); -} -#[cfg(not(target_family = "wasm"))] -pub use io::*; - -/// cbindgen:ignore -#[cfg(target_family = "wasm")] -mod web { - // This file is automatically generated, so please do not edit it. - // @generated by `flutter_rust_bridge`@ 2.11.1. - - // Section: imports - - use super::*; - use flutter_rust_bridge::for_generated::byteorder::{ - NativeEndian, ReadBytesExt, WriteBytesExt, - }; - use flutter_rust_bridge::for_generated::wasm_bindgen; - use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; - use flutter_rust_bridge::{Handler, IntoIntoDart}; - - // Section: boilerplate - - flutter_rust_bridge::frb_generated_boilerplate_web!(); -} -#[cfg(target_family = "wasm")] -pub use web::*; diff --git a/mobile/native/src/lib.rs b/mobile/native/src/lib.rs deleted file mode 100644 index 3c536c58..00000000 --- a/mobile/native/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))] - -pub mod api; -pub mod client; -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod frb_generated; From 88218f50ddccfa521a37d33cfb8047db89e1b92c Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 10 Jun 2026 13:21:28 +0200 Subject: [PATCH 32/37] feat(mobile-rn): add RN host project, tooling, and ubrn wiring Turn the mobile/rn scaffold into a complete React Native 0.76 project (minus the machine-generated native host dirs): index.js, app.json, metro/babel/ react-native config, jest + eslint + prettier, and the RN 0.76 devDependency set. Add ubrn.config.yaml and ubrn:* scripts, and wire getOkenaNative() to load the generated module from src/generated. Relocate the JetBrainsMono fonts to mobile/rn/assets. npm run typecheck + lint + test all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/rn/.eslintrc.js | 22 + mobile/rn/.gitignore | 5 + mobile/rn/.prettierrc | 8 + mobile/rn/.watchmanconfig | 1 + mobile/rn/CLAUDE.md | 47 + mobile/rn/README.md | 228 +- mobile/rn/__tests__/cells.test.ts | 84 + mobile/rn/app.json | 4 + .../assets}/JetBrainsMono-Bold.ttf | Bin .../assets}/JetBrainsMono-BoldItalic.ttf | Bin .../assets}/JetBrainsMono-Italic.ttf | Bin .../assets}/JetBrainsMono-Regular.ttf | Bin mobile/rn/babel.config.js | 3 + mobile/rn/index.js | 16 + mobile/rn/jest.config.js | 3 + mobile/rn/metro.config.js | 11 + mobile/rn/package-lock.json | 8897 ++++++++++++++--- mobile/rn/package.json | 40 +- mobile/rn/react-native.config.js | 15 + mobile/rn/src/components/StatusIndicator.tsx | 14 +- mobile/rn/src/components/TerminalView.tsx | 10 +- mobile/rn/src/models/layoutNode.ts | 20 +- mobile/rn/src/native/cells.ts | 9 +- mobile/rn/src/native/okena.ts | 44 +- mobile/rn/ubrn.config.yaml | 43 + 25 files changed, 7949 insertions(+), 1575 deletions(-) create mode 100644 mobile/rn/.eslintrc.js create mode 100644 mobile/rn/.prettierrc create mode 100644 mobile/rn/.watchmanconfig create mode 100644 mobile/rn/CLAUDE.md create mode 100644 mobile/rn/__tests__/cells.test.ts create mode 100644 mobile/rn/app.json rename mobile/{fonts => rn/assets}/JetBrainsMono-Bold.ttf (100%) rename mobile/{fonts => rn/assets}/JetBrainsMono-BoldItalic.ttf (100%) rename mobile/{fonts => rn/assets}/JetBrainsMono-Italic.ttf (100%) rename mobile/{fonts => rn/assets}/JetBrainsMono-Regular.ttf (100%) create mode 100644 mobile/rn/babel.config.js create mode 100644 mobile/rn/index.js create mode 100644 mobile/rn/jest.config.js create mode 100644 mobile/rn/metro.config.js create mode 100644 mobile/rn/react-native.config.js create mode 100644 mobile/rn/ubrn.config.yaml diff --git a/mobile/rn/.eslintrc.js b/mobile/rn/.eslintrc.js new file mode 100644 index 00000000..0e657aac --- /dev/null +++ b/mobile/rn/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + root: true, + extends: '@react-native', + rules: { + // Formatting is handled by the opt-in `npm run format` (prettier), not + // enforced as a lint error — the hand-authored sources use deliberate + // alignment (e.g. the binding-contract comment boxes) that we keep as-is. + 'prettier/prettier': 'off', + + // Bitwise ops are core to this codebase: decoding the packed cell buffer + // and ARGB ⇄ channel/CSS conversion (native/cells.ts, theme.ts). + 'no-bitwise': 'off', + + // `void promise` is the intentional "fire-and-forget" marker used across the + // stores (e.g. `void conn.loadServers()`), so it stays allowed. + 'no-void': 'off', + + // The sources use concise single-line guards (`if (cond) return;`); we don't + // force braces on them. ESLint still flags real correctness issues. + curly: 'off', + }, +}; diff --git a/mobile/rn/.gitignore b/mobile/rn/.gitignore index 99e8c630..541dc6e6 100644 --- a/mobile/rn/.gitignore +++ b/mobile/rn/.gitignore @@ -4,3 +4,8 @@ android/ ios/ .expo/ *.log + +# ubrn output: generated bindings + checked-out rust (run `npm run ubrn:android|ios`) +src/generated/ +cpp/generated/ +rust_modules/ diff --git a/mobile/rn/.prettierrc b/mobile/rn/.prettierrc new file mode 100644 index 00000000..79d0274d --- /dev/null +++ b/mobile/rn/.prettierrc @@ -0,0 +1,8 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/mobile/rn/.watchmanconfig b/mobile/rn/.watchmanconfig new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/mobile/rn/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/mobile/rn/CLAUDE.md b/mobile/rn/CLAUDE.md new file mode 100644 index 00000000..ae6e4766 --- /dev/null +++ b/mobile/rn/CLAUDE.md @@ -0,0 +1,47 @@ +# Mobile App — React Native (uniffi over the Rust core) + +Remote terminal client for Android/iOS. RN UI over the shared Rust core +(`crates/okena-mobile-ffi`, exposed via uniffi/ubrn as a JSI TurboModule). Native terminal +rendering with `react-native-skia` — **no `xterm.js`**. Replaced the retired Flutter app. + +Architecture overview: `../../docs/mobile-status.md`. Migration plan: `../RN_MIGRATION.md`. + +## Commands + +```bash +cd mobile/rn +npm ci +npm run typecheck # tsc --noEmit, strict +npm run lint # eslint +npm test # jest +npm run format # prettier (opt-in; NOT enforced by lint) +``` + +Device build (needs Android NDK / Xcode — see `README.md`): `npm run ubrn:android|ios` then +`npm run android|ios`. The native host dirs (`android/`, `ios/`) and ubrn output +(`src/generated/`) are generated and gitignored. + +## Key boundaries + +- **`src/native/okena.ts`** — the `OkenaNative` TS interface: the hand-maintained contract for + the ~60 functions in `crates/okena-mobile-ffi/src/lib.rs`. Keep both sides in sync. + `getOkenaNative()` `require`s the ubrn-generated module from `src/generated` (throws with a + "run ubrn" message until generated). +- **`src/native/cells.ts`** — decoder for the packed cell buffer from `get_visible_cells_packed` + (the render hot path). Its byte layout is the contract the Rust encoder must match; the jest + smoke test (`__tests__/cells.test.ts`) guards it. +- The native module is **dependency-injected**, never imported globally: stores via + `configureConnectionStore` / `configureWorkspaceStore`, `TerminalView`/`TerminalPane` via a + `native` prop. This keeps everything testable with a mock and lets `tsc`/jest run with no + native module present. + +## Conventions + +- **Package manager: npm** (`package-lock.json`). Don't switch — RN autolinking / CocoaPods / + ubrn are validated against npm/yarn. +- **State: zustand** stores with polling (mirrors the old provider cadence): fast (500ms) while + connecting, slow (1–2s) when connected. +- **ESLint** enforces correctness only; `prettier/prettier`, `no-bitwise` (cell/ARGB decoding), + `no-void` (fire-and-forget), and `curly` are off by design (see `.eslintrc.js`). +- **uniffi ⇄ ubrn version pairing** must match: `uniffi = "0.31"` (crate) ↔ + `uniffi-bindgen-react-native@0.31.0-3` (devDep). Bump together. diff --git a/mobile/rn/README.md b/mobile/rn/README.md index dc7808a0..edcb5ad9 100644 --- a/mobile/rn/README.md +++ b/mobile/rn/README.md @@ -1,170 +1,142 @@ -# Okena mobile — React Native UI (scaffold) - -This directory is the **React Native** side of the Flutter→RN migration described in -[`../RN_MIGRATION.md`](../RN_MIGRATION.md). It contains the two technically-meaty, -high-value pieces of the RN layer plus minimal project config: - -1. **The native↔TS binding contract** — `src/native/okena.ts`. The full `OkenaNative` - interface mirroring the ~60 Rust FFI functions in `mobile/native/src/api/{connection,terminal,state}.rs`, - with all the record/enum types. -2. **The packed-cell decoder** — `src/native/cells.ts`. Reads the little-endian binary - cell buffer that the render hot path consumes. -3. **The native terminal renderer** — `src/components/TerminalView.tsx`. A - `@shopify/react-native-skia` port of `mobile/lib/src/widgets/terminal_painter.dart` - (3-pass paint) + the sizing/poll loop from `terminal_view.dart`. **No `xterm.js`.** -4. **Theme** — `src/theme.ts`, ported from `mobile/lib/src/theme/app_theme.dart`. - -## ⚠️ This is an UN-BUILT scaffold - -It was authored on a **headless Linux box with no Android/iOS SDKs**, so it has **not** -been compiled or run on a device. What IS verified: the TypeScript type-checks against -the public APIs of `react-native@0.76` and `@shopify/react-native-skia@^1.5` (run -`npm install && npm run typecheck` once you have network). What is NOT verified: anything -that needs the native toolchain (the `ubrn`-generated module, Skia native binaries, an -emulator/device). - -The contract files (`src/native/*`) are **specs both sides agree on**. The real native -module is *generated* — `src/native/okena.ts`'s `getOkenaNative()` throws until you wire -the generated package in (see step 4 below). `TerminalView` takes the native module via a -prop, so it is testable against a mock implementing `OkenaNative` without the real binding. - -### One function does not exist Rust-side yet - -`getVisibleCellsPacked(connId, terminalId): ArrayBuffer` is **being added** to the Rust -crate (`crates/okena-mobile-ffi`, migration Phase 1). The exact byte layout is the -contract documented in `src/native/cells.ts`. Until it lands, the renderer's per-frame -`getVisibleCellsPacked` call will fail — fall back to `getVisibleCells` (records) or mock it. +# Okena mobile — React Native ---- +The **React Native** mobile client: the UI layer over the shared Rust core +(`crates/okena-mobile-ffi`, exposed to TypeScript via uniffi/ubrn), with a native +`react-native-skia` terminal renderer (**no `xterm.js`**). This replaces the retired Flutter +app; the migration plan is [`../RN_MIGRATION.md`](../RN_MIGRATION.md) and the architecture +overview is [`../../docs/mobile-status.md`](../../docs/mobile-status.md). + +## What's here -## Local build steps (run these on a machine with the RN toolchain) +A complete RN 0.76 project **minus the native host directories** (`android/`, `ios/`), which +are machine-generated (see step 1 below). What is in the repo: -Prereqs: Node ≥ 18, Watchman, JDK 17, Android SDK + NDK (for Android), Xcode + CocoaPods -(for iOS), and the Rust toolchain with the mobile targets (`aarch64-linux-android`, -`aarch64-apple-ios`, `aarch64-apple-ios-sim`). +- **JS host config** — `index.js`, `app.json`, `metro.config.js`, `babel.config.js`, + `react-native.config.js`, `tsconfig.json`, `jest.config.js`, `.eslintrc.js`, `.prettierrc`. +- **The native↔TS binding contract** — `src/native/okena.ts`: the `OkenaNative` interface + (the ~60 functions exported from `crates/okena-mobile-ffi/src/lib.rs`) + all record/enum + types, plus `getOkenaNative()` which resolves the ubrn-generated module from `src/generated`. +- **The packed-cell decoder** — `src/native/cells.ts`: reads the little-endian cell buffer + that `get_visible_cells_packed` produces (the render hot path). +- **App** — screens (`ServerList`, `Pairing`, `Workspace`), zustand stores (dependency- + injected, so testable with a mock `OkenaNative`), `TerminalView` (Skia 3-pass paint), + `KeyToolbar`, `LayoutRenderer`, `ProjectDrawer`, theme, and the JetBrainsMono fonts + (`assets/`). -### 1. Scaffold a bare RN host app (new architecture ON) +### Verified vs. not verified -`@shopify/react-native-skia` and the `ubrn` native module are TurboModules/Fabric, so a -**bare** RN app (or Expo with a dev-client/prebuild — *not* Expo Go) is required. +Verified in CI / on any machine (no mobile toolchain needed): ```bash -# from mobile/rn/ -npx @react-native-community/cli@latest init OkenaMobile --version 0.76.5 -# RN 0.76 enables the new architecture by default. Confirm: -# android/gradle.properties → newArchEnabled=true -# ios: RCT_NEW_ARCH_ENABLED=1 (set by pod install on 0.76) +cd mobile/rn +npm ci +npm run typecheck # tsc --noEmit, strict +npm run lint # eslint +npm test # jest (packed-cell decoder smoke test) ``` -Then move/symlink the `src/` of this scaffold into the new app (or set the app's -`tsconfig`/Metro to resolve this package). The simplest path is to copy `src/`, -`tsconfig.json`, and the deps from `package.json` into the generated app. +**Not** verified here (needs the mobile toolchain + a device/emulator): the ubrn cross-compile, +the Skia native binaries, and an on-device run. Those are the steps below. -### 2. Install dependencies +> Package manager: **npm** (the lockfile is `package-lock.json`). RN 0.76 native autolinking, +> CocoaPods, and ubrn are validated against npm/yarn — don't swap in a different manager here. -```bash -npm install @shopify/react-native-skia@^1.5.0 -npm install # react, react-native already pinned by the init template -# iOS native pods: -( cd ios && pod install ) -``` - -### 3. Bundle the JetBrainsMono fonts +--- -The fonts already live at `../fonts/JetBrainsMono-{Regular,Bold,Italic,BoldItalic}.ttf`. -Load them in JS with Skia's `useFont` (preferred — keeps them out of native asset configs): +## Device-side setup (run on a machine with the RN toolchain) -```ts -import { useFont } from '@shopify/react-native-skia'; -import { TerminalTheme } from './src/theme'; +Prereqs: Node ≥ 18, Watchman, JDK 17, Android SDK + NDK + `cargo-ndk` (Android), Xcode + +CocoaPods (iOS), and the Rust mobile targets: -const regular = useFont(require('../fonts/JetBrainsMono-Regular.ttf'), TerminalTheme.defaultFontSize); -const bold = useFont(require('../fonts/JetBrainsMono-Bold.ttf'), TerminalTheme.defaultFontSize); -const italic = useFont(require('../fonts/JetBrainsMono-Italic.ttf'), TerminalTheme.defaultFontSize); -const boldItalic = useFont(require('../fonts/JetBrainsMono-BoldItalic.ttf'), TerminalTheme.defaultFontSize); -// render once all four are non-null: -// +```bash +rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android +rustup target add aarch64-apple-ios aarch64-apple-ios-sim +cargo install cargo-ndk ``` -(`TerminalView` re-sizes the fonts in place to its `fontSize` prop, so passing them at any -base size is fine.) The chrome UI's `.SF Pro` maps to RN's `System` font on iOS; Android -falls back to Roboto — see `src/theme.ts`. +### 1. Generate the native host projects (`android/`, `ios/`) -### 4. Generate the Rust↔TS bindings with `ubrn` - -The native module is generated from the sibling Rust crate by -[`uniffi-bindgen-react-native`](https://github.com/jhugman/uniffi-bindgen-react-native) (`ubrn`). +`@shopify/react-native-skia` and the ubrn TurboModule are Fabric/TurboModules, so a **bare** +RN app (new architecture ON — the RN 0.76 default) is required; Expo Go won't work. ```bash -# Install the generator (verify the current version during the Phase-0 spike): -npm install --save-dev uniffi-bindgen-react-native - -# A ubrn.config.yaml at the app root points at the Rust crate, e.g.: -# name: okena-mobile-ffi -# rust: -# directory: ../../../crates/okena-mobile-ffi # the uniffi-annotated crate -# manifestPath: Cargo.toml -# bindings: -# cppModuleName: okena_mobile_ffi -# outputDir: ./modules/okena-mobile-ffi -# android: { ... } # NDK targets + jniLibs wiring -# ios: { ... } # xcframework wiring - -# Cross-compile + generate the TS/C++/JNI glue: -npx ubrn build android --config ubrn.config.yaml --and-generate -npx ubrn build ios --config ubrn.config.yaml --and-generate -( cd ios && pod install ) # re-run pods to pick up the generated xcframework +# from a temp dir: generate a host with the SAME app name as app.json ("OkenaMobile") +npx @react-native-community/cli@latest init OkenaMobile --version 0.76.5 +# copy ONLY the generated native dirs into this project: +cp -R OkenaMobile/android OkenaMobile/ios ./ ``` -`ubrn` emits an installable JS package whose exported functions match `OkenaNative`. Wire -it into `src/native/okena.ts`: +The JS/config files in this repo (`index.js`, `app.json`, `metro.config.js`, …) already match +what the template produces, so you only need its `android/` and `ios/` directories (both are +gitignored here). Confirm new-arch is on: `android/gradle.properties → newArchEnabled=true`. -```ts -// src/native/okena.ts — replace the throwing getOkenaNative() body: -import * as gen from 'okena-mobile-ffi'; // ← the ubrn output package -export function getOkenaNative(): OkenaNative { - return gen as unknown as OkenaNative; // generated names already camelCase -} +### 2. Install JS deps + link fonts + +```bash +npm ci +npx react-native-asset # links assets/JetBrainsMono-*.ttf (react-native.config.js) ``` -> The `crates/okena-mobile-ffi` crate is owned by a separate agent (Phase 1 of the plan): -> it strips the `flutter_rust_bridge` attributes, adds `#[uniffi::export]`, ports all ~60 -> functions, and **adds `get_visible_cells_packed`**. Do not edit `mobile/native` for this. +The Skia renderer additionally loads the same ttf via `useFont(require('../../assets/...'))` +in `WorkspaceScreen.tsx`, so the fonts are both linked (for ``) and bundled. + +### 3. Generate the Rust↔TS bindings with ubrn -### 5. Run +`uniffi-bindgen-react-native` (`ubrn`) cross-compiles `crates/okena-mobile-ffi` and emits the +JSI TurboModule + TypeScript into `src/generated` (gitignored). Config: `ubrn.config.yaml`. ```bash -npx react-native run-android # device/emulator with the app installed -npx react-native run-ios # simulator +npm run ubrn:android # ubrn build android --config ubrn.config.yaml --and-generate --release +npm run ubrn:ios # ubrn build ios --config ubrn.config.yaml --and-generate --release +( cd ios && pod install ) # pick up the generated xcframework ``` -The first run does the Rust cross-compile via `ubrn`'s Gradle/CocoaPods integration (the -RN equivalent of what `cargokit` does for Flutter today). Ensure `$HOME/.cargo/bin` is on -the PATH the Gradle daemon sees, and use `rustls-tls` (not `native-tls`) Rust-side to avoid -cross-compiling OpenSSL for the NDK — same constraints as the Flutter build (`../CLAUDE.md`). +`getOkenaNative()` (`src/native/okena.ts`) already `require`s `../generated`, so once this +runs the app is wired — no code edit needed. ---- +> **Version pairing:** ubrn and uniffi minor versions must match. This repo pins +> `uniffi-bindgen-react-native@0.31.0-3` (devDependency) ↔ `uniffi = "0.31"` in +> `crates/okena-mobile-ffi/Cargo.toml`. If `ubrn` reports a metadata/contract-version +> mismatch, bump both together. + +> **NDK / TLS:** ensure `$HOME/.cargo/bin` is on the PATH the Gradle daemon sees. The crate +> already selects `rustls-tls` (via `okena-core`'s `client` feature), so no OpenSSL is +> cross-compiled for the NDK. -## Verifying just the TS (no device needed) +### 4. Run ```bash -cd mobile/rn -npm install # needs network for react / react-native / skia type packages -npm run typecheck # tsc --noEmit, strict +npm run android # device/emulator +npm run ios # simulator ``` +### 5. Phase-0 spikes (validate the two unknowns — see `../RN_MIGRATION.md` §3) + +- **S1 (toolchain):** confirm `initApp()` + `connect()` + `connectionStatus()` work end-to-end + through the ubrn module on a real Android device *and* iOS sim. +- **S2 (rendering):** confirm `react-native-skia` sustains the cell-grid paint at 60fps. + +--- + ## File map ``` mobile/rn/ -├── README.md # this file -├── package.json # RN 0.76, react 18.3, skia ^1.5, typescript -├── tsconfig.json # strict, react-jsx, bundler resolution +├── index.js · app.json # RN entry + app name +├── metro.config.js · babel.config.js # bundler + transpiler +├── react-native.config.js # font asset linking +├── ubrn.config.yaml # ubrn: crate path, targets, output dirs +├── jest.config.js · __tests__/ # jest (cells decoder smoke test) +├── .eslintrc.js · .prettierrc # lint + format +├── tsconfig.json · package.json +├── assets/JetBrainsMono-*.ttf # bundled monospace fonts └── src/ - ├── theme.ts # colors + typography (← app_theme.dart) + ├── App.tsx · theme.ts ├── native/ - │ ├── okena.ts # OkenaNative contract (← api/*.rs), getOkenaNative() shim - │ └── cells.ts # packed cell-buffer decoder + flag constants + ARGB helpers - └── components/ - └── TerminalView.tsx # Skia 3-pass renderer (← terminal_painter.dart + terminal_view.dart) + │ ├── okena.ts # OkenaNative contract + getOkenaNative() + │ └── cells.ts # packed cell-buffer decoder + ├── state/ # zustand stores (DI), persistence, navigation + ├── screens/ # ServerList, Pairing, Workspace + ├── components/ # TerminalView (Skia), KeyToolbar, LayoutRenderer, … + └── models/ # SavedServer, LayoutNode ``` diff --git a/mobile/rn/__tests__/cells.test.ts b/mobile/rn/__tests__/cells.test.ts new file mode 100644 index 00000000..0c6def03 --- /dev/null +++ b/mobile/rn/__tests__/cells.test.ts @@ -0,0 +1,84 @@ +/** + * Smoke test for the packed visible-cell decoder — the contract the Rust + * `get_visible_cells_packed` encoder (crates/okena-mobile-ffi) must satisfy. + */ +import { + decodeCells, + decodeCellsView, + argbToChannels, + argbToCss, + FLAG_BOLD, + HEADER_BYTES, + CELL_BYTES, +} from '../src/native/cells'; + +interface Cell { + codepoint: number; + fg: number; + bg: number; + flags: number; +} + +/** Build a packed buffer exactly as the Rust encoder does (LE throughout). */ +function packGrid(cols: number, rows: number, cells: Cell[]): ArrayBuffer { + const buf = new ArrayBuffer(HEADER_BYTES + cols * rows * CELL_BYTES); + const v = new DataView(buf); + v.setUint16(0, cols, true); + v.setUint16(2, rows, true); + let off = HEADER_BYTES; + for (const c of cells) { + v.setUint32(off + 0, c.codepoint, true); + v.setUint32(off + 4, c.fg, true); + v.setUint32(off + 8, c.bg, true); + v.setUint8(off + 12, c.flags); + off += CELL_BYTES; + } + return buf; +} + +const SAMPLE: Cell[] = [ + {codepoint: 0x41 /* 'A' */, fg: 0xff112233, bg: 0xff000000, flags: FLAG_BOLD}, + {codepoint: 0x20 /* ' ' */, fg: 0, bg: 0, flags: 0}, +]; + +describe('decodeCells', () => { + it('decodes header + cells round-trip from the packed format', () => { + const grid = decodeCells(packGrid(2, 1, SAMPLE)); + expect(grid.cols).toBe(2); + expect(grid.rows).toBe(1); + expect(grid.cells).toHaveLength(2); + expect(grid.cells[0]).toEqual({ + codepoint: 0x41, + fg: 0xff112233, + bg: 0xff000000, + flags: FLAG_BOLD, + }); + expect(grid.cells[1].codepoint).toBe(0x20); + }); + + it('throws on a truncated buffer', () => { + const short = packGrid(2, 1, SAMPLE).slice(0, 10); + expect(() => decodeCells(short)).toThrow(RangeError); + }); +}); + +describe('PackedCells (zero-alloc view)', () => { + it('exposes per-cell getters matching decodeCells', () => { + const view = decodeCellsView(packGrid(2, 1, SAMPLE)); + expect(view.count).toBe(2); + expect(view.codepoint(0)).toBe(0x41); + expect(view.char(0)).toBe('A'); + expect(view.flags(0) & FLAG_BOLD).toBe(FLAG_BOLD); + expect(view.isBlank(1)).toBe(true); + }); +}); + +describe('ARGB helpers', () => { + it('unpacks channels from 0xAARRGGBB', () => { + expect(argbToChannels(0xff112233)).toEqual({a: 255, r: 0x11, g: 0x22, b: 0x33}); + }); + + it('formats an rgba() string', () => { + expect(argbToCss(0xff112233)).toBe('rgba(17, 34, 51, 1.0000)'); + }); +}); diff --git a/mobile/rn/app.json b/mobile/rn/app.json new file mode 100644 index 00000000..3b784f6c --- /dev/null +++ b/mobile/rn/app.json @@ -0,0 +1,4 @@ +{ + "name": "OkenaMobile", + "displayName": "Okena" +} diff --git a/mobile/fonts/JetBrainsMono-Bold.ttf b/mobile/rn/assets/JetBrainsMono-Bold.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-Bold.ttf rename to mobile/rn/assets/JetBrainsMono-Bold.ttf diff --git a/mobile/fonts/JetBrainsMono-BoldItalic.ttf b/mobile/rn/assets/JetBrainsMono-BoldItalic.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-BoldItalic.ttf rename to mobile/rn/assets/JetBrainsMono-BoldItalic.ttf diff --git a/mobile/fonts/JetBrainsMono-Italic.ttf b/mobile/rn/assets/JetBrainsMono-Italic.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-Italic.ttf rename to mobile/rn/assets/JetBrainsMono-Italic.ttf diff --git a/mobile/fonts/JetBrainsMono-Regular.ttf b/mobile/rn/assets/JetBrainsMono-Regular.ttf similarity index 100% rename from mobile/fonts/JetBrainsMono-Regular.ttf rename to mobile/rn/assets/JetBrainsMono-Regular.ttf diff --git a/mobile/rn/babel.config.js b/mobile/rn/babel.config.js new file mode 100644 index 00000000..f7b3da3b --- /dev/null +++ b/mobile/rn/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], +}; diff --git a/mobile/rn/index.js b/mobile/rn/index.js new file mode 100644 index 00000000..deb22c84 --- /dev/null +++ b/mobile/rn/index.js @@ -0,0 +1,16 @@ +/** + * Okena mobile — React Native entry point. + * + * Registers the root component with the native host. The app name must match + * `app.json`'s `name` and the value the generated Android/iOS host projects + * pass to `ReactActivityDelegate` / `RCTRootView`. + * + * @format + */ + +import {AppRegistry} from 'react-native'; + +import App from './src/App'; +import {name as appName} from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/mobile/rn/jest.config.js b/mobile/rn/jest.config.js new file mode 100644 index 00000000..8eb675e9 --- /dev/null +++ b/mobile/rn/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'react-native', +}; diff --git a/mobile/rn/metro.config.js b/mobile/rn/metro.config.js new file mode 100644 index 00000000..ba95bf4a --- /dev/null +++ b/mobile/rn/metro.config.js @@ -0,0 +1,11 @@ +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); + +/** + * Metro configuration + * https://reactnative.dev/docs/metro + * + * @type {import('@react-native/metro-config').MetroConfig} + */ +const config = {}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/mobile/rn/package-lock.json b/mobile/rn/package-lock.json index 64daf0ca..50aca277 100644 --- a/mobile/rn/package-lock.json +++ b/mobile/rn/package-lock.json @@ -15,15 +15,28 @@ "zustand": "^4.5.5" }, "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native-community/cli-platform-ios": "15.0.1", + "@react-native/babel-preset": "0.76.5", + "@react-native/eslint-config": "0.76.5", + "@react-native/metro-config": "0.76.5", + "@react-native/typescript-config": "0.76.5", + "@types/jest": "^29.5.13", "@types/react": "^18.3.12", - "typescript": "^5.6.3" + "@types/react-test-renderer": "^18.3.0", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "18.3.1", + "typescript": "^5.6.3", + "uniffi-bindgen-react-native": "0.31.0-3" }, "engines": { "node": ">=18" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" } }, "node_modules/@babel/code-frame": { @@ -88,6 +101,35 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/eslint-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.29.7.tgz", + "integrity": "sha512-zxt+UJTOMKvUt3yOg+D58MLuz334pHp93qifMFcjIIO+9hN6t+ufw2gi7vDPMpxvfnHRR+3VVXvIjineCcgyXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", @@ -407,7 +449,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" @@ -424,7 +465,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -440,7 +480,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -456,7 +495,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" @@ -473,7 +511,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", @@ -491,7 +528,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" @@ -575,7 +611,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" }, @@ -681,7 +716,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -868,7 +902,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -934,7 +967,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -981,7 +1013,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1050,7 +1081,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1067,7 +1097,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1083,7 +1112,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1100,7 +1128,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1116,7 +1143,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7" @@ -1133,7 +1159,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1149,7 +1174,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1214,7 +1238,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1260,7 +1283,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1276,7 +1298,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1309,7 +1330,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", @@ -1328,7 +1348,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1361,7 +1380,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1426,7 +1444,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7" @@ -1522,7 +1539,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1617,7 +1633,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1634,7 +1649,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1738,7 +1752,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1754,7 +1767,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1789,7 +1801,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, @@ -1805,7 +1816,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1838,7 +1848,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" @@ -1855,7 +1864,6 @@ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", @@ -1941,7 +1949,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -1968,7 +1975,6 @@ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -2089,6 +2095,201 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -2123,6 +2324,72 @@ "node": ">=8" } }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, "node_modules/@jest/create-cache-key-function": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", @@ -2150,6 +2417,33 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jest/fake-timers": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", @@ -2167,65 +2461,199 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform": { + "node_modules/@jest/reporters": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", + "@types/node": "*", "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "jest-worker": "^29.7.0", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jest/types": { + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/schemas": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2277,1855 +2705,5661 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@react-native-async-storage/async-storage": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", - "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, "license": "MIT", "dependencies": { - "merge-options": "^3.0.4" + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, - "peerDependencies": { - "react-native": "^0.0.0-0 || >=0.65 <1.0" + "engines": { + "node": ">= 8" } }, - "node_modules/@react-native/assets-registry": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", - "integrity": "sha512-MN5dasWo37MirVcKWuysRkRr4BjNc81SXwUtJYstwbn8oEkfnwR9DaqdDTo/hHOnTdhafffLIa2xOOHcjDIGEw==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.5.tgz", - "integrity": "sha512-xe7HSQGop4bnOLMaXt0aU+rIatMNEQbz242SDl8V9vx5oOTI0VbZV9yLy6yBc6poUlYbcboF20YVjoRsxX4yww==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { - "@react-native/codegen": "0.76.5" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@react-native/babel-preset": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.5.tgz", - "integrity": "sha512-1Nu5Um4EogOdppBLI4pfupkteTjWfmI0hqW8ezWTg7Bezw0FtBj8yS8UYVd3wTnDFT9A5mA2VNoNUqomJnvj2A==", + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", "license": "MIT", "dependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.25.1", - "@babel/plugin-transform-literals": "^7.25.2", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.25.2", - "@babel/plugin-transform-react-jsx-self": "^7.24.7", - "@babel/plugin-transform-react-jsx-source": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.76.5", - "babel-plugin-syntax-hermes-parser": "^0.25.1", - "babel-plugin-transform-flow-enums": "^0.0.2", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": ">=18" + "merge-options": "^3.0.4" }, "peerDependencies": { - "@babel/core": "*" + "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, - "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", - "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "node_modules/@react-native-community/cli": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz", + "integrity": "sha512-xIGPytx2bj5HxFk0c7S25AVuJowHmEFg5LFC9XosKc0TSOjP1r6zGC6OqC/arQV/pNuqmZN2IFnpgJn0Bn+hhQ==", + "dev": true, "license": "MIT", "dependencies": { - "hermes-parser": "0.25.1" + "@react-native-community/cli-clean": "15.0.1", + "@react-native-community/cli-config": "15.0.1", + "@react-native-community/cli-debugger-ui": "15.0.1", + "@react-native-community/cli-doctor": "15.0.1", + "@react-native-community/cli-server-api": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "@react-native-community/cli-types": "15.0.1", + "chalk": "^4.1.2", + "commander": "^9.4.1", + "deepmerge": "^4.3.0", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.1.3", + "prompts": "^2.4.2", + "semver": "^7.5.2" + }, + "bin": { + "rnc-cli": "build/bin.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "license": "MIT" + "node_modules/@react-native-community/cli-clean": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-15.0.1.tgz", + "integrity": "sha512-flGTfT005UZvW2LAXVowZ/7ri22oiiZE4pPgMvc8klRxO5uofKIRuohgiHybHtiCo/HNqIz45JmZJvuFrhc4Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } }, - "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "node_modules/@react-native-community/cli-config": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-15.0.1.tgz", + "integrity": "sha512-SL3/9zIyzQQPKWei0+W1gNHxCPurrxqpODUWnVLoP38DNcvYCGtsRayw/4DsXgprZfBC+FsscNpd3IDJrG59XA==", + "dev": true, "license": "MIT", "dependencies": { - "hermes-estree": "0.25.1" + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "cosmiconfig": "^9.0.0", + "deepmerge": "^4.3.0", + "fast-glob": "^3.3.2", + "joi": "^17.2.1" } }, - "node_modules/@react-native/codegen": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.5.tgz", - "integrity": "sha512-FoZ9VRQ5MpgtDAnVo1rT9nNRfjnWpE40o1GeJSDlpUMttd36bVXvsDm8W/NhX8BKTWXSX+CPQJsRcvN1UPYGKg==", + "node_modules/@react-native-community/cli-config-apple": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-15.0.1.tgz", + "integrity": "sha512-GEHUx4NRp9W9or6vygn0TgNeFkcJdNjrtko0vQEJAS4gJdWqP/9LqqwJNlUfaW5jHBN7TKALAMlfRmI12Op3sg==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "glob": "^7.1.1", - "hermes-parser": "0.23.1", - "invariant": "^2.2.4", - "jscodeshift": "^0.14.0", - "mkdirp": "^0.5.1", - "nullthrows": "^1.1.1", - "yargs": "^17.6.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@babel/preset-env": "^7.1.6" + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" } }, - "node_modules/@react-native/community-cli-plugin": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.5.tgz", - "integrity": "sha512-3MKMnlU0cZOWlMhz5UG6WqACJiWUrE3XwBEumzbMmZw3Iw3h+fIsn+7kLLE5EhzqLt0hg5Y4cgYFi4kOaNgq+g==", + "node_modules/@react-native-community/cli-config/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@react-native-community/cli-config/node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.76.5", - "@react-native/metro-babel-transformer": "0.76.5", - "chalk": "^4.0.0", - "execa": "^5.1.1", - "invariant": "^2.2.4", - "metro": "^0.81.0", - "metro-config": "^0.81.0", - "metro-core": "^0.81.0", - "node-fetch": "^2.2.0", - "readline": "^1.3.0", - "semver": "^7.1.3" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" }, "engines": { - "node": ">=18" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "@react-native-community/cli-server-api": "*" + "typescript": ">=4.9.5" }, "peerDependenciesMeta": { - "@react-native-community/cli-server-api": { + "typescript": { "optional": true } } }, - "node_modules/@react-native/debugger-frontend": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.5.tgz", - "integrity": "sha512-5gtsLfBaSoa9WP8ToDb/8NnDBLZjv4sybQQj7rDKytKOdsXm3Pr2y4D7x7GQQtP1ZQRqzU0X0OZrhRz9xNnOqA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-native/dev-middleware": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.5.tgz", - "integrity": "sha512-f8eimsxpkvMgJia7POKoUu9uqjGF6KgkxX4zqr/a6eoR1qdEAWUd6PonSAqtag3PAqvEaJpB99gLH2ZJI1nDGg==", + "node_modules/@react-native-community/cli-config/node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.76.5", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.2.0", - "connect": "^3.6.5", - "debug": "^2.2.0", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "selfsigned": "^2.4.1", - "serve-static": "^1.13.1", - "ws": "^6.2.3" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native/dev-middleware/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@react-native-community/cli-config/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { - "ms": "2.0.0" + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@react-native/dev-middleware/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.76.5.tgz", - "integrity": "sha512-7KSyD0g0KhbngITduC8OABn0MAlJfwjIdze7nA4Oe1q3R7qmAv+wQzW+UEXvPah8m1WqFjYTkQwz/4mK3XrQGw==", + "node_modules/@react-native-community/cli-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, "engines": { - "node": ">=18" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native/js-polyfills": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.76.5.tgz", - "integrity": "sha512-ggM8tcKTcaqyKQcXMIvcB0vVfqr9ZRhWVxWIdiFO1mPvJyS6n+a+lLGkgQAyO8pfH0R1qw6K9D0nqbbDo865WQ==", + "node_modules/@react-native-community/cli-config/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=4" } }, - "node_modules/@react-native/metro-babel-transformer": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.5.tgz", - "integrity": "sha512-Cm9G5Sg5BDty3/MKa3vbCAJtT3YHhlEaPlQALLykju7qBS+pHZV9bE9hocfyyvc5N/osTIGWxG5YOfqTeMu1oQ==", + "node_modules/@react-native-community/cli-debugger-ui": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-15.0.1.tgz", + "integrity": "sha512-xkT2TLS8zg5r7Vl9l/2f7JVUoFECnVBS+B5ivrSu2PNZhKkr9lRmJFxC9aVLFb5lIxQQKNDvEyiIDNfP7wjJiA==", + "devOptional": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.76.5", - "hermes-parser": "0.23.1", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@babel/core": "*" + "serve-static": "^1.13.1" } }, - "node_modules/@react-native/normalize-colors": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.5.tgz", - "integrity": "sha512-6QRLEok1r55gLqj+94mEWUENuU5A6wsr2OoXpyq/CgQ7THWowbHtru/kRGRr6o3AQXrVnZheR60JNgFcpNYIug==", - "license": "MIT" - }, - "node_modules/@react-native/virtualized-lists": { - "version": "0.76.5", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.76.5.tgz", - "integrity": "sha512-M/fW1fTwxrHbcx0OiVOIxzG6rKC0j9cR9Csf80o77y1Xry0yrNPpAlf8D1ev3LvHsiAUiRNFlauoPtodrs2J1A==", + "node_modules/@react-native-community/cli-doctor": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-15.0.1.tgz", + "integrity": "sha512-YCu44lZR3zZxJJYVTqYZFz9cT9KBfbKI4q2MnKOvkamt00XY3usooMqfuwBAdvM/yvpx7M5w8kbM/nPyj4YCvQ==", + "dev": true, "license": "MIT", "dependencies": { - "invariant": "^2.2.4", - "nullthrows": "^1.1.1" - }, + "@react-native-community/cli-config": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native-community/cli-platform-apple": "15.0.1", + "@react-native-community/cli-platform-ios": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "command-exists": "^1.2.8", + "deepmerge": "^4.3.0", + "envinfo": "^7.13.0", + "execa": "^5.0.0", + "node-stream-zip": "^1.9.1", + "ora": "^5.4.1", + "semver": "^7.5.2", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1", + "yaml": "^2.2.1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/react": "^18.2.6", - "react": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=6" } }, - "node_modules/@shopify/react-native-skia": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-1.12.4.tgz", - "integrity": "sha512-8QDIBKSU7XB3Lc1kAv4jSFddTQK8AE+1AEoJnQLNllsiex1gufLQ8kN7rs9zii+iboSY8tYKT7ocV+5cE2Exdw==", + "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "license": "MIT", "dependencies": { - "canvaskit-wasm": "0.40.0", - "react-reconciler": "0.27.0" - }, - "bin": { - "setup-skia-web": "scripts/setup-canvaskit.js" - }, - "peerDependencies": { - "react": ">=18.0 <19.0.0", - "react-native": ">=0.64 <0.78.0", - "react-native-reanimated": ">=2.0.0" + "ansi-regex": "^4.1.0" }, - "peerDependenciesMeta": { - "react-native": { - "optional": true - }, - "react-native-reanimated": { - "optional": true - } + "engines": { + "node": ">=6" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "license": "BSD-3-Clause", + "node_modules/@react-native-community/cli-platform-android": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-15.0.1.tgz", + "integrity": "sha512-QlAMomj6H6TY6pHwjTYMsHDQLP5eLzjAmyW1qb03w/kyS/72elK2bjsklNWJrscFY9TMQLqw7qoAsXf1m5t/dg==", + "dev": true, + "license": "MIT", "dependencies": { - "type-detect": "4.0.8" + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.4.1", + "logkitty": "^0.7.1" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "license": "BSD-3-Clause", + "node_modules/@react-native-community/cli-platform-apple": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-15.0.1.tgz", + "integrity": "sha512-iQj1Dt2fr/Q7X2CQhyhWnece3eLDCark1osfiwpViksOfTH2WdpNS3lIwlFcIKhsieFU7YYwbNuFqQ3tF9Dlvw==", + "dev": true, + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@react-native-community/cli-config-apple": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-xml-parser": "^4.4.1" } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@react-native-community/cli-platform-ios": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-15.0.1.tgz", + "integrity": "sha512-6pKzXEIgGL20eE1uOn8iSsNBlMzO1LG+pQOk+7mvD172EPhKm/lRzUVDX5gO/2jvsGoNw6VUW0JX1FI2firwqA==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "@react-native-community/cli-platform-apple": "15.0.1" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/@react-native-community/cli-server-api": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-15.0.1.tgz", + "integrity": "sha512-f3rb3t1ELLaMSX5/LWO/IykglBIgiP3+pPnyl8GphHnBpf3bdIcp7fHlHLemvHE06YxT2nANRxRPjy1gNskenA==", + "devOptional": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.0.0" + "@react-native-community/cli-debugger-ui": "15.0.1", + "@react-native-community/cli-tools": "15.0.1", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "pretty-format": "^26.6.2", + "serve-static": "^1.13.1", + "ws": "^6.2.3" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@react-native-community/cli-server-api/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "devOptional": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "node_modules/@react-native-community/cli-server-api/node_modules/@types/yargs": { + "version": "15.0.20", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz", + "integrity": "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==", + "devOptional": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@types/yargs-parser": "*" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "node_modules/@react-native-community/cli-server-api/node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "devOptional": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" } }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "node_modules/@react-native-community/cli-server-api/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "devOptional": true, "license": "MIT" }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "node_modules/@react-native-community/cli-tools": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-15.0.1.tgz", + "integrity": "sha512-N79A+u/94roanfmNohVcNGu6Xg+0idh63JHZFLC9OJJuZwTifGMLDfSTHZATpR1J7rebozQ5ClcSUePavErnSg==", + "devOptional": true, "license": "MIT", "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "mime": "^2.4.1", + "open": "^6.2.0", + "ora": "^5.4.1", + "prompts": "^2.4.2", + "semver": "^7.5.2", + "shell-quote": "^1.7.3", + "sudo-prompt": "^9.0.0" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "devOptional": true, "license": "MIT", "dependencies": { - "@types/istanbul-lib-report": "*" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/node": { - "version": "25.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", - "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "node_modules/@react-native-community/cli-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "devOptional": true, "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "engines": { + "node": ">=4" } }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "node_modules/@react-native-community/cli-tools/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "devOptional": true, "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "license": "MIT" - }, - "node_modules/@webgpu/types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz", - "integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==", - "license": "BSD-3-Clause" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "node": ">=10" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@react-native-community/cli-tools/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "devOptional": true, "license": "MIT", "bin": { - "acorn": "bin/acorn" + "mime": "cli.js" }, "engines": { - "node": ">=0.4.0" + "node": ">=4.0.0" } }, - "node_modules/anser": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", - "license": "MIT" - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@react-native-community/cli-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, "engines": { "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@react-native-community/cli-tools/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", + "node_modules/@react-native-community/cli-tools/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "devOptional": true, + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "p-limit": "^3.0.2" }, "engines": { - "node": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@react-native-community/cli-types": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-15.0.1.tgz", + "integrity": "sha512-sWiJ62kkGu2mgYni2dsPxOMBzpwTjNsDH1ubY4mqcNEI9Zmzs0vRwwDUEhYqwNGys9+KpBKoZRrT2PAlhO84xA==", + "dev": true, "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "joi": "^17.2.1" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" - }, - "node_modules/ast-types": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", - "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "node_modules/@react-native-community/cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, "engines": { - "node": ">=4" - } - }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "license": "MIT" - }, - "node_modules/babel-core": { - "version": "7.0.0-bridge.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", - "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "license": "MIT", - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "^12.20.0 || >=14" } }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "node_modules/@react-native-community/cli/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.8.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "license": "BSD-3-Clause", + "node_modules/@react-native-community/cli/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "node_modules/@react-native-community/cli/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" + "yocto-queue": "^0.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", - "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "node_modules/@react-native-community/cli/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.8", - "semver": "^6.3.1" + "p-limit": "^3.0.2" }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@react-native/assets-registry": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", + "integrity": "sha512-MN5dasWo37MirVcKWuysRkRr4BjNc81SXwUtJYstwbn8oEkfnwR9DaqdDTo/hHOnTdhafffLIa2xOOHcjDIGEw==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", - "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.5.tgz", + "integrity": "sha512-xe7HSQGop4bnOLMaXt0aU+rIatMNEQbz242SDl8V9vx5oOTI0VbZV9yLy6yBc6poUlYbcboF20YVjoRsxX4yww==", "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", - "core-js-compat": "^3.48.0" + "@react-native/codegen": "0.76.5" }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=18" } }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", - "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "node_modules/@react-native/babel-preset": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.5.tgz", + "integrity": "sha512-1Nu5Um4EogOdppBLI4pfupkteTjWfmI0hqW8ezWTg7Bezw0FtBj8yS8UYVd3wTnDFT9A5mA2VNoNUqomJnvj2A==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.76.5", + "babel-plugin-syntax-hermes-parser": "^0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "@babel/core": "*" } }, - "node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz", - "integrity": "sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA==", + "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", "license": "MIT", "dependencies": { - "hermes-parser": "0.23.1" + "hermes-parser": "0.25.1" } }, - "node_modules/babel-plugin-transform-flow-enums": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", - "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "license": "MIT", "dependencies": { - "@babel/plugin-syntax-flow": "^7.12.1" + "hermes-estree": "0.25.1" } }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "node_modules/@react-native/codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.5.tgz", + "integrity": "sha512-FoZ9VRQ5MpgtDAnVo1rT9nNRfjnWpE40o1GeJSDlpUMttd36bVXvsDm8W/NhX8BKTWXSX+CPQJsRcvN1UPYGKg==", "license": "MIT", "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.23.1", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" + "@babel/preset-env": "^7.1.6" } }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "node_modules/@react-native/community-cli-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.5.tgz", + "integrity": "sha512-3MKMnlU0cZOWlMhz5UG6WqACJiWUrE3XwBEumzbMmZw3Iw3h+fIsn+7kLLE5EhzqLt0hg5Y4cgYFi4kOaNgq+g==", "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" + "@react-native/dev-middleware": "0.76.5", + "@react-native/metro-babel-transformer": "0.76.5", + "chalk": "^4.0.0", + "execa": "^5.1.1", + "invariant": "^2.2.4", + "metro": "^0.81.0", + "metro-config": "^0.81.0", + "metro-core": "^0.81.0", + "node-fetch": "^2.2.0", + "readline": "^1.3.0", + "semver": "^7.1.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@react-native-community/cli-server-api": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli-server-api": { + "optional": true + } } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@react-native/debugger-frontend": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.5.tgz", + "integrity": "sha512-5gtsLfBaSoa9WP8ToDb/8NnDBLZjv4sybQQj7rDKytKOdsXm3Pr2y4D7x7GQQtP1ZQRqzU0X0OZrhRz9xNnOqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.5.tgz", + "integrity": "sha512-f8eimsxpkvMgJia7POKoUu9uqjGF6KgkxX4zqr/a6eoR1qdEAWUd6PonSAqtag3PAqvEaJpB99gLH2ZJI1nDGg==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.76.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/@react-native/eslint-config": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.76.5.tgz", + "integrity": "sha512-FnzjnwuWrpuJaBfjLMEPtGe6dy3d2Mc3cnoOGF5ghDbpHP2JUHp1GoKRZdZpJlGXJyQTi8wULpyKK6v8jM0dOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/eslint-parser": "^7.25.1", + "@react-native/eslint-plugin": "0.76.5", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-ft-flow": "^2.0.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": ">=8", + "prettier": ">=2" + } + }, + "node_modules/@react-native/eslint-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/eslint-plugin/-/eslint-plugin-0.76.5.tgz", + "integrity": "sha512-yAd3349bvWXlegStk6o/lOofRVmr/uSLNdAEsFXw18OlxjnBSx9U3teJtQNA91DfquQAcmSgf1lIBv+MUJ+fnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.76.5.tgz", + "integrity": "sha512-7KSyD0g0KhbngITduC8OABn0MAlJfwjIdze7nA4Oe1q3R7qmAv+wQzW+UEXvPah8m1WqFjYTkQwz/4mK3XrQGw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.76.5.tgz", + "integrity": "sha512-ggM8tcKTcaqyKQcXMIvcB0vVfqr9ZRhWVxWIdiFO1mPvJyS6n+a+lLGkgQAyO8pfH0R1qw6K9D0nqbbDo865WQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.5.tgz", + "integrity": "sha512-Cm9G5Sg5BDty3/MKa3vbCAJtT3YHhlEaPlQALLykju7qBS+pHZV9bE9hocfyyvc5N/osTIGWxG5YOfqTeMu1oQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.76.5", + "hermes-parser": "0.23.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.76.5.tgz", + "integrity": "sha512-+bklxpRj1BAFzAwOI29MjdddwlC6wTJYlnMK9a77GiowELNeRO4R8UD1dRepOoSVpPFfFlLbFiqYQXqBrbl1pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native/js-polyfills": "0.76.5", + "@react-native/metro-babel-transformer": "0.76.5", + "metro-config": "^0.81.0", + "metro-runtime": "^0.81.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.5.tgz", + "integrity": "sha512-6QRLEok1r55gLqj+94mEWUENuU5A6wsr2OoXpyq/CgQ7THWowbHtru/kRGRr6o3AQXrVnZheR60JNgFcpNYIug==", + "license": "MIT" + }, + "node_modules/@react-native/typescript-config": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/typescript-config/-/typescript-config-0.76.5.tgz", + "integrity": "sha512-dRbY4XQTUUxR5Oq+S+2/5JQVU6WL0qvNnAz51jiXllC+hp5L4bljSxlzaj5CJ9vzGNFzm56m5Y9Q6MltoIU4Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.76.5.tgz", + "integrity": "sha512-M/fW1fTwxrHbcx0OiVOIxzG6rKC0j9cR9Csf80o77y1Xry0yrNPpAlf8D1ev3LvHsiAUiRNFlauoPtodrs2J1A==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@shopify/react-native-skia": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-1.12.4.tgz", + "integrity": "sha512-8QDIBKSU7XB3Lc1kAv4jSFddTQK8AE+1AEoJnQLNllsiex1gufLQ8kN7rs9zii+iboSY8tYKT7ocV+5cE2Exdw==", + "license": "MIT", + "dependencies": { + "canvaskit-wasm": "0.40.0", + "react-reconciler": "0.27.0" + }, + "bin": { + "setup-skia-web": "scripts/setup-canvaskit.js" + }, + "peerDependencies": { + "react": ">=18.0 <19.0.0", + "react-native": ">=0.64 <0.78.0", + "react-native-reanimated": ">=2.0.0" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-test-renderer": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.1.tgz", + "integrity": "sha512-vAhnk0tG2eGa37lkU9+s5SoroCsRI08xnsWFiAXOuPH2jqzMbcXvKExXViPi1P5fIklDeCvXqyrdmipFaSkZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "^18" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webgpu/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz", + "integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==", + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-fragments": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", + "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^1.0.7", + "slice-ansi": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/ansi-fragments/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-fragments/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appdirsjs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", + "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz", + "integrity": "sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.23.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", + "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvaskit-wasm": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.40.0.tgz", + "integrity": "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==", + "license": "BSD-3-Clause", + "dependencies": { + "@webgpu/types": "0.1.21" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/errorhandler": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz", + "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-ft-flow": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-ft-flow/-/eslint-plugin-ft-flow-2.0.3.tgz", + "integrity": "sha512-Vbsd/b+LYA99jUbsL6viEUWShFaYQt2YQs3QN3f+aeszOhh2sgdcU0mjzDyD4yyBvMc8qy2uwvBBWfMzEX06tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "@babel/eslint-parser": "^7.12.0", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-native": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-4.1.0.tgz", + "integrity": "sha512-QLo7rzTBOl43FvVqDdq5Ql9IoElIuTdjrz9SKAXCvULvBoRZ44JGSkx9z4999ZusCsb4rK3gjS8gOGyeYqZv2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-react-native-globals": "^0.1.1" + }, + "peerDependencies": { + "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-native-globals": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", + "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", + "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT" + }, + "node_modules/flow-parser": { + "version": "0.317.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.317.0.tgz", + "integrity": "sha512-pzwkCzruTUg6f5HH7N0OvVjX7dVc361tnsEkSCbMC9cJ5zxZY84de8l+DraCmnGsIbi+jQPPxtTKfBJFnYCJZQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.34", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", - "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "dunder-proto": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "engines": { + "node": ">= 0.4" }, - "bin": { - "browserslist": "cli.js" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "license": "Apache-2.0", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { - "node-int64": "^0.4.0" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/hermes-estree": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.1.tgz", + "integrity": "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==", "license": "MIT" }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "node_modules/hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.1.tgz", + "integrity": "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==", "license": "MIT", "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" + "hermes-estree": "0.23.1" } }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "caller-callsite": "^2.0.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=4" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.8" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": ">=10.17.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001797", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", - "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + "type": "patreon", + "url": "https://www.patreon.com/feross" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "CC-BY-4.0" - }, - "node_modules/canvaskit-wasm": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.40.0.tgz", - "integrity": "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==", - "license": "BSD-3-Clause", - "dependencies": { - "@webgpu/types": "0.1.21" - } + "license": "BSD-3-Clause" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 4" } }, - "node_modules/chrome-launcher": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", - "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0" + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" }, "bin": { - "print-chrome-path": "bin/print-chrome-path.js" + "image-size": "bin/image-size.js" }, "engines": { - "node": ">=12.13.0" + "node": ">=16.x" } }, - "node_modules/chromium-edge-launcher": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", - "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", - "license": "Apache-2.0", + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/chromium-edge-launcher/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, "bin": { - "mkdirp": "bin/cmd.js" + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">=10" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" + "has-bigints": "^1.0.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1" + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "hasown": "^2.0.3" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.368", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", - "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" + "node": ">=0.10.0" } }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, "license": "MIT", "dependencies": { - "stackframe": "^1.3.4" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "license": "MIT", "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" + "isobject": "^3.0.1" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/flow-enums-runtime": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", - "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", - "license": "MIT" - }, - "node_modules/flow-parser": { - "version": "0.317.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.317.0.tgz", - "integrity": "sha512-pzwkCzruTUg6f5HH7N0OvVjX7dVc361tnsEkSCbMC9cJ5zxZY84de8l+DraCmnGsIbi+jQPPxtTKfBJFnYCJZQ==", + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { + "node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "call-bound": "^1.0.3" }, "engines": { - "node": "*" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "is-docker": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/hermes-estree": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.1.tgz", - "integrity": "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, "license": "MIT" }, - "node_modules/hermes-parser": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.1.tgz", - "integrity": "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.23.1" - } + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=0.10.0" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, "engines": { - "node": ">=10.17.0" + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=16.x" + "node": ">=10" } }, - "node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "license": "MIT", "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "license": "MIT", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=0.8.19" + "node": ">=0.10.0" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, "license": "MIT", "dependencies": { - "loose-envify": "^1.0.0" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.3" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", - "bin": { - "is-docker": "cli.js" + "dependencies": { + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, "engines": { - "node": ">=0.12.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/jest-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "license": "BSD-3-Clause", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "license": "BSD-3-Clause", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "detect-newline": "^3.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { @@ -4179,6 +8413,36 @@ "fsevents": "^2.3.2" } }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -4213,6 +8477,24 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, "node_modules/jest-regex-util": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", @@ -4222,6 +8504,177 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -4268,6 +8721,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", @@ -4298,6 +8771,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4385,12 +8872,40 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4403,13 +8918,59 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/leven": { @@ -4421,6 +8982,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -4446,6 +9021,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4458,18 +9040,135 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", + "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-fragments": "^0.2.1", + "dayjs": "^1.8.15", + "yargs": "^15.1.0" + }, + "bin": { + "logkitty": "bin/logkitty.js" + } + }, + "node_modules/logkitty/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/logkitty/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/logkitty/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4528,6 +9227,16 @@ "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "license": "Apache-2.0" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -4552,6 +9261,16 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/metro": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/metro/-/metro-0.81.5.tgz", @@ -5001,6 +9720,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -5016,6 +9742,16 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/nocache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", + "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -5028,6 +9764,35 @@ "node": ">= 0.10.5" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -5072,6 +9837,20 @@ "node": ">=18" } }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5111,6 +9890,114 @@ "node": ">=18.18" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -5123,6 +10010,16 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5163,6 +10060,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -5184,16 +10141,39 @@ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5254,6 +10234,16 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5348,6 +10338,42 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -5383,6 +10409,66 @@ "asap": "~2.0.6" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -5392,6 +10478,27 @@ "inherits": "~2.0.3" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5545,6 +10652,60 @@ "node": ">=0.10.0" } }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-test-renderer": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz", + "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-is": "^18.3.1", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-test-renderer/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readline": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", @@ -5575,6 +10736,29 @@ "node": ">=0.10.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -5599,6 +10783,27 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpu-core": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", @@ -5643,19 +10848,181 @@ "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "isarray": "^2.0.5" }, "engines": { "node": ">= 0.4" @@ -5664,29 +11031,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", "dependencies": { - "glob": "^7.1.3" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/scheduler": { @@ -5825,6 +11185,62 @@ "node": ">= 0.8" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5876,12 +11292,95 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5891,6 +11390,61 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5970,21 +11524,165 @@ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.6" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/strip-ansi": { @@ -5999,6 +11697,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -6008,6 +11716,40 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "devOptional": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6095,6 +11837,13 @@ "node": ">=8" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -6134,12 +11883,61 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6158,6 +11956,84 @@ "node": ">=8" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6172,6 +12048,25 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -6218,6 +12113,27 @@ "node": ">=4" } }, + "node_modules/uniffi-bindgen-react-native": { + "version": "0.31.0-3", + "resolved": "https://registry.npmjs.org/uniffi-bindgen-react-native/-/uniffi-bindgen-react-native-0.31.0-3.tgz", + "integrity": "sha512-br7giBJRr/j00rdMXhOZGMGuDWMAVUcxC9O3lco0KRqzEAxUz00+gegPW5tKamvKbrnj7NB682XUCYrcagtxFA==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "ubrn": "bin/cli.cjs", + "uniffi-bindgen-react-native": "bin/cli.cjs" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6257,6 +12173,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -6266,6 +12192,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -6275,6 +12208,31 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -6290,6 +12248,16 @@ "makeerror": "1.0.12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -6327,6 +12295,112 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -6387,6 +12461,22 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -6414,6 +12504,19 @@ "node": ">=12" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/mobile/rn/package.json b/mobile/rn/package.json index a2f73bba..3de0e3f4 100644 --- a/mobile/rn/package.json +++ b/mobile/rn/package.json @@ -2,10 +2,23 @@ "name": "okena-mobile-rn", "version": "0.0.0", "private": true, - "description": "Okena mobile client — React Native UI over the Rust core (uniffi/ubrn). New architecture, native Skia terminal renderer. SCAFFOLD: not yet wired to a full RN host project — see README.md.", + "description": "Okena mobile client — React Native UI over the Rust core (uniffi/ubrn). New architecture, native Skia terminal renderer. The native host projects (android/, ios/) are generated by `react-native init` + merge — see README.md.", + "repository": { + "type": "git", + "url": "git+https://github.com/contember/okena.git", + "directory": "mobile/rn" + }, "scripts": { + "start": "react-native start", + "android": "react-native run-android", + "ios": "react-native run-ios", "typecheck": "tsc --noEmit", - "lint": "eslint src --ext .ts,.tsx" + "lint": "eslint src --ext .ts,.tsx", + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "test": "jest", + "ubrn:android": "ubrn build android --config ubrn.config.yaml --and-generate --release", + "ubrn:ios": "ubrn build ios --config ubrn.config.yaml --and-generate --release", + "ubrn:checkout": "ubrn checkout --config ubrn.config.yaml" }, "dependencies": { "@react-native-async-storage/async-storage": "^2.1.0", @@ -15,12 +28,25 @@ "zustand": "^4.5.5" }, "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native-community/cli-platform-ios": "15.0.1", + "@react-native/babel-preset": "0.76.5", + "@react-native/eslint-config": "0.76.5", + "@react-native/metro-config": "0.76.5", + "@react-native/typescript-config": "0.76.5", + "@types/jest": "^29.5.13", "@types/react": "^18.3.12", - "typescript": "^5.6.3" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" + "@types/react-test-renderer": "^18.3.0", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "18.3.1", + "typescript": "^5.6.3", + "uniffi-bindgen-react-native": "0.31.0-3" }, "engines": { "node": ">=18" diff --git a/mobile/rn/react-native.config.js b/mobile/rn/react-native.config.js new file mode 100644 index 00000000..b356ead9 --- /dev/null +++ b/mobile/rn/react-native.config.js @@ -0,0 +1,15 @@ +/** + * React Native CLI config. + * + * `assets` registers the bundled JetBrainsMono ttf files with the native build + * so `fontFamily: 'JetBrainsMono'` resolves in `` styles. The Skia + * terminal renderer loads the same files directly via `useFont(require(...))` + * (see WorkspaceScreen.tsx), so they are needed both linked and bundled. + */ +module.exports = { + project: { + ios: {}, + android: {}, + }, + assets: ['./assets'], +}; diff --git a/mobile/rn/src/components/StatusIndicator.tsx b/mobile/rn/src/components/StatusIndicator.tsx index d13f5d90..c78d9f89 100644 --- a/mobile/rn/src/components/StatusIndicator.tsx +++ b/mobile/rn/src/components/StatusIndicator.tsx @@ -86,13 +86,7 @@ export const StatusIndicator: React.FC<{ status: ConnectionStatus }> = ({ status style={[ styles.dot, { backgroundColor: color, opacity: pulse }, - isConnected && { - shadowColor: color, - shadowOpacity: 0.5, - shadowRadius: 6, - shadowOffset: { width: 0, height: 0 }, - elevation: 4, - }, + isConnected && [styles.glow, { shadowColor: color }], ]} /> = ({ const resizeTimer = useRef | null>(null); const initialResizeSent = useRef(false); - // Resize the font in-place so metrics match the requested size. - useMemo(() => { + // Resize the fonts in-place so metrics match the requested size, then measure + // a cell. Kept as one memo so `measureCell` always runs after `setSize` and + // `fontSize` is a real (visible-to-eslint) dependency — the Skia font is + // mutated in place, so the `fonts` reference alone wouldn't track size changes. + const metrics = useMemo(() => { fonts.regular.setSize(fontSize); fonts.bold?.setSize(fontSize); fonts.italic?.setSize(fontSize); fonts.boldItalic?.setSize(fontSize); + return measureCell(fonts.regular); }, [fonts, fontSize]); - const metrics = useMemo(() => measureCell(fonts.regular), [fonts, fontSize]); - // ── Layout → cols/rows → resizeLocal + debounced resizeTerminal ────────── const onLayout = useCallback( (e: LayoutChangeEvent) => { diff --git a/mobile/rn/src/models/layoutNode.ts b/mobile/rn/src/models/layoutNode.ts index 8e4efaa8..e7c10a6c 100644 --- a/mobile/rn/src/models/layoutNode.ts +++ b/mobile/rn/src/models/layoutNode.ts @@ -58,35 +58,35 @@ export type LayoutNode = TerminalNode | SplitNode | TabsNode; * `whereType()` behavior: children that fail to parse are dropped. */ function parseNode(map: Record): LayoutNode | null { - const type = map['type']; + const type = map.type; switch (type) { case 'terminal': return { type: 'terminal', terminalId: - typeof map['terminal_id'] === 'string' - ? (map['terminal_id'] as string) + typeof map.terminal_id === 'string' + ? (map.terminal_id as string) : undefined, - minimized: map['minimized'] === true, - detached: map['detached'] === true, + minimized: map.minimized === true, + detached: map.detached === true, }; case 'split': { - const children = parseChildren(map['children']); - const rawSizes = map['sizes']; + const children = parseChildren(map.children); + const rawSizes = map.sizes; const sizes = Array.isArray(rawSizes) ? rawSizes.filter((s): s is number => typeof s === 'number') : []; return { type: 'split', - direction: map['direction'] === 'vertical' ? 'vertical' : 'horizontal', + direction: map.direction === 'vertical' ? 'vertical' : 'horizontal', sizes, children, }; } case 'tabs': { - const children = parseChildren(map['children']); + const children = parseChildren(map.children); const activeTab = - typeof map['active_tab'] === 'number' ? (map['active_tab'] as number) : 0; + typeof map.active_tab === 'number' ? (map.active_tab as number) : 0; return { type: 'tabs', activeTab, children }; } default: diff --git a/mobile/rn/src/native/cells.ts b/mobile/rn/src/native/cells.ts index d8ae5813..32fa2603 100644 --- a/mobile/rn/src/native/cells.ts +++ b/mobile/rn/src/native/cells.ts @@ -1,11 +1,10 @@ /** * cells.ts — decoder for the packed visible-cell buffer. * - * SCAFFOLD NOTE: this matches a binary format produced by a Rust FFI function - * (`getVisibleCellsPacked`) that is being ADDED to `crates/okena-mobile-ffi` - * as part of the migration (it does not exist in `mobile/native` yet). This - * file is the authoritative spec of that wire format; the Rust encoder must - * produce exactly these bytes. + * This matches the binary format produced by the Rust FFI function + * `get_visible_cells_packed` in `crates/okena-mobile-ffi/src/lib.rs`. This file + * is the authoritative spec of that wire format; the Rust encoder must produce + * exactly these bytes. * * Format (little-endian throughout): * diff --git a/mobile/rn/src/native/okena.ts b/mobile/rn/src/native/okena.ts index 44c25f8e..5f33736c 100644 --- a/mobile/rn/src/native/okena.ts +++ b/mobile/rn/src/native/okena.ts @@ -16,7 +16,8 @@ * │ becomes the typed re-export / shim over the generated module). │ * └───────────────────────────────────────────────────────────────────────┘ * - * Signature source of truth: `mobile/native/src/api/{connection,terminal,state}.rs`. + * Signature source of truth: `crates/okena-mobile-ffi/src/lib.rs` (the + * `#[uniffi::export]` surface). * * Sync vs. async mapping (mirrors the Rust `#[frb(sync)]` / `async fn` split, * which `ubrn` maps to synchronous JSI host functions vs. JS Promises): @@ -248,9 +249,8 @@ export interface OkenaNative { * of 13 bytes each (`codepoint:u32, fg:u32, bg:u32, flags:u8`). Decode with * `decodeCells` from `./cells`. * - * NOTE: This function does NOT yet exist in `mobile/native/src/api/*.rs`; it - * is being ADDED Rust-side as part of the migration (`crates/okena-mobile-ffi`, - * Phase 1). The exact byte format here is the contract for that addition. + * Implemented Rust-side as `get_visible_cells_packed` in + * `crates/okena-mobile-ffi/src/lib.rs`; the byte format here is the contract. */ getVisibleCellsPacked(connId: ConnId, terminalId: TerminalId): ArrayBuffer; @@ -422,21 +422,31 @@ export interface OkenaNative { } /** - * Resolve the real native module once `ubrn` has generated it. + * Resolve the real native module generated by `ubrn` into `src/generated` + * (see `ubrn.config.yaml`). The lookup is a synchronous Metro `require` — the + * render hot path calls the native getters synchronously, so a dynamic + * `import()` (async) is not an option. * - * Replace the body with the generated package import, e.g.: + * The require is lazy (inside the function, not a top-level import) so that: + * - this typed contract and the presentational components compile/bundle + * before `npm run ubrn:*` has produced the module, and + * - tests / off-device code that inject a mock `OkenaNative` (via + * `configureConnectionStore` / `configureWorkspaceStore` / the `native` + * prop) never hit this path. * - * import * as gen from 'okena-mobile-ffi'; // ubrn output package name - * export const Okena: OkenaNative = gen as unknown as OkenaNative; - * - * Until then this throws, so the typed contract compiles and the - * presentational components (which accept the module via props) stay testable - * with a mock implementing `OkenaNative`. + * Until the module is generated, the `require` throws and we rethrow with a + * pointer to the generation step (see mobile/rn/README.md). */ export function getOkenaNative(): OkenaNative { - throw new Error( - 'okena native module not wired up yet — generate it with `ubrn` from ' + - 'crates/okena-mobile-ffi and replace getOkenaNative() in src/native/okena.ts. ' + - 'See mobile/rn/README.md.', - ); + try { + const gen = require('../generated'); + return (gen.default ?? gen) as OkenaNative; + } catch (e) { + throw new Error( + 'okena native module not generated yet — run `npm run ubrn:android` or ' + + '`npm run ubrn:ios` to cross-compile crates/okena-mobile-ffi and emit ' + + 'src/generated. See mobile/rn/README.md. Underlying error: ' + + String(e), + ); + } } diff --git a/mobile/rn/ubrn.config.yaml b/mobile/rn/ubrn.config.yaml new file mode 100644 index 00000000..cd83aeed --- /dev/null +++ b/mobile/rn/ubrn.config.yaml @@ -0,0 +1,43 @@ +# uniffi-bindgen-react-native (ubrn) configuration. +# +# Drives the cross-compile of the Rust FFI crate (crates/okena-mobile-ffi) and +# the generation of the JSI TurboModule + TypeScript bindings. Consumed by: +# npm run ubrn:android → ubrn build android --config ubrn.config.yaml --and-generate +# npm run ubrn:ios → ubrn build ios --config ubrn.config.yaml --and-generate +# +# Requires the mobile toolchain (Android NDK + cargo-ndk, or Xcode) — see +# README.md §"Generate the native module". Not run in CI. +# +# VERSION PAIRING: ubrn and uniffi minor versions must match. This repo uses +# `uniffi-bindgen-react-native` 0.31.0-3 (devDependencies) ↔ `uniffi = "0.31"` +# in crates/okena-mobile-ffi/Cargo.toml. Bump both together. + +# Native/JS module name. +name: okena_mobile_ffi + +# The Rust crate to build, relative to this file. +rust: + directory: ../../crates/okena-mobile-ffi + manifestPath: Cargo.toml + +# Where the generated glue lands (both dirs are gitignored — regenerated by +# `npm run ubrn:*`). The hand-written binding contract/shim stays in +# src/native/okena.ts and re-exports from src/generated. +bindings: + ts: src/generated + cpp: cpp/generated + +# Android: build for all four ABIs. `directory`/`jniLibs` use ubrn defaults +# (android/, android/app/src/main/jniLibs) once the native host project exists. +android: + targets: + - arm64-v8a + - armeabi-v7a + - x86_64 + - x86 + +# iOS: device + simulator (sim arch picked to match the build host). +ios: + targets: + - aarch64-apple-ios + - aarch64-apple-ios-sim From 85cd1885f5183ca77d841e7b5c84427c9ada41e7 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 10 Jun 2026 13:21:52 +0200 Subject: [PATCH 33/37] chore(mobile): remove the Flutter app; update workspace, CI, and docs Delete the retired Flutter client (lib, ios/android/linux shells, tests, cargokit, pubspec/flutter configs). Replace the Flutter build-mobile CI job with a mobile-rn job (npm typecheck + lint) and drop it from the release deps. Update the root README/CLAUDE, docs/mobile-status.md, .gitignore, and the RN_MIGRATION status to describe the React Native + uniffi stack. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build.yml | 114 +- .gitignore | 19 +- CLAUDE.md | 7 +- README.md | 4 +- docs/mobile-status.md | 335 +- mobile/.gitignore | 45 - mobile/.metadata | 30 - mobile/CLAUDE.md | 187 - mobile/README.md | 16 - mobile/RN_MIGRATION.md | 9 +- mobile/analysis_options.yaml | 28 - mobile/android/.gitignore | 14 - mobile/android/app/build.gradle.kts | 44 - .../android/app/src/debug/AndroidManifest.xml | 7 - .../android/app/src/main/AndroidManifest.xml | 47 - .../kotlin/com/example/mobile/MainActivity.kt | 5 - .../res/drawable-v21/launch_background.xml | 12 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 9540 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 5205 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 14911 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 28820 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 46942 -> 0 bytes .../app/src/main/res/values-night/styles.xml | 18 - .../app/src/main/res/values/styles.xml | 18 - .../app/src/profile/AndroidManifest.xml | 7 - mobile/android/build.gradle.kts | 24 - mobile/android/gradle.properties | 2 - .../gradle/wrapper/gradle-wrapper.properties | 5 - mobile/android/settings.gradle.kts | 26 - mobile/devtools_options.yaml | 3 - mobile/flutter_rust_bridge.yaml | 3 - mobile/integration_test/simple_test.dart | 14 - mobile/ios/.gitignore | 34 - mobile/ios/Flutter/AppFrameworkInfo.plist | 24 - mobile/ios/Flutter/Debug.xcconfig | 2 - mobile/ios/Flutter/Release.xcconfig | 2 - mobile/ios/Podfile | 43 - mobile/ios/Podfile.lock | 35 - mobile/ios/Runner.xcodeproj/project.pbxproj | 739 ---- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 101 - .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - mobile/ios/Runner/AppDelegate.swift | 16 - .../AppIcon.appiconset/Contents.json | 122 - .../Icon-App-1024x1024@1x.png | Bin 260461 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 1495 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 3956 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 7249 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 2484 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 6845 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 12834 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 3956 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 11230 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 21345 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 21345 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 42201 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 10373 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 31674 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 37065 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 - mobile/ios/Runner/Base.lproj/Main.storyboard | 26 - mobile/ios/Runner/Info.plist | 75 - mobile/ios/Runner/Runner-Bridging-Header.h | 1 - mobile/ios/RunnerTests/RunnerTests.swift | 12 - mobile/lib/main.dart | 164 - mobile/lib/src/models/layout_node.dart | 92 - mobile/lib/src/models/saved_server.dart | 56 - .../src/providers/connection_provider.dart | 160 - .../lib/src/providers/workspace_provider.dart | 200 - mobile/lib/src/rust/api/connection.dart | 53 - .../lib/src/rust/api/connection.freezed.dart | 386 -- mobile/lib/src/rust/api/state.dart | 567 --- mobile/lib/src/rust/api/terminal.dart | 268 -- mobile/lib/src/rust/frb_generated.dart | 3632 ----------------- mobile/lib/src/rust/frb_generated.io.dart | 495 --- mobile/lib/src/rust/frb_generated.web.dart | 495 --- mobile/lib/src/screens/pairing_screen.dart | 218 - .../lib/src/screens/server_list_screen.dart | 278 -- mobile/lib/src/screens/workspace_screen.dart | 1369 ------- mobile/lib/src/theme/app_theme.dart | 120 - mobile/lib/src/widgets/key_toolbar.dart | 744 ---- mobile/lib/src/widgets/layout_renderer.dart | 400 -- mobile/lib/src/widgets/project_drawer.dart | 991 ----- mobile/lib/src/widgets/status_indicator.dart | 120 - mobile/lib/src/widgets/terminal_painter.dart | 265 -- mobile/lib/src/widgets/terminal_view.dart | 619 --- mobile/linux/.gitignore | 1 - mobile/linux/CMakeLists.txt | 128 - mobile/linux/flutter/CMakeLists.txt | 88 - .../flutter/generated_plugin_registrant.cc | 11 - .../flutter/generated_plugin_registrant.h | 15 - mobile/linux/flutter/generated_plugins.cmake | 24 - mobile/linux/runner/CMakeLists.txt | 26 - mobile/linux/runner/main.cc | 6 - mobile/linux/runner/my_application.cc | 148 - mobile/linux/runner/my_application.h | 21 - mobile/pubspec.yaml | 96 - mobile/rust_builder/.gitignore | 29 - mobile/rust_builder/README.md | 1 - mobile/rust_builder/android/.gitignore | 9 - mobile/rust_builder/android/build.gradle | 56 - mobile/rust_builder/android/settings.gradle | 1 - .../android/src/main/AndroidManifest.xml | 3 - mobile/rust_builder/cargokit/.gitignore | 4 - mobile/rust_builder/cargokit/LICENSE | 42 - mobile/rust_builder/cargokit/README | 11 - mobile/rust_builder/cargokit/build_pod.sh | 58 - .../cargokit/build_tool/README.md | 5 - .../cargokit/build_tool/analysis_options.yaml | 34 - .../cargokit/build_tool/bin/build_tool.dart | 8 - .../cargokit/build_tool/lib/build_tool.dart | 8 - .../lib/src/android_environment.dart | 195 - .../lib/src/artifacts_provider.dart | 266 -- .../build_tool/lib/src/build_cmake.dart | 40 - .../build_tool/lib/src/build_gradle.dart | 49 - .../build_tool/lib/src/build_pod.dart | 89 - .../build_tool/lib/src/build_tool.dart | 271 -- .../cargokit/build_tool/lib/src/builder.dart | 198 - .../cargokit/build_tool/lib/src/cargo.dart | 48 - .../build_tool/lib/src/crate_hash.dart | 124 - .../build_tool/lib/src/environment.dart | 68 - .../cargokit/build_tool/lib/src/logging.dart | 52 - .../cargokit/build_tool/lib/src/options.dart | 309 -- .../lib/src/precompile_binaries.dart | 202 - .../cargokit/build_tool/lib/src/rustup.dart | 136 - .../cargokit/build_tool/lib/src/target.dart | 140 - .../cargokit/build_tool/lib/src/util.dart | 172 - .../build_tool/lib/src/verify_binaries.dart | 84 - .../cargokit/build_tool/pubspec.lock | 453 -- .../cargokit/build_tool/pubspec.yaml | 33 - .../cargokit/cmake/cargokit.cmake | 99 - .../cargokit/cmake/resolve_symlinks.ps1 | 34 - .../cargokit/gradle/plugin.gradle | 179 - .../rust_builder/cargokit/run_build_tool.cmd | 91 - .../rust_builder/cargokit/run_build_tool.sh | 104 - mobile/rust_builder/ios/Classes/dummy_file.c | 1 - .../rust_builder/ios/rust_lib_mobile.podspec | 45 - mobile/rust_builder/linux/CMakeLists.txt | 19 - .../rust_builder/macos/Classes/dummy_file.c | 1 - .../macos/rust_lib_mobile.podspec | 44 - mobile/rust_builder/pubspec.yaml | 34 - mobile/rust_builder/windows/.gitignore | 17 - mobile/rust_builder/windows/CMakeLists.txt | 20 - mobile/test/models/layout_node_test.dart | 117 - mobile/test/models/saved_server_test.dart | 65 - mobile/test/models/terminal_flags_test.dart | 53 - mobile/test/widget_test.dart | 30 - mobile/test_driver/integration_test.dart | 3 - 158 files changed, 176 insertions(+), 18635 deletions(-) delete mode 100644 mobile/.gitignore delete mode 100644 mobile/.metadata delete mode 100644 mobile/CLAUDE.md delete mode 100644 mobile/README.md delete mode 100644 mobile/analysis_options.yaml delete mode 100644 mobile/android/.gitignore delete mode 100644 mobile/android/app/build.gradle.kts delete mode 100644 mobile/android/app/src/debug/AndroidManifest.xml delete mode 100644 mobile/android/app/src/main/AndroidManifest.xml delete mode 100644 mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt delete mode 100644 mobile/android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 mobile/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 mobile/android/app/src/main/res/values-night/styles.xml delete mode 100644 mobile/android/app/src/main/res/values/styles.xml delete mode 100644 mobile/android/app/src/profile/AndroidManifest.xml delete mode 100644 mobile/android/build.gradle.kts delete mode 100644 mobile/android/gradle.properties delete mode 100644 mobile/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 mobile/android/settings.gradle.kts delete mode 100644 mobile/devtools_options.yaml delete mode 100644 mobile/flutter_rust_bridge.yaml delete mode 100644 mobile/integration_test/simple_test.dart delete mode 100644 mobile/ios/.gitignore delete mode 100644 mobile/ios/Flutter/AppFrameworkInfo.plist delete mode 100644 mobile/ios/Flutter/Debug.xcconfig delete mode 100644 mobile/ios/Flutter/Release.xcconfig delete mode 100644 mobile/ios/Podfile delete mode 100644 mobile/ios/Podfile.lock delete mode 100644 mobile/ios/Runner.xcodeproj/project.pbxproj delete mode 100644 mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 mobile/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 mobile/ios/Runner/AppDelegate.swift delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 mobile/ios/Runner/Base.lproj/Main.storyboard delete mode 100644 mobile/ios/Runner/Info.plist delete mode 100644 mobile/ios/Runner/Runner-Bridging-Header.h delete mode 100644 mobile/ios/RunnerTests/RunnerTests.swift delete mode 100644 mobile/lib/main.dart delete mode 100644 mobile/lib/src/models/layout_node.dart delete mode 100644 mobile/lib/src/models/saved_server.dart delete mode 100644 mobile/lib/src/providers/connection_provider.dart delete mode 100644 mobile/lib/src/providers/workspace_provider.dart delete mode 100644 mobile/lib/src/rust/api/connection.dart delete mode 100644 mobile/lib/src/rust/api/connection.freezed.dart delete mode 100644 mobile/lib/src/rust/api/state.dart delete mode 100644 mobile/lib/src/rust/api/terminal.dart delete mode 100644 mobile/lib/src/rust/frb_generated.dart delete mode 100644 mobile/lib/src/rust/frb_generated.io.dart delete mode 100644 mobile/lib/src/rust/frb_generated.web.dart delete mode 100644 mobile/lib/src/screens/pairing_screen.dart delete mode 100644 mobile/lib/src/screens/server_list_screen.dart delete mode 100644 mobile/lib/src/screens/workspace_screen.dart delete mode 100644 mobile/lib/src/theme/app_theme.dart delete mode 100644 mobile/lib/src/widgets/key_toolbar.dart delete mode 100644 mobile/lib/src/widgets/layout_renderer.dart delete mode 100644 mobile/lib/src/widgets/project_drawer.dart delete mode 100644 mobile/lib/src/widgets/status_indicator.dart delete mode 100644 mobile/lib/src/widgets/terminal_painter.dart delete mode 100644 mobile/lib/src/widgets/terminal_view.dart delete mode 100644 mobile/linux/.gitignore delete mode 100644 mobile/linux/CMakeLists.txt delete mode 100644 mobile/linux/flutter/CMakeLists.txt delete mode 100644 mobile/linux/flutter/generated_plugin_registrant.cc delete mode 100644 mobile/linux/flutter/generated_plugin_registrant.h delete mode 100644 mobile/linux/flutter/generated_plugins.cmake delete mode 100644 mobile/linux/runner/CMakeLists.txt delete mode 100644 mobile/linux/runner/main.cc delete mode 100644 mobile/linux/runner/my_application.cc delete mode 100644 mobile/linux/runner/my_application.h delete mode 100644 mobile/pubspec.yaml delete mode 100644 mobile/rust_builder/.gitignore delete mode 100644 mobile/rust_builder/README.md delete mode 100644 mobile/rust_builder/android/.gitignore delete mode 100644 mobile/rust_builder/android/build.gradle delete mode 100644 mobile/rust_builder/android/settings.gradle delete mode 100644 mobile/rust_builder/android/src/main/AndroidManifest.xml delete mode 100644 mobile/rust_builder/cargokit/.gitignore delete mode 100644 mobile/rust_builder/cargokit/LICENSE delete mode 100644 mobile/rust_builder/cargokit/README delete mode 100755 mobile/rust_builder/cargokit/build_pod.sh delete mode 100644 mobile/rust_builder/cargokit/build_tool/README.md delete mode 100644 mobile/rust_builder/cargokit/build_tool/analysis_options.yaml delete mode 100644 mobile/rust_builder/cargokit/build_tool/bin/build_tool.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/build_tool.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/android_environment.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/build_pod.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/build_tool.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/builder.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/cargo.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/environment.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/logging.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/options.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/rustup.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/target.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/util.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart delete mode 100644 mobile/rust_builder/cargokit/build_tool/pubspec.lock delete mode 100644 mobile/rust_builder/cargokit/build_tool/pubspec.yaml delete mode 100644 mobile/rust_builder/cargokit/cmake/cargokit.cmake delete mode 100644 mobile/rust_builder/cargokit/cmake/resolve_symlinks.ps1 delete mode 100644 mobile/rust_builder/cargokit/gradle/plugin.gradle delete mode 100755 mobile/rust_builder/cargokit/run_build_tool.cmd delete mode 100755 mobile/rust_builder/cargokit/run_build_tool.sh delete mode 100644 mobile/rust_builder/ios/Classes/dummy_file.c delete mode 100644 mobile/rust_builder/ios/rust_lib_mobile.podspec delete mode 100644 mobile/rust_builder/linux/CMakeLists.txt delete mode 100644 mobile/rust_builder/macos/Classes/dummy_file.c delete mode 100644 mobile/rust_builder/macos/rust_lib_mobile.podspec delete mode 100644 mobile/rust_builder/pubspec.yaml delete mode 100644 mobile/rust_builder/windows/.gitignore delete mode 100644 mobile/rust_builder/windows/CMakeLists.txt delete mode 100644 mobile/test/models/layout_node_test.dart delete mode 100644 mobile/test/models/saved_server_test.dart delete mode 100644 mobile/test/models/terminal_flags_test.dart delete mode 100644 mobile/test/widget_test.dart delete mode 100644 mobile/test_driver/integration_test.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5f7d701..eba4981d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,100 +67,38 @@ jobs: - name: Run tests run: cargo test - # Mobile builds: Android APK - # iOS temporarily disabled: macos-latest Xcode now requires a Development Team / - # provisioning profile even with `flutter build ios --no-codesign`, which breaks - # the unsigned device build. Re-enable once codesigning is sorted. - build-mobile: - if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - platform: android - artifact: okena-android - - runs-on: ${{ matrix.os }} - + # React Native mobile client — TypeScript checks. + # + # The native app build (uniffi-bindgen-react-native cross-compiling + # crates/okena-mobile-ffi to an Android NDK .so / iOS xcframework, then + # `react-native run-*`) needs the mobile toolchain and is NOT yet wired into + # CI — see mobile/rn/README.md for the device-side steps. Until then CI + # type-checks and lints the RN/TS sources; the Rust FFI crate itself is + # already covered by the `check` job's workspace build + clippy + tests. + mobile-rn: + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: mobile/rn steps: - uses: actions/checkout@v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.93" - - - name: Add Android Rust targets - if: matrix.platform == 'android' - run: | - rustup target add armv7-linux-androideabi - rustup target add aarch64-linux-android - rustup target add x86_64-linux-android - rustup target add i686-linux-android - - - name: Add iOS Rust targets - if: matrix.platform == 'ios' - run: | - rustup target add aarch64-apple-ios - rustup target add x86_64-apple-ios - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + - name: Setup Node + uses: actions/setup-node@v4 with: - shared-key: mobile-${{ matrix.platform }} - workspaces: mobile/native - - - name: Setup Java (Android) - if: matrix.platform == 'android' - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - - name: Flutter pub get - working-directory: mobile - run: flutter pub get + node-version: 20 + cache: npm + cache-dependency-path: mobile/rn/package-lock.json - - name: Build Android APKs - if: matrix.platform == 'android' - working-directory: mobile - run: | - flutter build apk --release --split-per-abi - flutter build apk --release + - name: Install dependencies + run: npm ci - - name: Prepare Android artifact - if: matrix.platform == 'android' - run: | - mkdir -p dist - cp mobile/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk dist/okena-arm64-v8a.apk - cp mobile/build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/okena-armeabi-v7a.apk - cp mobile/build/app/outputs/flutter-apk/app-x86_64-release.apk dist/okena-x86_64.apk - cp mobile/build/app/outputs/flutter-apk/app-release.apk dist/okena-universal.apk - - - name: Build iOS (no codesign) - if: matrix.platform == 'ios' - working-directory: mobile - run: flutter build ios --release --no-codesign - - - name: Prepare iOS artifact - if: matrix.platform == 'ios' - run: | - mkdir -p dist - cd mobile/build/ios/iphoneos - zip -r ../../../../dist/okena-ios.zip Runner.app + - name: Typecheck + run: npm run typecheck - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact }} - path: dist/ - retention-days: 7 + - name: Lint + run: npm run lint # Full multi-platform build: tags + manual trigger build: @@ -260,7 +198,7 @@ jobs: # Create release when a tag is pushed release: - needs: [build, build-mobile] + needs: [build] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index 90039139..856894a3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,13 +18,18 @@ settings.local.json dist -# Mobile (Flutter) -mobile/.dart_tool/ -mobile/build/ -mobile/.flutter-plugins -mobile/.flutter-plugins-dependencies -mobile/pubspec.lock -mobile/native/target/ +# Mobile (React Native) +mobile/rn/node_modules/ +mobile/rn/ios/Pods/ +mobile/rn/ios/build/ +mobile/rn/android/.gradle/ +mobile/rn/android/build/ +mobile/rn/android/app/build/ +mobile/rn/.cxx/ +# uniffi-bindgen-react-native generated output (regenerated by `npm run ubrn:*`) +mobile/rn/src/generated/ +mobile/rn/cpp/generated/ +mobile/rn/rust_modules/ # Web client web/node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md index b49492bb..60e12a5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,8 @@ On Windows, build from **x64 Native Tools Command Prompt for VS 2022** to avoid ``` src/ # Desktop app — main binary, GPUI views, app coordinator -crates/ # Library crates (23 crates, see below) -mobile/ # Mobile app (Flutter + Rust FFI) +crates/ # Library crates (24 crates, see below) +mobile/ # Mobile app — React Native UI (mobile/rn) over the Rust core via uniffi (crates/okena-mobile-ffi) web/ # Web client (React + TypeScript + xterm.js) assets/ # Fonts, icons (assets/icons/*.svg referenced as icons/*.svg) scripts/ # Build & utility scripts @@ -56,6 +56,7 @@ Most logic lives in `crates/`. The `src/` modules are thin re-exports (`pub use | `okena-ext-github` | GitHub status extension | | `okena-ext-updater` | Self-update system | | `okena-core` | Shared types, API client, key handling | +| `okena-mobile-ffi` | uniffi FFI surface for the React Native mobile app (`mobile/rn`); self-contained ConnectionManager / TerminalHolder engine over `okena-core` | ## Module-Specific Context @@ -68,5 +69,5 @@ Read these when working in the corresponding areas: - `crates/okena-workspace/CLAUDE.md` — State management, LayoutNode tree, persistence - `crates/okena-terminal/CLAUDE.md` — PTY threading model, shell detection - `crates/okena-git/CLAUDE.md` — Diff parsing, worktree operations -- `mobile/CLAUDE.md` — Flutter + Rust FFI mobile app +- `mobile/rn/CLAUDE.md` — React Native mobile app (uniffi over `okena-mobile-ffi`) - `web/CLAUDE.md` — React web client diff --git a/README.md b/README.md index a97dc1c7..e1cf8343 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ The install script includes built-in auto-update support. On macOS and Linux, Ok ### Remote Control & Companion Apps - **Remote API** - Local HTTP/WebSocket server for remote terminal control (see `docs/remote.md`) -- **Mobile app** - Flutter + Rust FFI companion app for Android/iOS (see `docs/mobile-status.md`) +- **Mobile app** - React Native companion app for Android/iOS over the Rust core via uniffi (see `docs/mobile-status.md`, code in `mobile/rn`) - **Web client** - Browser-based terminal access via built-in web UI - **Secure pairing** - HMAC-SHA256 token auth with rate-limited pairing codes @@ -194,7 +194,7 @@ Settings are stored in the platform's config directory (macOS: `~/Library/Applic | [Project Services](docs/services.md) | okena.yaml, Docker Compose integration, auto-restart | | [Git Worktrees](docs/worktrees.md) | Worktree management, sync watcher, path templates | | [Remote Control API](docs/remote.md) | HTTP/WebSocket API, pairing, authentication | -| [Mobile Client](docs/mobile-status.md) | Flutter + Rust FFI mobile companion app | +| [Mobile Client](docs/mobile-status.md) | React Native (uniffi) mobile companion app | ## Dependencies diff --git a/docs/mobile-status.md b/docs/mobile-status.md index a00d2768..285468e8 100644 --- a/docs/mobile-status.md +++ b/docs/mobile-status.md @@ -2,83 +2,74 @@ ## Overview -Flutter + Rust FFI mobile app (Android/iOS) for controlling a remote Okena desktop instance. Uses `alacritty_terminal` in Rust for ANSI processing — identical terminal emulation as the desktop app. Communicates with the desktop's remote server via REST + WebSocket. +React Native mobile app (Android/iOS) for controlling a remote Okena desktop instance. The Rust +core (`alacritty_terminal` for ANSI processing — identical terminal emulation as the desktop) is +reused below an FFI-agnostic seam and exposed to TypeScript via **uniffi** (a JSI TurboModule +generated by `uniffi-bindgen-react-native`). The terminal grid is painted natively with +`react-native-skia` — **no `xterm.js`, no WebView**. Communicates with the desktop's remote server +over REST + WebSocket. + +> Migration note: this app replaced an earlier Flutter + `flutter_rust_bridge` client. The binding +> generator and UI were swapped (`flutter_rust_bridge` → uniffi, Dart widgets → RN components); the +> protocol, TLS, reconnect and ANSI emulation in `okena-core` were reused unchanged. See +> [`mobile/RN_MIGRATION.md`](../mobile/RN_MIGRATION.md) for the full plan and rationale. ## Architecture ``` -┌──────────────────────────────────────────────────────────────┐ -│ Flutter Mobile App │ -│ │ -│ ┌──────────────────┐ ┌────────────────────────────────┐ │ -│ │ Dart UI │ │ Rust (via flutter_rust_bridge) │ │ -│ │ │ │ │ │ -│ │ Screens │ │ ConnectionManager (OnceLock) │ │ -│ │ Providers │ │ ├─ RemoteClient │ │ -│ │ Widgets │ │ ├─ MobileConnectionHandler │ │ -│ │ │ │ └─ TerminalHolder per terminal │ │ -│ │ │ │ └─ alacritty_terminal::Term │ │ -│ └──────────────────┘ └────────────────────────────────┘ │ -│ │ │ │ -│ flutter_rust_bridge (FFI) │ │ -└──────────┼──────────────────────────┼─────────────────────────┘ - │ HTTP + WebSocket - ▼ ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Okena Desktop (server) │ -│ │ -│ Remote Server (src/remote/) │ +┌─────────────────────────────────────────────────────────────┐ +│ React Native app (TypeScript) mobile/rn/ │ +│ │ +│ Screens / navigation / zustand stores │ +│ KeyToolbar, ProjectDrawer, LayoutRenderer (RN components) │ +│ TerminalView → react-native-skia (native GPU) │ +│ │ reads packed cell buffer, 3-pass paint │ +│ ▼ │ +│ TS bindings (generated by uniffi-bindgen-react-native, JSI) │ +└────────┼────────────────────────────────────────────────────┘ + │ JSI (in-process: sync host fns + Promises) +┌────────▼────────────────────────────────────────────────────┐ +│ crates/okena-mobile-ffi (uniffi-annotated, self-contained) │ +│ ConnectionManager (OnceLock + tokio runtime) │ +│ ├─ RemoteClient (okena-core) │ +│ ├─ MobileConnectionHandler │ +│ └─ TerminalHolder per terminal → alacritty_terminal::Term │ +└────────┼─────────────────────────────────────────────────────┘ + │ HTTP + WebSocket + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Okena Desktop — remote server (src/remote/) │ │ ├── POST /v1/pair → code → bearer token │ │ ├── GET /v1/state → workspace snapshot (JSON) │ -│ ├── POST /v1/actions → send_text, split, close, resize │ -│ └── WS /v1/stream → binary PTY frames + state events │ -└──────────────────────────────────────────────────────────────┘ +│ ├── POST /v1/actions → send_text, split, close, resize… │ +│ └── WS /v1/stream → binary PTY frames + state events │ +└─────────────────────────────────────────────────────────────┘ ``` ## Repository Structure ``` -Cargo.toml ← workspace: members = [".", "mobile/native", "crates/okena-core"] +Cargo.toml ← workspace: members include "crates/okena-mobile-ffi" src/ ← desktop app crates/okena-core/ ← shared crate (API types, client state machine, theme colors) +crates/okena-mobile-ffi/ ← uniffi FFI crate (self-contained — no flutter tooling) + Cargo.toml ← crate-type = ["cdylib","staticlib","lib"]; uniffi 0.29 + src/ + lib.rs ← #[uniffi::export] surface (~60 fns) + packed cell buffer + types.rs ← uniffi Record/Enum mirrors (+ From conversions) + api/ ← plain state-extraction structs + accessors + client/ ← ConnectionManager / MobileConnectionHandler / TerminalHolder mobile/ - android/, ios/ ← platform shells - lib/ - main.dart ← App entry, MultiProvider setup, AppRouter - src/ - models/ - saved_server.dart ← SavedServer data class, JSON persistence - layout_node.dart ← Sealed classes: TerminalNode, SplitNode, TabsNode - providers/ - connection_provider.dart ← Saved servers CRUD, connection lifecycle, status polling - workspace_provider.dart ← Project list polling, focused project tracking - screens/ - server_list_screen.dart ← Server list + add bottom sheet - pairing_screen.dart ← Connect → pair flow, code input - workspace_screen.dart ← AppBar + drawer + layout + key toolbar - theme/ - app_theme.dart ← JetBrainsMono font, Catppuccin dark colors - widgets/ - key_toolbar.dart ← ESC, TAB, CTRL/ALT (sticky), arrows - layout_renderer.dart ← Recursive layout tree → TerminalView/Flex/Tabs - project_drawer.dart ← Project list drawer with disconnect button - status_indicator.dart ← Colored dot + label - terminal_painter.dart ← CustomPainter: bg rects → text → cursor - terminal_view.dart ← Terminal widget: resize, input, 30fps polling - rust/api/ ← generated Dart bindings (do not edit) + RN_MIGRATION.md ← migration plan (uniffi + native Skia rendering) fonts/ ← JetBrainsMono (Regular, Bold, Italic, BoldItalic) - native/ ← Rust FFI crate - Cargo.toml + rn/ ← React Native app (see mobile/rn/README.md) src/ - lib.rs ← pub mod api; pub mod client; - api/ - connection.rs ← connect, pair, disconnect, connection_status - terminal.rs ← get_visible_cells, get_cursor, send_text, resize_terminal - state.rs ← get_projects, is_dirty, send_special_key, get_project_layout_json - client/ - manager.rs ← ConnectionManager singleton (OnceLock + tokio runtime) - handler.rs ← MobileConnectionHandler (impl ConnectionHandler) - terminal_holder.rs ← TerminalHolder (alacritty_terminal::Term wrapper) + native/okena.ts ← OkenaNative binding contract + getOkenaNative() + native/cells.ts ← packed cell-buffer decoder (matches the Rust encoder) + state/ ← zustand stores (connection, workspace) — DI for tests + screens/ ← ServerList, Pairing, Workspace + components/ ← TerminalView (Skia), KeyToolbar, LayoutRenderer, ProjectDrawer… + models/ ← SavedServer, LayoutNode AST ``` ## Data Flow @@ -86,43 +77,36 @@ mobile/ ### PTY output (server → mobile screen) ``` -Remote PTY process - → PtyBroadcaster (server) - → WebSocket binary frame [proto=1][type=1][stream_id:u32][data...] - → RemoteClient WS reader task (okena-core) +Remote PTY → WS binary frame → RemoteClient WS reader (okena-core) → MobileConnectionHandler.on_terminal_output() - → TerminalHolder.process_output(data) ← alacritty ANSI processing - → dirty flag set - → Flutter polls is_dirty() every 33ms → get_visible_cells() via FFI - → TerminalPainter (CustomPainter) renders cell grid + → TerminalHolder.process_output(data) ← alacritty ANSI processing → dirty flag + → RN render loop (requestAnimationFrame, gated on isDirty()) + → getVisibleCellsPacked() → ArrayBuffer → decodeCells() → Skia 3-pass paint ``` +The hot path uses `get_visible_cells_packed` (a compact `cols,rows` header + 13-byte cells) instead +of marshalling thousands of records per frame across JSI — the key perf lever (Decision C in the +migration plan). The record form `get_visible_cells` is kept for non-hot callers. + ### Keyboard input (mobile → server) ``` -Soft keyboard / key toolbar tap - → Dart calls FFI send_text() or send_special_key() - → ConnectionManager.send_ws_message() - → WsClientMessage::SendText via WebSocket - → Server bridge → PtyManager.send_input() - → Remote PTY stdin +Soft keyboard / KeyToolbar tap → sendText() / sendSpecialKey() (JSI) + → ConnectionManager.send_ws_message() → WsClientMessage::SendText → WS → server PTY stdin ``` ### State sync (project list, layouts) ``` -WS "state_changed" event or initial connect - → RemoteClient fetches GET /v1/state - → Parses StateResponse, diffs against cached state - → Creates/removes TerminalHolders for added/removed terminals - → Auto-subscribes to new terminal streams - → state_cache updated in MobileConnection - → Flutter reads via FFI get_projects() +WS "state_changed" / initial connect → RemoteClient fetches GET /v1/state + → diff against cached state → create/remove TerminalHolders → state_cache updated + → RN stores poll getProjects()/getFolders()/getProjectLayoutJson() (1s) ``` ## Shared Core: okena-core -The `crates/okena-core/` crate contains all code shared between desktop and mobile: +`crates/okena-core/` contains everything shared between desktop and mobile (reused **unchanged** by +the RN client): | Module | Contents | |--------|----------| @@ -132,127 +116,64 @@ The `crates/okena-core/` crate contains all code shared between desktop and mobi | `client::state` | `diff_states()`, `collect_state_terminal_ids()` | | `theme::colors` | `ThemeColors`, `DARK_THEME`, `ansi_to_argb()` | | `keys` | `SpecialKey` enum with `to_bytes()` | -| `ws` | Binary PTY frame format helpers | -| `types` | `SplitDirection` | +| `types` | `SplitDirection`, `DiffMode`, `FolderColor` | -The `client` module is behind a `client` feature flag (adds tokio, reqwest, tokio-tungstenite, async-channel, futures). +The `client` feature pulls tokio, reqwest, tokio-tungstenite, async-channel, futures — all with the +**rustls** backends, so the NDK / iOS builds never cross-compile OpenSSL. -Desktop uses the same `RemoteClient` with `DesktopConnectionHandler` (creates `Terminal` objects in the GPUI `TerminalsRegistry`). Mobile uses `MobileConnectionHandler` (creates `TerminalHolder` objects in a shared `HashMap`). +Desktop uses the same `RemoteClient` with `DesktopConnectionHandler`; mobile uses +`MobileConnectionHandler` (creates `TerminalHolder` objects in a shared `HashMap`). ## Key Decisions -### Flutter + Rust FFI (not React Native + xterm.js) - -- Same terminal parser as desktop (alacritty_terminal) — no rendering divergence -- Shared Rust code via okena-core — real code reuse, not just type duplication -- CustomPainter for grid rendering — full control, no WebView overhead -- Higher build complexity (NDK cross-compilation) — acceptable tradeoff - -### NoopEventListener on mobile - -The server's `Term` already handles PtyWrite responses (cursor reports, DA sequences). If the mobile `Term` also forwarded these back via WebSocket, they'd be written to the PTY twice. So mobile uses a no-op listener. - -### Terminal ID namespacing - -Remote terminal IDs use the prefix `remote:{connection_id}:{terminal_id}` to avoid collisions. The ConnectionHandler receives both the raw `terminal_id` (for WS messages to the server) and the `prefixed_id` (for local storage keys). - -### ConnectionManager as OnceLock singleton - -Mobile doesn't have GPUI's entity system. A `static OnceLock` with a 2-thread tokio runtime provides the async backbone. All FFI functions access it via `ConnectionManager::get()`. - -### FFI ConnectionStatus simplification - -The FFI `ConnectionStatus` enum collapses `Reconnecting { attempt }` into `Connecting` — mobile UI doesn't need the attempt counter. - -### DARK_THEME as default palette - -Cell colors use `ThemeColors::DARK_THEME` for ANSI → ARGB conversion. Theme switching can be added later by passing a `ThemeColors` reference from the Flutter side. +- **React Native + Rust via uniffi (not RN + xterm.js).** Keep the desktop's exact terminal parser + (`alacritty_terminal`) and real code reuse via `okena-core`; render natively with + react-native-skia. The documented fallback if the ubrn build proves too painful is to drop Rust on + mobile and reuse the web client's TS protocol with a TS ANSI parser feeding the *same* Skia + renderer — `xterm.js` stays rejected. See `mobile/RN_MIGRATION.md` §5. +- **Self-contained FFI crate.** `okena-mobile-ffi` owns the `ConnectionManager` / `TerminalHolder` + engine outright (carried over from the retired `mobile/native`, frb attributes stripped). It + depends only on `okena-core` + uniffi — no Flutter tooling. +- **NoopEventListener on mobile.** The server's `Term` already handles PtyWrite responses; the mobile + `Term` uses a no-op listener so they aren't written back to the PTY twice. +- **Terminal ID namespacing.** Remote IDs are prefixed `remote:{connection_id}:{terminal_id}`. +- **ConnectionManager as `OnceLock` singleton** with a 2-thread tokio runtime — no GPUI entity system + on mobile. All FFI functions access it via `ConnectionManager::get()`. +- **FFI `ConnectionStatus`** collapses `Reconnecting { attempt }` into `Connecting`. +- **DARK_THEME** as the default ANSI → ARGB palette; server-driven theme sync is a later addition. + +## FFI Surface (`crates/okena-mobile-ffi/src/lib.rs`) + +~60 `#[uniffi::export]` functions. Cheap getters on the render hot path are **sync** JSI host +functions; anything that crosses the wire / awaits a server response is **async** (JS Promise, via +`#[uniffi::export(async_runtime = "tokio")]`). The full contract — and the TypeScript shape ubrn +emits — is mirrored in [`mobile/rn/src/native/okena.ts`](../mobile/rn/src/native/okena.ts). Highlights: + +| Function | Sync/async | Description | +|----------|------------|-------------| +| `init_app()` / `connect()` / `pair()` / `disconnect()` / `connection_status()` | mixed | Connection lifecycle | +| `get_visible_cells_packed(conn, term) → Vec` | sync | Packed grid buffer — the render hot path | +| `get_visible_cells` / `get_cursor` / `get_scroll_info` / selection getters | sync | Render/selection reads | +| `is_dirty(conn, term) → bool` | sync | Unread output since last fetch | +| `send_text` / `resize_terminal` / `resize_local` / `scroll` / `send_special_key` | sync | Input / viewport | +| `get_projects` / `get_folders` / `get_project_order` / `get_project_layout_json` / `get_fullscreen_terminal` | sync | Cached-state reads | +| terminal / git / service / project / layout actions (`create_terminal`, `git_diff`, `start_service`, `add_project`, `update_split_sizes`, `move_pane_to`, …) | async | POST `/v1/actions`, return `()`/JSON | ## Current State -### Done - -| Layer | What | Status | -|-------|------|--------| -| **Shared core** | okena-core with API types, RemoteClient state machine, ThemeColors | Complete | -| **Desktop client** | `src/remote_client/` — DesktopConnectionHandler, RemoteBackend, sidebar integration | Complete | -| **Desktop server** | All endpoints: health, pair, state, actions (including resize, create_terminal), WS stream | Complete | -| **Web client** | React SPA at `/v1/web/` — connect, pair, browse projects, render terminals (xterm.js) | Complete | -| **Mobile Rust core** | ConnectionManager, MobileConnectionHandler, TerminalHolder, all FFI functions wired to real networking | Complete | -| **Mobile Flutter UI** | Full UI: ServerListScreen, PairingScreen, WorkspaceScreen, project drawer, terminal rendering, key toolbar, layout rendering | Complete | -| **Mobile state management** | ConnectionProvider (saved servers, polling), WorkspaceProvider (project list, focus tracking) | Complete | -| **Terminal rendering** | CustomPainter (3-pass: bg, text, cursor), 30fps dirty polling, auto-resize with debounce | Complete | -| **Key toolbar** | ESC, TAB, CTRL/ALT sticky toggles, arrow keys | Complete | -| **Layout rendering** | Recursive split/tab layout from JSON, portrait-mode auto-vertical, tab switching | Complete | -| **Saved servers** | SharedPreferences persistence with JSON serialization | Complete | -| **Rust tests** | 8 mobile native + 23 okena-core unit tests | Passing | -| **Dart tests** | 22 unit tests (7 SavedServer, 7 LayoutNode, 8 terminal flags/colors) | Passing | - -### Not yet done (polish) - -| What | Description | -|------|-------------| -| **Gestures** | Text selection, pinch-to-zoom font size, scrollback (two-finger scroll) | -| **Auto-reconnect UI** | Visual feedback banner for reconnection attempts | -| **Long-press arrows** | Key repeat on long-press for arrow keys | -| **Theme sync** | Receive theme colors from server instead of hardcoded DARK_THEME | -| **F-keys** | F1–F12 in toolbar (swipe-up row) | -| **App icon & splash** | Custom launcher icon, branded splash screen | -| **On-device testing** | End-to-end test on physical Android device with real Okena server | - -## FFI Surface - -### connection.rs - -| Function | Sync | Description | -|----------|------|-------------| -| `init_app()` | init | Setup FRB + ConnectionManager | -| `connect(host, port) → String` | sync | Create connection, start health check, return conn_id | -| `pair(conn_id, code)` | async | Pair with code, start WS | -| `disconnect(conn_id)` | sync | Close WS, cleanup terminals | -| `connection_status(conn_id) → ConnectionStatus` | sync | Current status | - -### terminal.rs - -| Function | Sync | Description | -|----------|------|-------------| -| `get_visible_cells(conn_id, terminal_id) → Vec` | sync | Grid cells with ARGB colors + flags | -| `get_cursor(conn_id, terminal_id) → CursorState` | sync | Cursor position, shape, visibility | -| `send_text(conn_id, terminal_id, text)` | async | Send text input via WS | -| `resize_terminal(conn_id, terminal_id, cols, rows)` | async | Resize local grid + send WS resize | - -### state.rs - -| Function | Sync | Description | -|----------|------|-------------| -| `get_projects(conn_id) → Vec` | sync | Project list from cached state | -| `get_focused_project_id(conn_id) → Option` | sync | Server's focused project | -| `is_dirty(conn_id, terminal_id) → bool` | sync | Terminal has new output | -| `send_special_key(conn_id, terminal_id, key)` | async | Send named key (Enter, CtrlC, ArrowUp, ...) | -| `get_project_layout_json(conn_id, project_id) → Option` | sync | Layout tree as JSON | -| `get_all_terminal_ids(conn_id) → Vec` | sync | Flat list of all terminal IDs | - -## Next Steps (Polish) - -### 1. Gestures & interaction - -- Pinch-to-zoom font size (adjust `_fontSize` → recompute grid → `resize_terminal()`) -- Long-press arrow keys for key repeat -- Text selection + copy (long-press → drag to select → clipboard) -- Two-finger scroll for scrollback - -### 2. Visual polish - -- Auto-reconnect banner (visual feedback when connection drops and reconnects) -- App icon and splash screen -- Theme sync from server (receive `ThemeColors` via state, pass to `TerminalPainter`) -- F1–F12 keys in toolbar (swipe-up secondary row) - -### 3. On-device testing - -- End-to-end test: connect to real Okena server, pair, browse projects, type in terminal -- Verify performance (30fps rendering, resize latency) -- Test with large terminal output (build logs, `htop`) +| Layer | Status | +|-------|--------| +| **Shared core** (`okena-core`) | Complete — reused unchanged | +| **Desktop client + server** | Complete (all endpoints) | +| **Web client** | Complete (React SPA, xterm.js) | +| **Mobile FFI crate** (`okena-mobile-ffi`) | Complete — self-contained, ~60 uniffi fns + packed buffer; `cargo test -p okena-mobile-ffi` passing (11 tests) | +| **RN TS app** (`mobile/rn`) | Scaffold complete — screens, zustand stores (DI-testable), Skia `TerminalView`, packed-cell decoder; type-checks + lints | +| **ubrn binding generation** | Config present (`mobile/rn/ubrn.config.yaml`); cross-compile not yet run (needs NDK/Xcode) | +| **Native host projects** (`android/`, `ios/`) | Not generated yet — `react-native init` + merge, see `mobile/rn/README.md` | +| **Phase-0 spikes** (toolchain S1, Skia throughput S2) | Not yet run on-device | + +See `mobile/rn/README.md` for the exact device-side steps and `mobile/RN_MIGRATION.md` for the +phased plan. ## Networking @@ -268,19 +189,15 @@ The remote server binds to a configurable IP (default localhost). Mobile clients ## Build ```bash -# Rust only -cargo build -p okena_mobile_native -cargo test -p okena_mobile_native - -# Regenerate Dart bindings -cd mobile && flutter_rust_bridge_codegen generate +# Rust FFI crate only (runs anywhere — no mobile toolchain needed) +cargo build -p okena-mobile-ffi +cargo test -p okena-mobile-ffi -# Build APK -export ANDROID_HOME=~/android-sdk -export PATH="$HOME/flutter/bin:$HOME/.cargo/bin:$PATH" -cd mobile && flutter build apk --debug +# RN TypeScript checks +cd mobile/rn && npm ci && npm run typecheck && npm run lint ``` -**Critical notes:** -- `run_build_tool.sh` needs `$HOME/.cargo/bin` in PATH (Gradle daemon doesn't inherit it) -- Use `rustls-tls` (not `native-tls`) for all deps to avoid cross-compiling OpenSSL +The native app build (ubrn cross-compile → Android `.so` / iOS xcframework, `pod install`, +`react-native run-*`) requires the mobile toolchain and is documented step-by-step in +`mobile/rn/README.md`. Use `rustls-tls` (already selected by `okena-core`'s `client` feature) to +avoid cross-compiling OpenSSL for the NDK. diff --git a/mobile/.gitignore b/mobile/.gitignore deleted file mode 100644 index 3820a95c..00000000 --- a/mobile/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/mobile/.metadata b/mobile/.metadata deleted file mode 100644 index 6b591ae4..00000000 --- a/mobile/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "67323de285b00232883f53b84095eb72be97d35c" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c - - platform: linux - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/mobile/CLAUDE.md b/mobile/CLAUDE.md deleted file mode 100644 index 845edb1f..00000000 --- a/mobile/CLAUDE.md +++ /dev/null @@ -1,187 +0,0 @@ -# Mobile App — Flutter + Rust FFI - -Remote terminal client for Android/iOS. Connects to the Okena desktop server via REST + WebSocket. Uses `alacritty_terminal` in Rust for ANSI processing — identical terminal emulation as the desktop app. - -## Build Commands - -```bash -# Rust crate only -cargo build -p okena_mobile_native -cargo test -p okena_mobile_native - -# Regenerate Dart FFI bindings (after changing Rust api/ signatures) -cd mobile && flutter_rust_bridge_codegen generate - -# Build APK -export ANDROID_HOME=~/android-sdk -export PATH="$HOME/flutter/bin:$HOME/.cargo/bin:$PATH" -cd mobile && flutter build apk --debug -``` - -**Critical:** -- `run_build_tool.sh` needs `$HOME/.cargo/bin` in PATH (Gradle daemon doesn't inherit it) -- Use `rustls-tls` (not `native-tls`) for all deps — avoids cross-compiling OpenSSL for NDK - -## Architecture - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Flutter Mobile App │ -│ │ -│ ┌──────────────────┐ ┌────────────────────────────────┐ │ -│ │ Dart UI │ │ Rust (via flutter_rust_bridge) │ │ -│ │ │ │ │ │ -│ │ Screens │ │ ConnectionManager (OnceLock) │ │ -│ │ Providers │ │ ├─ RemoteClient │ │ -│ │ Widgets │ │ ├─ MobileConnectionHandler │ │ -│ │ │ │ └─ TerminalHolder per terminal │ │ -│ │ │ │ └─ alacritty_terminal::Term │ │ -│ └──────────────────┘ └────────────────────────────────┘ │ -│ │ │ │ -│ flutter_rust_bridge (FFI) │ │ -└──────────┼──────────────────────────┼─────────────────────────┘ - │ HTTP + WebSocket - ▼ ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Okena Desktop (remote server on src/remote/) │ -│ POST /v1/pair, GET /v1/state, POST /v1/actions, WS /v1/stream │ -└──────────────────────────────────────────────────────────────┘ -``` - -## Directory Structure - -``` -mobile/ -├── lib/ -│ ├── main.dart # App entry, MultiProvider setup, AppRouter -│ └── src/ -│ ├── models/ -│ │ ├── layout_node.dart # Sealed classes: TerminalNode, SplitNode, TabsNode -│ │ └── saved_server.dart # Server config (host, port, label), JSON persistence -│ ├── providers/ -│ │ ├── connection_provider.dart # Saved servers, connection lifecycle, status polling -│ │ └── workspace_provider.dart # Project list, focused project, 1s polling -│ ├── screens/ -│ │ ├── server_list_screen.dart # Server list + add bottom sheet -│ │ ├── pairing_screen.dart # Connect → pair flow, code input -│ │ └── workspace_screen.dart # App bar + drawer + layout + key toolbar -│ ├── theme/ -│ │ └── app_theme.dart # JetBrainsMono font, Catppuccin dark colors -│ ├── widgets/ -│ │ ├── key_toolbar.dart # ESC, TAB, CTRL/ALT (sticky), arrows -│ │ ├── layout_renderer.dart # Recursive layout tree → TerminalView/Flex/Tabs -│ │ ├── project_drawer.dart # Project list drawer with disconnect button -│ │ ├── status_indicator.dart # Colored dot + label -│ │ ├── terminal_painter.dart # CustomPainter: bg rects → text → cursor -│ │ └── terminal_view.dart # Terminal widget: resize, input, 30fps polling -│ └── rust/ # GENERATED by flutter_rust_bridge — do not edit -│ └── api/ # Dart bindings for native/src/api/ -├── native/ # Rust FFI crate (okena_mobile_native) -│ └── src/ -│ ├── lib.rs -│ ├── api/ -│ │ ├── connection.rs # init_app, connect, pair, disconnect, connection_status -│ │ ├── terminal.rs # get_visible_cells, get_cursor, send_text, resize_terminal -│ │ └── state.rs # get_projects, is_dirty, send_special_key, get_project_layout_json -│ ├── client/ -│ │ ├── manager.rs # ConnectionManager singleton (OnceLock + 2-thread tokio runtime) -│ │ ├── handler.rs # MobileConnectionHandler (impl ConnectionHandler) -│ │ └── terminal_holder.rs # alacritty_terminal::Term wrapper + dirty flag -│ └── frb_generated.rs # GENERATED — do not edit -├── fonts/ # JetBrainsMono (Regular, Bold, Italic, BoldItalic) -├── rust_builder/ # Cargokit — NDK cross-compilation tooling -├── flutter_rust_bridge.yaml # FRB config: rust_root=native/, dart_output=lib/src/rust -└── pubspec.yaml # Flutter deps: provider, shared_preferences, flutter_rust_bridge -``` - -## Data Flow - -### Terminal output (server → screen) - -``` -Remote PTY → WS binary frame → RemoteClient → MobileConnectionHandler.on_terminal_output() - → TerminalHolder.process_output() (alacritty ANSI parse) → dirty flag - → Flutter polls is_dirty() every 33ms → get_visible_cells() → CustomPainter repaint -``` - -### Keyboard input (user → server) - -``` -Soft keyboard / KeyToolbar tap → FFI send_text() or send_special_key() - → ConnectionManager.send_ws_message() → WS → Server → PTY stdin -``` - -### State sync - -``` -WS "state_changed" event → RemoteClient fetches GET /v1/state - → diff_states() → create/remove TerminalHolders → state_cache updated - → Flutter polls get_projects() every 1s → UI update -``` - -## Key Patterns - -### State management -- `ChangeNotifier` providers with polling (no push from Rust → Dart) -- `ConnectionProvider`: 500ms fast poll during connect/pair, 2s when connected -- `WorkspaceProvider`: 1s poll for project list, auto-selects focused project - -### Terminal rendering -- `TerminalPainter` (CustomPainter): 3-pass — background rects, text, cursor -- `TerminalView`: `LayoutBuilder` measures size → computes cols/rows → `resize_terminal()` FFI -- 33ms refresh timer (~30fps) via `is_dirty()` check -- 200ms debounce on resize - -### Layout rendering -- `LayoutRenderer` parses `get_project_layout_json()` into sealed `LayoutNode` classes -- Recursive rendering: `TerminalNode` → `TerminalView`, `SplitNode` → `Flex`, `TabsNode` → tab bar -- Auto-converts horizontal splits to vertical in portrait orientation - -### Key toolbar -- CTRL/ALT are sticky toggles (tap to activate, next key applies modifier, auto-resets) -- CTRL+key sends control character directly (e.g., CTRL+C → `\x03`) - -### Rust singleton -- `ConnectionManager` is `OnceLock` — no GPUI entity system on mobile -- 2-thread tokio runtime for async networking -- All FFI functions access via `ConnectionManager::get()` -- `NoopEventListener` on `Term` — prevents duplicate PtyWrite responses (server already handles them) - -## FFI Surface (native/src/api/) - -### connection.rs -| Function | Description | -|----------|-------------| -| `init_app()` | Setup FRB + ConnectionManager | -| `connect(host, port) → String` | Create connection, return conn_id | -| `pair(conn_id, code)` | Async pair + start WS | -| `disconnect(conn_id)` | Close WS, cleanup | -| `connection_status(conn_id) → ConnectionStatus` | Current status | - -### terminal.rs -| Function | Description | -|----------|-------------| -| `get_visible_cells(conn_id, terminal_id) → Vec` | Grid cells with ARGB + flags | -| `get_cursor(conn_id, terminal_id) → CursorState` | Position, shape, visibility | -| `send_text(conn_id, terminal_id, text)` | Send text via WS (async) | -| `resize_terminal(conn_id, terminal_id, cols, rows)` | Resize local + send WS (async) | - -### state.rs -| Function | Description | -|----------|-------------| -| `get_projects(conn_id) → Vec` | Project list from cache | -| `get_focused_project_id(conn_id) → Option` | Server's focused project | -| `is_dirty(conn_id, terminal_id) → bool` | Has new output since last read | -| `send_special_key(conn_id, terminal_id, key)` | Named key (async) | -| `get_project_layout_json(conn_id, project_id) → Option` | Layout tree as JSON | -| `get_all_terminal_ids(conn_id) → Vec` | All terminal IDs | - -## Dependencies - -- **flutter_rust_bridge 2.11.1** — Rust↔Dart FFI bridge + codegen -- **provider** — State management (ChangeNotifier) -- **shared_preferences** — Persist saved server list -- **okena-core** (workspace crate) — Shared types, RemoteClient, ThemeColors -- **alacritty_terminal** — Terminal emulation (same as desktop) -- **tokio** + **tokio-tungstenite** (rustls-tls) — Async runtime + WebSocket -- **reqwest** (rustls-tls) — HTTP client for REST API diff --git a/mobile/README.md b/mobile/README.md deleted file mode 100644 index 7b012856..00000000 --- a/mobile/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# mobile - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/mobile/RN_MIGRATION.md b/mobile/RN_MIGRATION.md index aa928113..fc1b6703 100644 --- a/mobile/RN_MIGRATION.md +++ b/mobile/RN_MIGRATION.md @@ -1,8 +1,11 @@ # Mobile app → React Native migration plan -Status: **proposal / spike-gated.** This branch (`feat/mobile-rn`) is PR #17 (`feat/ios`) -rebased onto current `main`. It still ships the Flutter app; this document is the plan to -replace the Flutter UI layer with React Native while **keeping the Rust core**. +Status: **in progress — Flutter removed, RN in place, device build pending.** The Flutter app +(`mobile/lib` + `mobile/native`) has been deleted; the Rust engine now lives self-contained in +`crates/okena-mobile-ffi` (uniffi), and `mobile/rn` is a complete RN 0.76 project + tooling + +`ubrn.config.yaml`. What remains is toolchain/device-bound: generating the native host +projects, running the ubrn cross-compile, and the Phase-0 spikes (§3) — see +[`rn/README.md`](rn/README.md). This document is kept as the design/rationale record. Primary strategy: **RN + Rust core via uniffi**, with **native GPU rendering** of the terminal (no `xterm.js`). Dropping Rust entirely is a documented *fallback*, not the goal. diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml deleted file mode 100644 index 0d290213..00000000 --- a/mobile/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore deleted file mode 100644 index be3943c9..00000000 --- a/mobile/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java -.cxx/ - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts deleted file mode 100644 index 18a310af..00000000 --- a/mobile/android/app/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id("com.android.application") - id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id("dev.flutter.flutter-gradle-plugin") -} - -android { - namespace = "com.example.mobile" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.mobile" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") - } - } -} - -flutter { - source = "../.." -} diff --git a/mobile/android/app/src/debug/AndroidManifest.xml b/mobile/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/mobile/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 771480b6..00000000 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt deleted file mode 100644 index b5dc9d07..00000000 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.mobile - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/mobile/android/app/src/main/res/drawable-v21/launch_background.xml b/mobile/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f3..00000000 --- a/mobile/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/mobile/android/app/src/main/res/drawable/launch_background.xml b/mobile/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/mobile/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 183a983d4c5ef66b57b8c4bf3b6db590a90a018d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9540 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 diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index bc8e0a002a86e9ca2d696e584066a0842cd8c12d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5205 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 diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 0235ae46dc142efe0e524a504e4217ccd3a38092..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28820 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 U8L$4I-v)ssL}Z0)1oZ>{10riTQ2+n{ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 3e5db9a69088af8c828a0e830a35224d3dc8c0a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46942 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 diff --git a/mobile/android/app/src/main/res/values-night/styles.xml b/mobile/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be7..00000000 --- a/mobile/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/mobile/android/app/src/main/res/values/styles.xml b/mobile/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef880..00000000 --- a/mobile/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/mobile/android/app/src/profile/AndroidManifest.xml b/mobile/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/mobile/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/mobile/android/build.gradle.kts b/mobile/android/build.gradle.kts deleted file mode 100644 index dbee657b..00000000 --- a/mobile/android/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties deleted file mode 100644 index fbee1d8c..00000000 --- a/mobile/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e4ef43fb..00000000 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/mobile/android/settings.gradle.kts b/mobile/android/settings.gradle.kts deleted file mode 100644 index ca7fe065..00000000 --- a/mobile/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false -} - -include(":app") diff --git a/mobile/devtools_options.yaml b/mobile/devtools_options.yaml deleted file mode 100644 index fa0b357c..00000000 --- a/mobile/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/mobile/flutter_rust_bridge.yaml b/mobile/flutter_rust_bridge.yaml deleted file mode 100644 index f945cbbf..00000000 --- a/mobile/flutter_rust_bridge.yaml +++ /dev/null @@ -1,3 +0,0 @@ -rust_input: crate::api -rust_root: native/ -dart_output: lib/src/rust \ No newline at end of file diff --git a/mobile/integration_test/simple_test.dart b/mobile/integration_test/simple_test.dart deleted file mode 100644 index 2582c066..00000000 --- a/mobile/integration_test/simple_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/main.dart'; -import 'package:mobile/src/rust/frb_generated.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - setUpAll(() async => await RustLib.init()); - testWidgets('App launches and shows server list', (WidgetTester tester) async { - await tester.pumpWidget(const OkenaApp()); - await tester.pumpAndSettle(); - expect(find.text('Okena'), findsWidgets); - }); -} diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore deleted file mode 100644 index 7a7f9873..00000000 --- a/mobile/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 391a902b..00000000 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - - diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6f..00000000 --- a/mobile/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bfe..00000000 --- a/mobile/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile deleted file mode 100644 index 620e46eb..00000000 --- a/mobile/ios/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock deleted file mode 100644 index e4313dd1..00000000 --- a/mobile/ios/Podfile.lock +++ /dev/null @@ -1,35 +0,0 @@ -PODS: - - Flutter (1.0.0) - - integration_test (0.0.1): - - Flutter - - rust_lib_mobile (0.0.1): - - Flutter - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - -DEPENDENCIES: - - Flutter (from `Flutter`) - - integration_test (from `.symlinks/plugins/integration_test/ios`) - - rust_lib_mobile (from `.symlinks/plugins/rust_lib_mobile/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - integration_test: - :path: ".symlinks/plugins/integration_test/ios" - rust_lib_mobile: - :path: ".symlinks/plugins/rust_lib_mobile/ios" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - -SPEC CHECKSUMS: - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - rust_lib_mobile: dea72a6cd79b7b0f9290832863b286c0e1089e95 - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb - -PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e - -COCOAPODS: 1.16.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 628a8575..00000000 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,739 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 5D927AE7C85E7E21F166320A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A00D3A07A3D7AB504E49C92B /* Pods_RunnerTests.framework */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 8CDAFE869349F09635AB6E97 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDC7A6B878DA9CD9A6F58B28 /* Pods_Runner.framework */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 08742543B10EB622BFFF46D1 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2DA56008CD86AEF940B2FB34 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 2E6912DF4BFB900C5A6D5BDA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3E8F5284E6CBECDEFD0CB98B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 4C3662B941C3A1379E40692E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 97CC3AB8249A32BF8FCC1482 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - A00D3A07A3D7AB504E49C92B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - EDC7A6B878DA9CD9A6F58B28 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8CDAFE869349F09635AB6E97 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A920B32E1280E1AD7354E52F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5D927AE7C85E7E21F166320A /* Pods_RunnerTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1200D467201819F9E21D60B5 /* Pods */ = { - isa = PBXGroup; - children = ( - 97CC3AB8249A32BF8FCC1482 /* Pods-Runner.debug.xcconfig */, - 4C3662B941C3A1379E40692E /* Pods-Runner.release.xcconfig */, - 3E8F5284E6CBECDEFD0CB98B /* Pods-Runner.profile.xcconfig */, - 2DA56008CD86AEF940B2FB34 /* Pods-RunnerTests.debug.xcconfig */, - 2E6912DF4BFB900C5A6D5BDA /* Pods-RunnerTests.release.xcconfig */, - 08742543B10EB622BFFF46D1 /* Pods-RunnerTests.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - 1200D467201819F9E21D60B5 /* Pods */, - BCB270922978D966DAFBE887 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - BCB270922978D966DAFBE887 /* Frameworks */ = { - isa = PBXGroup; - children = ( - EDC7A6B878DA9CD9A6F58B28 /* Pods_Runner.framework */, - A00D3A07A3D7AB504E49C92B /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 3A2067B0755B6C7A1CDF4EFD /* [CP] Check Pods Manifest.lock */, - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - A920B32E1280E1AD7354E52F /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - ED4B0DDE84CC1AC9C309073D /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - EF29B406BDA8A4DF6634F855 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3A2067B0755B6C7A1CDF4EFD /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - ED4B0DDE84CC1AC9C309073D /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - EF29B406BDA8A4DF6634F855 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = UA8K24B574; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = UA8K24B574; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.okena.mobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2DA56008CD86AEF940B2FB34 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2E6912DF4BFB900C5A6D5BDA /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 08742543B10EB622BFFF46D1 /* Pods-RunnerTests.profile.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = UA8K24B574; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = UA8K24B574; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = UA8K24B574; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.okena.mobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = UA8K24B574; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.okena.mobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index e3773d42..00000000 --- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14..00000000 --- a/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift deleted file mode 100644 index c30b367e..00000000 --- a/mobile/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { - GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) - } -} diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab..00000000 --- a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index f17c65b8111982fe950ef3d102d70b2995362a2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 260461 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%0DdHSN-ajvncd~GxSi~|P32u&AE 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�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

`0+1h@GoZ)xoZk`Ku?P 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 diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2aed6485e5e61613dc05a29bf5ba21eab0314fa9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3956 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 T;@&JG9Ri6I6)_Y=dWlp~5HS>iEWJn-0qF#! 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 diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 51348c93ce3297868fa07b6bd49d2359c7a4a086..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12834 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 F{6Bh{V)Ot2 diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index a2e8f335de9d346e5a4f7c4fd78b5dded9a05c73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11230 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 diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 975e2dea279ff284515c48012b0e993ec4a2a9e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21345 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 diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e2b6dbaecd7ba90b4c478ccf755b1f2987a80f3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42201 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 diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 3b525e4491cbc7a68560b5209c82e650a12dd1c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10373 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 X$N!MzaGZSqhk;R$QI)QfG!OePx~xUY diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 7e91b7c8e872fb5ce80b3104d4d9a26aadb464a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31674 zcmV)YK&-!sP)1^@s67{VYS0000WV@Og>004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x z00(qQO+^Rl0t*d01AD1!^Z)>Qnn^@KRCwCO{dc@=$$8fae=DrDb~y3gbIwhjqoi)N za+Vw<+gKL1WjV}Xz&7TUVayC38+gH(0mCqS-ghwYGD94a?XekPY-7MC$hIUaTdmk? zb(Feqci(*OJ!!`ks%rkI6>6=$&+V2my!(FkJv*$uR;{XE{hnA)3H~cSzU|FF!-@XO zm}S>Bum)^{JLQ}`2kx|kQ#iK;Yy#`xhQJW$J4kR&J6sG-0opky!6^pkk*z(BWb$N>Vcj4#3b%JxX+fn`20oFO^Y;(r)wZj0$ zIcJ@7)4FUSAV025Vs1%gTSl6y}~)}0Cx&lbI$g`Mb0?^Ryb#I*m6*; zA0}tZ!Ljv#Rf8E0cpD_)fOdgUzIUDDI={vFUUF{n{B58#-#OaXT7GQznDD%=)m}dV ze1A9>g17q2Hc#JI);X60GvElgCIsifeHq*%;2u>ndf|7T{JGhC-tbfW`on+ezZeF7 z&jI$nxBf$<9N@5tK)eFPw*cQL1P?mrb{sacHpkcuW9Jx~Vr+_a8P?@EXG$=YomT>? z-BedLUN$!(1VV(z2w?FuQGSjP^%+I^p7MMm_|N~oI#yH{Yn`oqzUutpuS9dc)_2YM z>T`APA!z6F;qsHsmH@QYU~GXgIfcn7OolNjh0U;I@oSMOKW-ibJyEh{DV;HZ9pd zoW%kmWT*41!3e+<0Hq783J?V6Xuc1B7lNUIO6&M~;M>3oThRV(7idj;nsx=i3(qHx zX|E71fYw)Uofkls$2Kjjde(z3C(CEd^D*=Eh%}o}nACy24&o!gFAAZ4%i8QHO1Aj) zr@v-xfmu7BJtNKF+h39{&Fyz`px! zKZkV=CFMgxh(8VDyR0i#X4y5yvkRofehI1yIL#%G2UoXQa#7433ohzwy%@NxxE%rJdEWelFYKh%WTZIty1hTeZIzM24bb--y7h|(Ah<`1RKNH9OM~Wij zvtRjz-!(?m@Aik^^Y)(y!B9!{ej()l4t)JAyAdBu9_MKKEV-Ekmk3xxh-1Ok9%xa6 z%i-#wy*YFJ-hylSd6wx^;%fv~mtor1S+0|PEzBiv+Sc%zQb>C73Twkt^m`i=MHZ*| z^i|fGcUzn1LdeHXZ9I@)*?;tRg_EzD!M^h?|A<~Rz*uvq5aO>0#P{ZAmK@EVVLZQx zwHZQG11+n*SQxNnVzJ#U(FT`WE=pdExoXx~XhYjT=;}=EHU^+*ka8@5nl?%n!NtNr zhvM>P%oTp#o;1e*=taQQ1+EZ+!shH9y}-4-r%CfkDiFUUg#3$znLo2Syp2zNjk?*_ z#9;4z%Rl1ZeDeLg{MPrrT?qM)fp^ccUA;Gclxcbu=j;-2Eg7)7vz86O&B3+EfX6e} z@!;x;wZ%aU+tX@iXdKiadgN76f=kjwap&9m1vNG5vFJ^j{ zjvjK(zEvsx_-CK~+2>z>?;qjP-WOgnW?nLbz3**5OHt$^iF@w@@i!di_M_=}j;3G5 z*u29eRpYxi6yEr8VzG042VBdJKlVH?0ND7m2<;LS&-Fpga&2v(yKNZI(f2AnK?hDWS;!DEKOJcD1 zz3rb-6sd@#{`X5E|2ONhbNk~bIGR0+b!LGzjssV91ERw?i>$&@=DHcUy65SFYeDZ? zu;N|>T;j#R6@pZm#S<@}Cc|_r2|B$%|AZ(ct@~lz*$R^PRoXml@A50&W3ZzJZJ2 zsu~{ZT#LUAz{S@FuGV?F;99DL2OUa#pt|6y+T_2MHHa4Vs>-ziHv>x-04-3y1aNix z?!^FlQ_(28=jf_6l41V@NxZ^nx;r#Be@9-7F7=b`N4MAS;mZD(e)j---&_AF!@)Yh zcS|Y%x5CVKZj2sfnqKKNq^qcRn5zcOO>`w88e8#eWUf|6AH!TXQ>g2sZ)(+c2Br(H z*7?2`aD8pewcxT5$DN}M-kZ0jOA5_*l|Qndtka9v7|(Wxg~{Gtn8`C=eCAglfAh=! zC>O4O_G<^&cfIwWqE$?8(sxK9|Gu%=xxMk1mw@Z$VzE(Nk5iNb5yx_!YZ==u7efT# z368A*CKkaUnua#NU9d2Z2Uqi6hAldFK)dIyqm6=_#8XQW?)8Sr7iM~16?~r*6y$>8r+Z9 zP%36Ly|&@7uZ(r?gN2#w-*)PaT)h5;g^|)r$4`KBb||I#%i!L4G&|3De&JYfEuBGK zaEWkumi)G6uCgnS%~I%m+>*p$2Q^@I&I81`Dqbf`aV$N^-14?J;A)He_C+P#snBk_ z?PPAah4=MD3-6+J53<(%D(9$YFNHuNakSKw1;%WHt37eTzv0ncNVpp3TN`(h=3}ny zJ@tC)%uj2T{BcuclO;b}WU%jf`_BIDo0qLm!7 z)1uhFx!7#Yn0iw9E)xuT>&((4=IQul4tM2feB5e2EZDXL~$*`C;;6_N& z6;AD(DRo+Xe!&QHeW3Kk3ZzjLDCmWyv6gKDiE1zXFy*^e_f4{Z) zzZ>??@v$%e%{GH6)x+3qRf_1px2{+{nmxM&T#L-r4oNg1T*&7NK~>#EsZUsEa3w<( zh1WfLy_h?1J;TfIyPKEYcMs>zo@R4%oqoTE(#nfKB@j{|Wa-L~{xi=6)n`?wf-#24 zc*>=VyFC8bSNQzrALr?(pJz0hqEtkrW6uD};-tn&heXwE7It?%kCDS@VIr)Fir*)s1v&r_mA zaqlbc=QVGAJKMM1Pws}yW|f`-{k}kHfiaNhP#CbK3$N-bB`eT0Vp3~JV*gjGHo(G% zDj9ElaKvXn@dZBio1fy^rCnm(CyM$+s)yEzXSG@tP_?M+E@)W>uH%lW9^>hCUbyxM z&e>m4D*ArsT>9`+|IE879=QE|2q7O5LjF{m?+uS;&(hYpUb?k-EV!(#m0x2Gd682T zIk()h!}tHU@8J)8&$n~WZFeG_!Wiq%7qF5Pvdvmj*5Hz|%V2HsDTP3mvtCFKH0w&Q zg@CxPIk~gOL$AJ%*S+omlu%r_aGB|JQr|tTMWV$zgE0kpmeT7-eCKz* zlmG6Ie?Rx%buY!#l4nINUTa2@r5+^(O3DCOUQ~wSGJL9v?JD-GzLNq`{gndez#3Nq zO|f-qomap1<=k`k?OeXH%jL^gYd@B;1*XWz@;Te5Px4)V@Vj{Dd*6#3+{(d`q8B?( z?Ffb|f_Wy`+js2Va2y?4($q6T(5wm%d?JZr7__}yo} z0rkM`--C6gFNFL_>#}=}rq8tUyShkkoTy%WHh#V1#9|39TTqySJkPlI-rM-`pZEd3 z^_#zeeC!w>O#L9cKz9-qcw{3QrEJ8b>HtRxl#(bVnx~e?Qs?p_(TggnWhp8pI+o~2 zVhcx+IfRDZissy{JG|yK4=|f$JpJ@DWO-UI2YdA!9^yxS{D(Pv&qM6)Ymx}go`ykR zaCijQb{+fsj@is%jH^LZiRT(%C0pr=p``FYDw(T#UHy%t=5=KZ=-MD?fMzY^MsV)d zlRWg=mvOK^=EC_Gyu(E_MvrKX6~~CgMR0W&5m)b}kc?;7an2c~dcSN;UZ_{x{yu~d zcS+=*NsEKE@%)01oh)%lv_13<%`ZhTE|uF7g(c__QR1Eu(F!)%2(gRWKwYctLIC%o`4{> zx-1yq2A5sVXtZ@PB@~nSA$gJZfnRmF8|s1E-iJVZ9q>cb?24LYH00e3&160V?EZ2zG{w9C7|l32!L50lNX?4 z&4aJLlhG*SnWvs9$4%DJj_z2uS#1$qU3W!wybyvcpD|01`a+10ILG7KTSoR(&erap|HA)^o*J-!WkQ}Aio9suQtSL882l=H zDckH8{#7NoWXWnuNrb{PU~s;=-2$BT%wh|N)ULc3U>!x_btWB4);1!({X4#$lRK|t zGFs)v{*0GD&|{hko<8q5I&#F?v35dW5sXKcd1jl)Pdz|he90>QF9JqV0W$@kY3)U0 z99ohAYgG(Lz(EyxRv=3=h@*)A<_F%%-t|2mdHBmMXRB5*Sc1>EslK(Wjz<+fO5!0x z$OyzkV17kgS7;&RE3C^Y?DQBWl)>ScQ$-LNv^!D%KJ;$SRA`|E2_xt8dA7RLK;@%%Xbb?;}Yz z$}xq-S%p$C9QIJr4lleg;$;sEDU9U&1xGr!oY@f!`jWjv*uQQ_XQk&`il)4UaNtA( zE}F?BD@9*+}78^xa&$e4T3- zcY|1^kBxPf26sdvwTq^6K+S15N2Gg5sWHa9LaCmX2)T~qPHQu)&6WUWF#+#baMhk} zuh@)Fdkq_9zr%`QtM?ci?C7ny)s|wS6Vb6lL9GR z39g{yLi_?YcL?R1ioo8_Som+hG54;77k}zlQ^VHb~g^4NKEd?#s-{2$BHOczXrUJ8jK&kWPagoA@An_H{w9cGLV3Up7CtSXceWHU=Pu?Xwv z4vRBBhJT)yWk2?R=Fsegw9IwFO3Ev`T}uA;_PXMjf~AYpsB1b(Rv=| zt_bo_vDVP*MZD*|Uyrn^WUA5`ry7?-<5VqO5lW$?KvnOhKx;w2uSp_>bPgpTP82vt zHp$56rMMd9q@#>%k~1G?%#L!3%y_YA9WoNN;xCn8J}x-AI>$MOR05M(q!vUa$mXLQ zXB}}*F*+=mPBQXrhBa{U*#kB=Ba)uxxhFGwR7<%Jh_rkt;i#w4S`tN)WEkNb^j0H; z5KQ*xSnI<&^O0qIW5nj!6#%kXfhc`n+1wB(ivDWsMQ9{QhMK779j#IcM*DO2E);B> z9$+%>s!)+{Ly=n!uFV*(#b_-Uj|@UsF2AtPt+#D4n>e2M;$>FWG`HWk&fr9kX$rGL z;~5H2eNQJg@E-`O2vLK0mcs3KoZz)@ekH&8Z$FIDJ&Y`T5Zo!hW}y`b?~HROFI}MMJ6q<{ zNeSY#26xiyP6qL!;0k?Vi!#}jL2c&@Z+`1TtgH-~9eT^ulqm#CdZpYJ5?7k&I#MM4 zh@`Jc=LHhz4I|QNPEQGlo0|yB5CHx4J5JPV6L{m>9^|9H@kxxyur|h8jT8na1isX5Gsx{)wIQoD8r+FWj>0kE zVi~xawT5*@jq3bZv@B zjR)4V`>gK_868e|>I>&t-&$p5eaO-77$+TZ60?8pkSiB=S>Ie`I+~%hVz@f+VJPQV z+gc?~VlM8D=&$uSaeAG2;F;0nhHK9q(pyPL1~JJn#uk>Sr%_t*!sENhB;oMJh&0_N z>EFwfk3GxD?RBDoVt;>%V9bdVJvKHItTpWK=1e964hUm>GL6s`p%HJ$0p4mg9^!XRB2)NuublMCe43ti@#0oP(+D@MG?JTkMVfI#fz6n;)L;JLNDpjOA^vFMLsLp z+CG8J9pk6Q3|EJ2Zf&5H^5Km%S(u>(RxsEj!V=U|2tK?bE)$>=`yE-B_IhUWm zM4Uu8Hn?(W4+-3M+Y0lkbD=~wW9@j4Cj7Kws5cK+S z?FbdZyIrD4vAXIrS;6aA7KOl|l;p&TKI8N> zxUEdMN)OC(*xc-K{{wgO{8yf@)4iQ67pGK??y$mkd(b-;W_1Oxqgz+3X>co*wID)p ziRH{CI^b(N=D-@u*>gK=tgSM>=9BSEZpo$v^KnK#FP)iE&>O@QdBMT8F+#1P2b#(5 zjI11(3(sC9FLEwjy2A5Up5yZM%RG7h=~B)&F_=hu{j4a8GE)<*wM059(}XNqYLYmn zD7E59YoaJZD@k4$@;t*@$DltzDTy%_sU&d{b8s+WG__p4F+qyYFdVM)!o`@Pu&k~1 zxbT8zJSix0i_(HP(&U9>Hup>wDQ~D`g(FQ1OyNrFST;((@IJ9P_C`_Vg$Dsrp=HeW z&K7IyXBn=ZId=*zHQ8j0+vs6( zLpsgL(wrjmdH%vlOm5lOSz)v{;lfkbSiLr2eS3w;;f&|cU*ef(pX1T5Jj&zGeVNNQ zE|KPWGa6+h7S{%}ANA)!h$wu~SaY292x8aFE%MBfFyFY3Ep4z93P zLD-=|*K+3UHqJQjCNLJ0S+cqJ;9KiZ!l7e{$qb|2DVt|k>2D-lcxsnR&+noKinPf1 z*cU&_!;k+CSy7->L=?rfGZp88b_VV6zEgrxYXh!|ZPs^D4g7KUT+hbN1gjR_2EP{} zOd1f$3RLx^kWI8ASh3W!t(iDdc(;yLnn*`DB;)CXE6;z5j~+h5-i`0&*4rN7#K~1o zpB`}Xq=dd_Cylp8vmCNqpq2AFTJdtsX1*A>78i81FD9j6V{;9Y+xl7L9$+T-mZTLHlN*LBJ&d(nJ-^T9=~eD|?Jd0U z^i@9o!O!s9pZ#q<^W>+=3*%S&XhmT&Z>2WVsTSqcylUx&?VD=;Z!vkLXO!QDzY+M7 z25p3P00g)SJXJq+sve4lnSAFj&jb#$8CqZ~ztc+7Px@@FpJ1gwWOsj$M?UvU^!gi| zJ$FC1-YQt%5ERw}Oer`TdA)2ffR&-BePy-;n6=RF_ZY4YIk{O&tj>;7M3C_u(@f(gN39034`?> z8>d#7j#DN_a|VL}Ynv-Peep@sEc45)q#!qQoM02Ef%lhCnvfGM-IHvy^jNXW7~~!POhrdHRWuvA(uV z5^K(#>(gHm=*Y3ZSGptmf{k?#s0MjX$N?#P-8SEnfqIRM+$%K`UV;R`m;HnnoHhts01XhSd0}AJtq`NQ=zvGtZ zf~%3uA{a<-wan)kwiJuvFeUCqbp+nzhT&?T{p&|uI6q_i+$MwdA!(X%xIgB`-X1Sp zeV)P;_$oLKCv4r>HAQz-z=|2erq+G2C^i=><^<$l0xcAe3!=BnVmEm-YDMSA)wzQ{ z7RUT;okzetOG&dCS}A7f0ki2L(^;rMsf^pBDI#(>$dm z;o3G~*FtP(x>jv!4Qth(iJP(5BG{VWRzlj<@bUK;NYh3Tl)tEs?PQ2J$co$prI<6D zjd0d+<3>S$s5rf&QCct>L*GK&^Kr~P_bjHBM_8m_HZx@Dyd}~DsN8gAQ3bL^h7$l% zJLhDb+36P3q8GunIK!&Al_<0O&Y!=CF@^V}7n&mV*$LHe(pg5*i#U7h7OU$6Eh5gu>i)K{J>{F;B)WNdCFvzvvtC;HdKtpmN=7aYzlg@2iGixH1)S1 z#a=2VF-#{p)A6*iZmUbWHgmn$lD76_01!$$oT_JRz!I&Ftff+Wnl-UYS%Oodq~iRu zml#i`IAJSm-iv&VgyN7()M+R~Q8gR(GQaEp0=$UlxAd5{aFkEfuXTnHvD=Cg=i8=}r*B zCkqT#G(~Q?awTW;gyPh;U}Hm|wPSzZi^IJ1?58OVhcKH94)?}Pjz)DkfyxvL!TwUf z)$S_c0oGPf7OA?@twb$z7F@6;&}13zP+Ypa%QIhnft}TDu3oxMFNzTeY`M(F7>TnM zDHKP0Q}R4xIOr3_5yqHc$=7kpJPnA-lxU$HLgBY3Ro-19oWvQ2Q$A&<+$GS1&6f95 zv0o=c#o-!TVhLF433&|2jsr&}4wtJTFZS#1xf-!FpQc=SeuTWG&%O8e*g2&T%5nX= zBR9gQ##u17S^x~|tAf!8u3osxbTUEf-qO`YFBM$16cRf>RdvV;L^}viFxHK?&H+X78kEx;fb$2LzWsaWm2K_iG{*ZzOLLD z!UN5fY-U5xnOJB>S7f@tAm{?4%Vyz9tfdHQiTC3n=92;|W6x0(1shulx82caJ}Y?Y ziGrvn>8~iHbXenjijZ|gT5#^PVtnXGjo>R!JVjCDL{WbUxY{?r3|wt1veidYw_*qF zUJFEEpo0a>Rh!T*JgrX_{P?Fn&pY4xW_ETqdGe`ena$^b_mK^UD@-O+_HOJWl_W`G zbfih5n10en)tPtg&_>k^At=RT`BbpJIpoj$$sgsN-}-gLea(E7@t=PEL;T}^`SaY^ zJ46%`S2(Qnd1(Gz-XY{AO?g-4f_3kOZ1q^(gQ0kH9c#07Y*$ZPB!|vtmEo~?K-Ur# z;b03GtoCuv@x_NPqx6td=T=#fg7ijNj=?$7xlgX#+|*>LV>*TT>;_Lf^0`Hlu30Dy zF$ET72(ASyvSKxXy7%0>|B`c=EbJ&adu0_K2QE4mN0~`c6=Ac6gQEkEjt+VGU3Zgb z1y^>jvA4g+ zyWj~wYXz)=uut7@OI$y#Xh8Y9CXN>#PgBBFku^>CxlB)c`&~by)`Uxr-OMxOatK+{YFL>1@U7JpK1e3hnq923%;c2Ue*HuI&zpaqpZU9gheyBi zv|o@bEjAZeVZAOEEE90X)#)<`A6yoqJZ2$a9&%5>Frn*XY2(|>P^l-4r5ReIZB`4d zQRf;r!gO*#FG`ut$6UMO7o7}NVj`!oCL&5A*4Gth&c$q>ido;(r1KOh_Nda_eI`X% zY`78?ZPp4W#G;F$ao>otkY~$eteETAo(wHW%EbXCsBCF67e&GAU-tli>QDVJCz1`G zf9f(jJ15z{am0m7m$~is+t}P#CsHv-M@ymV!727f2NX73J{NiqL>t_-KIWKA z#$_$S;O2q3n{mfOxAIqh>L>ZzfA8<}vClnR<`g?bA-!%UylzGniYh7zQ4OraNzwSw zj?*fr55I}})OffS4hnO3sDjE(CF`ohLMXbxv-*jxP42k;EZf`LoZK3+u^F+trs(w~ zQS9rE83WEqlAg~OyXE$XKlM{T#Lxc2O+N5zzvat*$^t11g!2}qtOwyHnXGPEr})89 z_nmw9Pdk@W*l|tkp`F5(b0Q?3FJcmUj^p2 zq8^4b>*aK!(@%m{FNWV$Z44(+ePk=x-B|P0@lh0Gjn9L-YRitW>q^-aZKC|O$nT;lgvfJb(UzuUFfuP1~IVZC!8y zT;=<$%&$}To%`0Gb}q-*Nps_295U$gZbvnhpsNBWDO)>h{H4G4$GPX8+nF8Axqk7G zYnS#|KefXBueyUle*k5&VZYyJZF9)E`?k5`RcASS#}0NT_|(InW_SNu6Z%;Ku%L7- zQc14vU+3zjE4=Q3*U(?>VK9NX5f}`5pm@`3UQf@(Jod~Jo+({L^=(3F`cSk0QdJMPvHJ|Fw^$GN(Hl`2i;Sb!lKpdonX;&VLp z)Kffo*DE-=z3D;bnzmakM2?4E@gOJrCwT0sCzzY8o%>bK!nW_P0a?d^(4CAefVI7a z%RttRoCwhdRLdz8Br7@`v5v6@a#OH#=GC0sI!C|PV`W9MeOfSFrOYz+$%lg#NP1;} z+d-T_FY&Lpw^y;w@%geaO1s8>>yBybTwQQ^T;zF%brxAg&O>XZt>=h^&Im43hbr@2H(*lBd_QIV%nGOOJi+?u74lhuj3qi!6q!NAig=)rS~9sYC!hJ9M5Gcd zud`1hjzWiM>DY6|;R?xT9(k0%@W1?3zVyk*=(!$e*ieDk-xf!psoOP-hOB%NO;8FqL#A+c#s z=a|YwXl1iH5l+^H+1CPAr!=Mzj_>&HuV-uL1o^CBxYI{Rirz+o$qddo`s)eKIlul< zfGK>gzQC`S?5)N`Nq74s*9_dU@QV{TTduk(94;3;d*Kp)`EUGnKK$=LPLlV^-FzC5 zIUF-N;|<^VD*oc1{i*c*biy}pTpjcfK^m-mlvxyIGSM|l5qb@O^qq2H+`FT+|c22GHP49V2 zxz$au(z~-^SF8PY#NK1=a>4H2AwTnf{9XRV z&;K%Vs!*uX)Nbx&&YZhneLFw>=l&dD|GGD!5{HZhLYKf%4VXl^D!eN$ooM%4hg#Yl zDH?xXcMgRUy*-E`c-Ok|+URsRvdo*ltE-Z8w+T+)0;}sjs}($us!Bsr`bD})T^dS| z=YlMUH^2RroZQ+d7r`_uy>A*Ws@SYr?Olfyg=etJByUfi5L#_;H7E9{Hm2b9Z+Zo% z&YZ;NmV8?HW%zx~Y%leN;Ue!65K5q8Nt7s}SQ7OWz4eHAs2S}|x%BK+jHxm&LOn3( z;#LZdsHA)oYcvj*J4TZ^|Lj-)1^@8x|18<{oQSA}Nr5198JoAS^XLBbkMmvM^lqY_ zXR*4-N|l{a`rPWQjl~&QKV4`A2p^1F2DYU@tK0!DV6VE(ZMFz4U*{a-alz$_h8x#? zVU(3s$I7Z>b%pbt+0mi=v=3x`f#bPM_W4q1WE;7vefoZaijc-)6VLwKS`} z_klY@2!eG}j;77WfvY`xobwJk9ZBBwwg-`T<-M2}Bx^DG%#coUqJhTb7H4d!XL+`Y zS0V-{dc-|VHZzR(QnZYSqEJkRcC4U#i*;{Tj$Y?VkqNGFtqPY8{YDuuh(J1RYq-3eT%tT z?G5=>luCZnErAC*2wAjN)r8pfJ6@bPxz2qrzk?#P6te=I_<-JQKlK%DfP7wH^D31A zWF*mv_CT47#D0j(7)s}ZM9 zD|SwKUzd)&o1!j^;+kxZ1rV1n^9*jk`&QQ0hqZ`oJo~mjIMlbAbA-Whe>1Y8wd0v9 z0AG+iS6LCp8g9Mq6x%zS6q)e=>uIt{PI{OjwZ!HYoBB#O*81EnSrr+{EXFvB%q#q} zaoXwJt~?Ppo!p&q^C>*S^1yBP^X50co}9cjNy5L<33jjVHkB8vdSoSAHDn}eC$CV# z>t|Z{bz(}?Pa)}8n1NE?eO9a&EGS+Mu{K*&uZ*!w#s!n9V`Uw-PI*P%+MwWGWjK^T z2%pt08w)45H#mK68)J%EJl1+hB?1G5zX=mI=dTMocV|Z1;~Wf^5Us(hPi7r=-hGZF zPRM74&oxql^eCf94Jd)ljR#p_J;;nN`et+A$VzW>&XEk_dS4*HF%vmNP&f?1fcZeU zAc`gLe#+IleV+RKGkoadAFH>*1A=0BG6=LU0>nvIi_j|4 z;L5*NxpL78Z5=HXk@aGeM&tx4H6d%bn@q6j#BuEsOedhq3Wu)3ZSbrvT2g}BNcV{K_e1Fz%?KF0SUxq*%@n_LtNo7^8yt~U+83# zV+)JRErs(g1X~t8spNT)8ievSxm2P^R$yhlkLv6awP2yu8g45|s}yUcTS3E#wH1Ew zd;b9M`HpXap(S%^N5VKHlBkIJ*l&HBzxU7n30L;_7j!)#%T(tw@2zIInoN8^dLd6g zXriM8plw^COP9QLW^wiXaf~K_;V@= zeRDvS9M=kHw=67L#z?rwQ8jmTbx=z&4gFu0NC<8(eC5g(=c&P@7B!R#A7HqzD7~y18mD*1wp*)jC zeN8gd$ha(6Qdn@J!(iUr@|_}r=9ZlvD%2XHp5U9_@pb&jAN?V=?l{4WX?IrOa3myL zedap<;2-}JKK8kXDGI=fI$fv!i70*2Rp+{LT2;nS*IgoLtV>6eDO-Z7W<6WZeAn&W zM$Z+k_tkXnC}@r1Ltz|~kz+h^?40#=DyrN~W6OoM0f>a~85UK^r|LUdicJDyXc{h- zfa`df?EzMA9n-$amey5;jOvaJkX$(OX^u$^MQSi}Lq0Dk3R_QjsU*reh!x}(rbW5( z+xx(jmdqzP={&<)+iFLtMFfH?bX_=~f3IQV#DE|CzVG9^zW4VdR|P5a*5s%-plADh z?h{|+@BY(&%rlo>z~;hB1LMjpR?!k~B6QG)G!v>#IIyCu$<_8#3!gaa{7xGd1QKB!qFRJu940Nu z1oYQpzt7Nd3oMp(a{1Ot$IjLUKlu|s##`U@Mp8AS=;XpX92$+BYJT-s|2;qVOaFqs z(UE5@>-@^2aP?8iL|(7+BhiY5wtY5U2dm~qRg<`-qm_ea$}$bcuf+0Y9`QM7G(=g8 z((r<{@)SB;D?eCS#4hm_45t%N5v;5@dcCH!d8O1BrrfvE_&OF92HJ!b`nYZauCTUA z3;hD!D0b~2(K4m8xm6L8NDUTe9qHH?-z~Dj`?!qrCX6ZFA!PvBnG)E_i_IdlG@FQa z_D1OdUDOk$GT|)md(V4#+qb-lDU;p4NyDlc}+T|uyvdH%4V>$T9=epGeQ zq;jmT3D!4ZZNn@2Rb8+m^D#Mds=pTcN~zZrXmZu0O0T z@)y%jz;{~Z`JNy7ihd@y<=Fd@1WJR*!77W@0%?V}GOa5evoJkOnT_W)KtfSd>0DGD zAN08tg8T2ko1CoGwpt{*9#4Pq8UFS^_y;`l^pgmyJg|%?#TT?voU7GJ5s1i@0q_RW z1WZ-~soUFuRpwloF&Z4x|E%!YPL^OF(s#N9YYp(;S>7jwR!BN*|1({h}I6ZmCoHz`q6fva++2}H0=t8F${Wyp=O<vS1zwN&_5bu6eBHa=!sgj^3JNru(e8ww z{n?-6-~HwXn9Xv(4$_p?UT6i@36Jf&|3YN+17?EHgT$6UT`Yp!*&)d6@%NO~xwO zunHYn6NHiQ%D*)<`eaMj>spcFG1U^brmark8LWFVwV$NwoFI$S1vgc}ul93QyLE)3 z!+8fQ(l5+0nZZN~6_;^#+lXkB8nStYb&FTQwcFJwqXOYHUwrgQe(KNvHNNM4zn>FZ zYh1gw$G`pc|HK!bcnqw<8QHAnuJxj3{Dr`aDdB5AeBB%0!l^s97&B^j&Vo*~_<3XB zy5KI_xnVnZZt-2;_U%0S)xU|!T!t|A;!XTwl|EAEv;#`{(%hzG zE9WW;%CnU%Ecq-im-3f>w8AmT3`e_@j+$QvSa+bh_2LCIcis$aO4#1>};cz#@qy~H+zICQI_S* zra7Y{pO&MwsKucxtLp%mq6rNd+knkl&t8RS7_D)$ENhsfB^Cqr3Db3Ss|G{2I~Ga> z1m6e1t9Lgmop)M$rQKr7FoZ|Gz#$`riu`5==_n_k`S6lZUVkf!0uwA-Yxihdxv#B0 z3oaK3C$SE)ytG_hsl?L}Il=O^`VR)+Nw~#A7vdZSAj_CxU)IVmQ>q5w((D6#lqsKck|cmAX@DN7Z{# zdh(;_m=<(mk&6pI!i-k`2r~NuD0O^d&3kGKC)}B~+9=dy-Zde$_u7lp`+KAm$Z~Hw zV=VJgNpb4noqzHxXoM&ffkZ zCY6wDMB%S1EM81I(W!S)n_Bg2KcJ#!jPYpB<%?rno-i51>Y8M@0)rL5L`^BLi{)7x zT$N#5`M;`3;clV}E(f0tSg!J`3Uu2@J1(T%8Sjd@f?RafQTgLKU?KK8}~>1B5i+E z)ep9C6q(;|Jc{7t4(y!v-yiJx3|C{@VDjlzKGQYJ8}XQBHkgSO3Ta($8MxXZd4aCj zU~#!e1E%(nmam21Q&I@Ywaa@rEKyI9?%8r*#)4!e#-;_fFn$(i1{+BR8$;I5u2AHb z>E4Wki@yBhY@RY5PyKds-2trcrrpmqP#m5GLYIT>&OwrRF`4InkMMMExO(YYNg%Sh%@+X|;XRfQ9gM;; zz-oK#E1y~Adr?a9)YH##csOEZI7B6i{9uk1AeABMY32ucxvQE63)v_`YQgZ-fVFc& zrZ?ssU7N7~{D{$H(yC3d^x?{xA_5Ak=8p?lN-Vjt>R99aUOct}91RGo)F4&n_Dlc6 z%C}PwZcA@!03xc_f?z%eq58_tRRha^R()Q_DCsgnGa^v=sv2>uwBLrW$h-*OzX8{; zIVO{GzsPd)RUx21@aNFlgX-|WpGqs(y>`Uqix*lI55xXl4z5P`swIXCLPcT27lX^8 zFWF}`h;j>FEj5=f@A33g&+&%WzLIz?rr4iDZm`npU{S0wY3}tk5h1l?wmZYj3i>+( zq!6rZ^tpTO7WNMh(Vr-{5xbeazD#Pgz6?92b2}9<>cc`$di4~GrwLVv(w#x484JGm zPVim?*A}kQy+SLuZSgL*DBS|1j7M`0_fpR6tT0@G(UD_xdtEN8rtow){0T7bh?JLbmO1x=HWND*Y)PqyVMGa zlA1J~^NEjtk=H$VKT%&1t;A$kM}BYa(n+VH2s6(?3iN8^)2gpc$q!R(ZkQZS8M&Mr zyZacEmyKPnXbLA53cooK`mxH93|I41chl9I>Iqo6Edqd5(=cShrOA*i@JwxFEV*{g zP_ALKiuHu+PUz*2mfcfo>H-*#49`qGc$9`-PtxlNW>c@5t*%K1LoW_bZ2JYb2m4+` z#)%+HEsuQuvlK;6FG&`XKNr(>j`^-sy8;GV2ChYG(sz(V3Wt;mWfhT%`P8Ss%y<00 z3!GUwNq<9Qk2LAE5k;Eej76{X(ET38!GvNmL$3Ca1(<1u$qKGry~Z46DKwO zfzOG+ z(EyVcTsoX!F-RG)kt%vdVocFUrn+rQz}2ej^&(^AD!>E-Tr*QMxoTflskDX*bi3DI z_Gs9%+h@z1sTDvQ(fDHLUwWI)VQoQP%(%XLjlCO+UVnv+6GPSpk~sF;`5hh@CS#aR zEaQ>E7(uTu+1ykZBlz6IkMQ}={5DE9^})IucP(Ar9V|%_mE5 zECR2GNuf|uGnvo%yZ`Ff`HA;@19zDL=byNN5jmo;6mCvxj@Y|4=J5F`M@5E|3XSG4 zopG4Wm|M$!HpQA*TN&>H{v{89XvA!-fmWv!i_BAjLqfHWWBpyQYUoAV5a!u;Q_TZx z)o?l%T%pww=*nfNs9`_LboK(5E=R1d-pPrRTPO|tdovF9{7p-(iL^v3MbgtmQH+iv zgp4>iFg)|sr}+=R_H(53F}--`7aez2^H+E{HXpwZD>d!cL9-Y1twrTN*rqP9mMWwx zrFda?pa1Fq`c=OB!I$xt)h#X^++g+eI%l^xke3CSFbp<&yet`#jZ&`9#ta9N(|Mo6 z*#wjCBW&6NMRlD8lYNzFR9;G-aNxT{C(KJq+reT~~6xSc^9F}pEklBT@8r%`jqY=6eJ!+qSdhR4S@z+~<2 z6fEjj%N`XFM8}5|4(P&Zf}wN4CrkCU7kLEx9@Q5Ml<~ziL@iVE7(J{WwGPHsi?2R^oHjkgX51HsCgpx^f{31i^4I|X;&59=ph&iUug^7NBW*5|C^ ztU`44uEoV4)x-!zMCID3W0B%Ga2;2gs6uRTQELpho!#ay|M?%{HFw{NwU+eRYwTaR z!TQ}h40qO$&XK1Ds-Lh@7$3Wk5^9_>_~PQTTc3fwE*{ti9b^#WMk?6Y<<_A$pC212mCqv#L( zNvfJ)0B6qmP}so@$M$J>{hM#+e@uRq|K%_Kb#|{_F98%5nJdE?0m!;-Zu?W4*(`55 zJja16IJ?8wX*JFg$BG|$|9g4OYwyRcB-rtc;)UyMJaju7_nkov6ZFnHQ9mMzgfBXG zdV`gF&yZ{mNCq(*{S_kFR90TxcCXz&7y6W5lxBiab1!zJpfxE2pVh*#Zh1d7c~QYR zLWQ~G8DHAg7rz^?HY!-%jJfU39=G11=@0yxN2LX8n}V$!*xrE?Cw*2$lEBWHGCo;2 zUh&ZF{5OB-d(c|@q9@jr%6kR0B?ai5TL4#M1eDR9<_1OUi{;>ItgXtaX3HvvMUnH) zZ+tD^{7rAbWd*Juw$%4geeN2TsVdks_tAbX6tLrXt1xs$EFaZnir;d89_PlWf8>t)vON~ZUCgg^ z^1m-~%lIf|IyUrrf-~nNC%61o%9Pm=DuR_YII-mu51kWioDgK0FYoluZ+SDXe(kHs zi@YxG+y+(yqGngz3WUp`Qb#gfP&GybN?v!XH>O}?W5D-)&o|H;O}%gC z(jIKC5S`gX^kc-vkmCGR%xH>Gis;Nqz|(3&*skimK%i=)B91juNDlT5hX)oRVK5X7 zhCVa|kf)*!Xs@jKxuP^AJ@3ZY*zEE9e*e3O;}}!3n1Q9m=PGb@9j!h_S+!F(bF~51 zGH@+)wrglJFLK`S#+Py5ZKp7MBit;*r3L!VQwVEewMTLOD#eYl56Z6g5o-h3J@Ntb zoek8)8F$=$J4KPz zVzSc1!rk3MaD_|Tn$LYhrMGO{QjypeE$#1JDVL2iXf1fhJ02n$&#sxYIp7U%c@3^C?p%q=MJ3)Ha|7+qmn!*-I!l$` zROu960dme^jA3VMl?U#>h2q8$Zk{0q32J@lk2My1?GQ7Xdw@xa>?g>R>xk`j7{m}M zG_nM*`yMoj=1OQV!&Z; zSe6QkqA3pi@&{ftBr$Y_S+? zxb5~GPOkJRp1OqW#fTFtkmiU;;r1uk=^O{>K|-{(hCFl9pMEmOr8y{xSRVoc8A;aS zguYbd4y?^dvl)>~&^n^pq-r53ywo$_Op3NuDrml1rjLcE@1*D~h||yu-#%aHq;1Ys z0lf)&w+s|Pn^8-UOR3OWqh*Y>g2T~1S)L*g^!u9#Gh|~k=Jva^&rZ$!{h7l1ypqI& zM~G(W8hEf-<9EP~6VGrvr%$tSVv}o^uA!9mmY}e$dt0o!(^$kokvyky);eGC@usZR z{NM3`ILqy~o+3&MX7e0zakQLC6v7;$A%~8DwIY|5t09k>(b^wJzBr#%Z4TcGB znxVDgmJ{1-#|h&!tu2`>n`4R`r6Ob*D{I7dlR+A$E4%ueCQ&dnU|c=O?FrRQo@nD% z1QRXnof-bDGN+E`zfzrs{6*7Xqv9 ztIB8;(rT8>`;f7^ogYs6%9>325lrU@Es=4A+Z#haf?0+g&wO=hCBYb6QIJ1>6|w?3 zNDvzX=*PGlBXExRjvZe8riXazGmmrW;uEAU@>ET!6BM@H&K7Gd^OAQyF9&#`8sSTN zh=sr16q>HP#7lD-fN6Q#Ll9=w z70Alz;+n}57B^AaF9u2Hg8sk}$C~Y(Q(iQ7oPq5%lA&^6{;k)$`2?zkjMhbPEfYln z*c^Uo@GL_l5u`b0JVnMEh4cy)iB?P`wzj(g$O`!oRhKIJp8APQB_b zzWbTCbLoQzeB$sbnT@>ucC1(OCLlRh42qZb`ysDjjUuAm;{`7syCsW^;<^oMdf? z9nY|Nfh()%*xcZZMa3Gq(({hOg9+l~su!I%#+Ye}y>Z0-ue^sJ=R3INQy=BSSHHqF zo0B2R{V4+x<~XL^{-|n?#c=g`bH8QB)aLdw_slZGVY%>Z7Mun#EP*eD;HqB+I#`ej z)U9s>O)ZdWJ*EFn6D0$-PTa>E-}GJ{eCSnd?LD z;)=5HniR-bBYH7zp819q7GAgxN+Jgda%Y`llCr(A!FRs$jlA~k9b6jk^ZdaL_U03e zH5A4$ND@vA*XYLyO2|4Fsj6)uLfxy==&I12!%-MRUKp%(7;8yQ!6=)tJ3Zw3e8j}& zOsrw*Akr)JqBWH2p=BQ-`aq13F_DgtLLfz|V7MySIW7E#3q3*HbLh%^ zD;H=<=>?4tqAuZ7Ex0v>|ICy%#40PZi8I&J^>}c#0^YTOzPIGK#DnG8*#RS-+Y0Y}>#vwR|+BxCP;-eYK`hcC6o#Eu&+dPn2 zrn^VXrzwTA%*Jz~lLOY*R~Za@2jHE zaJ7fj{`^WSbfi&{Ku5By-Qi1J#yyFS1WI~iDbEe_X~AS%Fh0u3QiswJXKv{;Ty>oP zs%3q%$L)7&5Ei9Gtv_X{BT4)<&r*nzfRCyi zuU6bK{IaS(U9or#9t6T@z|>py9?Muk+H4iFS#V@Fq9dQAh)E3AQA|>7;kRayQi8GM z;}jwNK8;f4NUcyS1MJZR@&dWBiYtu(nH!3Wd-S(fSzp~|zCWRV`wo$k*xB5NrSbx| z+CyrE&>Eo?mBkiD0z|cUN>w!8IWXn#g+L4v^4%H5m#?t3waJ^+YdE!X3+IpaIX^yN zGP%s!=F92n6CMzD&d%C8+h@1vt@ZqZ;?$zGq_-L~T#-a^>Cu)R3|fo!HZD@sgD+)} z_2B%*?en<@!0Ni-#J1&$M>CF&1h?Ix84O*$Elkpbl{HtE$Mj=S)ngTSpk=97|t z+!ZeHK;(JJV73u&ItMZUQYEHJGDou_hER#u{j6ecgo z52j!pd6w6852Wxj*cnxBmnVEBJ}I$jjvLRA!ya}tMGpH|W5{+7(1{}6-XOhtK>v;% zqKzTNg8oFtR&Nw*P9}=s#SPY;Jzyg_ z#kt9x(KC-QDCTUfoa1DCFRLrtxI|*CLukQpGxnlS3xsfWc_gI-E31Meq2BsWmWzJF z5*!u4gednBQi7FL54OUu_+#^=q{t%nZupW|aV+Q!TR06iMdz%RSf^0dA&o5SvkIW}n*lN}P>C-rYQ|G^5}{WIn0e}VX&d&i2i_7Ds?=g5 z?O7?&h)DYpQe~T!^wzB|!6yi|YZ1PPNjcSpl88v+<^{H}40eVLcUBpF=_+f{3O$ve zT*6v%f+!l&+gxL?)+6r642Fuqiex(RPT>APlof8gmt1S;CxWm-dlWAJtcX?mkd7`l z4DIzmulYgO5!~}~$@9;6aICC(aTaNRTxDp-aRVxAz0)<%Md^z0OlATdtYTE98{ieq zjyr9k=v?b>7hHiD1Tfk`+I-Q@C0_(rz+mNm&O%5oUA;je1Xzbq64j5$j^<_Ol?}2W zjKgV%Q+`_(W&KW4uG|5{7s4%%FAP%qy(H~C_4=BU*!?41Vd{OYU78|rh+)6n6tVL8 z2t=eH(a?*0`E=>qNMRu@AoGfd$So8Wml@nB_4{1kv4&cWIe2uRY+i8J#x|cCKgT$~ zj$Y9uafB@l^LdUOrR-f*Xq_+^$VPcC*CtjwZPaz>1M99RXliPNB&+?rv(^KvGQ!q3 zeHQBe4X<}?Y&Pdjf}(R-?txX53VfDUBGO;8D%(_P5Jl09be6)=;j~sWs?S?X0&B4t zfGc#tcp}QR!xeQkn3gznOTkqo&D9HUh2-MZ114$Cu&hv{h6z$lDCT)9I4q1$K(pnp z096q>rDVCYnZOypyuGT>QwPXx=|#3FEstSdV4XuEJPT^Cln5zsu{V;1bzpQGV0l%? z+ckA10XdA3CkKf29>t|GX@J}S?R4fshVCkrE;$nsB+8WG;J6y%RCSekZVB{JCt@5MqmrUeBWV8 z2Fi1H7zyMbjD-PT;jpob|~hl7n6e+Ii6#t4rfcI5csf>lsKz=d2s8n z5w+L_;l-6Hw~47lrx3Lm^_G_?Sk_s0 z`k1#O9a6PD`4yO?YIftO3<79?ptaBG>JL4DyuRf1yvlH>jEAZSun8_3duNb`4+Ld*^Tm?2Muu>qEPqyum2z;A2pawEqUgMVh=1`xhZN_g=pzrReZ5}?UjDlfUkT` zCxYm(E#4}TTURi&K7vpc`mH_}q5^Aa$!gD_MY+?+dP>9$ZaT%bBYZ z-wnVdz*K`9H_`GiyI{E`>6Lr@j%m&>xQ%tpIKCJC}ODMgdOx(%=5`@+mw!N`nWN24N+_7G4j_OuezY z-+7~CFGsy)pUwOpO7Uh7p#|AN&g38^D-23S6fOhnm>!JjsdakmF*^1m9Z5l91zBc@ zgx@y0$Q^lJ`Kw9>)Bd$*EbnNQ%KO6*t^}M|*z#DGo1?1CA?Ky_Cz|@IRjj!xPwrg! zY+O-FdwNsKJ6whEOxN$jr3(i<`NS7Jd$rQ%+o7Pw9BhNj)np|AG1kCQwQ!@1qBSIU z%#txFB+gl{g9%NfA`Yi1zy0~AxaaJf>tE0wT+HL=w6b)TFWS}SIgl+-x-D?O57jMjeDw6V_b)np}c>``K^BP**9 zO6kRG!fb-=kY0)zCL2mXCJ&oO)G5yMj>2AhgEyf6M zsjlc9l#L;B3|D$gGt1~`&NQ{e{g}Athc<@ zU#*<{MR55mRJAR{o_4tFl*`W=zsHLQyK5^_TC|Iab-~sBF~9bSFY`y={RU*DFu6ka zV@zJun_oF0YL8&cziTT8p5HUlx>Bcdw)B%#0buVbm)7fJ9)#eOMTin?RUy(qY*yhe zA$+V-MhZQQQ5!wvS`YdWqNn{9(AOqR9^YqtaZZ0TqL>+0RS$t9b#rc6+oni!T_S};!?YGTllM8`hRofozdd9tlz8MDlbUwziFoi9_q$Vj0WYy+V=r}yYr zmTTjs@=jQ9fyy3y=662N3(r69Glyj(BEwMOGH}&wR{A}Cz+VF5h6Z=Vfi>31WvsPy z{a`f{0Hj~p63LiC_xQl)p5cx6-oit7oyL`pPMt)48#@6u^Nn`aB8*+^9OL~~PCD-^ zGv&=mu(H$vMK!x+qoWy7ZwBgPj(FCR5|sZ;Y455~y%@a`qgE5dAcDjn??x%bwJEa; zW9EAq$+{w5jX3zygmasl#FrG~;u>=^VRdDTe4e9{nDr9_lGHGrS<>85?ooRrO(IX(3koIZEkJQc##O;7}?5vNZdp^PkH`Pp*B+GzM^(Yad#p)&7uGgrjnk z`>eo70h^uCmckmgGdf%Bc*-gp_kQO1XY7keQv$?TFQZ;aq z#e$UN%5W9nd<9*!)HoHBILkO4^Yg#;1^(>%+c>#Cz@-wMc&~Rw<(6tzmEtipPMjBg zO|FTk3-EeN(X)XQ0xK$$yX*`p5W;##UvOWB0%sllt%PJPV(;k*v5txR8igWO8yp_(qtpuV<_hy^h82R|Aoe<73Tfsr*5L|^ zwGL@25m*^46{rN#l=_zxC|T)|9#j<|MXC7v_lx^rcE_mtwqZhV)KvPNfA1p!TEl#9 z`QUGSfXkPjMC-UUajUb1;A*8z*WhZHREh{8L=NJL26q{l6*f;6T@-EB5>*yO{eeT4 zMRK)sIA!TaeIC1Tz(4=+BfS5cUrXH6*t|gZV_d-Jw(!j2T%CLrPKzqqQ{R9P(x;Y) zaG<=-=E@xed{Ml{SLS57ORn^TZSn#)66E8&={V!Cxy57-lY{On`ez2{zG8HK!t6#t zvZ8Ri4(kj(*=M!4NuC#ElbqgA;HDYYLNbi$0mfQvURri#S$AujRZOA+mkeKnRZUt= zqu{$h2WtSg_^7PJhW(uLE*_^mYbXUe@?e`yq2KqV7k=jxpXGB8|GKYODOJf{^2Nns zP;`#KQdP1C$T_&n8n^-+WqbwdQWp0p-EWh>W3!GXR@V>Xa z9NCL-#u4?C21deQo%F72pjvw8T$#fe?&v1UjhL#Atv2nH5`;yTCRLTqEy|3{+I>+t zTxuw$IfyEi$NOr0U4Ma49$5Xm)}gSBpFCjn{M=`H6mU43F*5~uy2oATR!DO|OVZ4e z8^}yfW}r7r==T)}%RKc=CQ9*9vr)OljkO+Ju6kO54VfxX?)~IxUWcKj)^G*xjW7n< zfXh382SY(&1YiFAGyK*Eex58F6X~8$kUR!lO{}raTp_pw5-ocOICOAX3n8yLXRa0| zzs)*}?t}-9%c|(^Ra6&X(Y^t5=lE9-e}zO!-u;@pky21NN0dYq)l|dy{j%|%L1$gy zC<$*!_?30Sr|dd>@49kNO51W!%6cM*S`V|e?gOTAS(Hv%JxJ)E9uS@AkzSfGzdR*7 zDtrYg2h(xR`Mv98HfNq4FfAsmtZp$%Ey+NW8^=5~WCcv;jwsP6tx#IjVzS!au-^T{ zx>8hCzzP_x4vhuu75Kzz9aOY;9FbBGl|b^Tcb-`)(P#=E`(0U;b|M4Gk zuz#Tgt`K02Nm^&Dc9^RkE?3s=l|2yR8gN|;p^gy6`NHIHC`>`5k_ErV@m8fM_c18b zP^zTFa(CEyKIIpF=P7J%_|{k6;WtJrEGp8i{C8u$$s?$ChIOs0FDzQ|6hd3{$ph#R0;QSPZ(g3HQAoZ8BTVN2^TGyp`?S4#Q4 zx&gA(R*^Yh#Yj^-d;0^)WDJiz`XnFz;6Gu1@2hB?_;l{7=x=BITdoM&%+>xLluS@E z26)yvd#H_>6hi37tSiV(#!#*XN&L&K$^dp@;$gisi+X@TkQP&Z@v~p$U^3%79=L-< zDNOEAQu=fnTRL8`I8*KiAe zqY_Oth$vFWbT>z8f$j?~Ts+{(D_58ona}%|Fil5X-`!>N^jXZjV3rmrt&vie+vEAg zEwy=HsTpOvq>$dZT#LGf!ED)1KiJ;y=Sr-~a?>vXm`d3fO;jMwDo3Yhthn4?$yyJr zC>C6~T=3;b9_Has|00vgRU)0Blx~T}u$obOm$l_OR|7JKR{he75^0Ob3S8Bv)d7CC>B=jPpON!I@^2o$#HE8V-}~U5oL(7{nWA)b`qkW3 z$i}x*l(-P#D&c0vSl2Wv)Cm;#O*h&d53(|{HF|D`is~+Jdnf1(S zm@EVq<@o|Tp~J$qbbB~RL2A$gNwlJg2AX_UaQW$dE?zril%*s(g1KLP{`H=IZy4B8 z_pCmvGsl%6iu`i8OBZIGf96phf9%)T+j|lzWUX_lD(+jCEN_FR23PP{4lZ95I??MO zxaOSuvcRE*PzWJjFeZO=o=wkX#hiXTTn4KJWxt!wBZVxt`4Npc3dE%pl8ciW|L_Z6 z<#i{o^G&y&=Jt&>v{DT)N)h49qn1m%f^wa%6JHBsiuDHV;p$e(Q#65-8h8?kiWR!A zK{(RGjM0@ThX)h(rZZ-RAyJxHkufdi%&lM3HOlu`>z_jcVuuu3Y*G z&p!LxT)pyHtevA(;$0D?NUZiFssY{+i?rMl%R%Pzz$}qoA=0Zj=N@&=zTf~I=>hZf z#!LwPp~7arZ;~F;k5>po3d>@X%a_b|bg<5%#43j@E#`c3cf^-&9P;YTRo--Jo4YpF z8T68}@%2SYuY&*UQ+!H8z=gmQb$df#b=j*vZD}A_XR))4`6R;>j-s$+#xN-ic`1%E zW4JuoY?K94r3taVVfK}<8K2yS$dvN+ax|O2LNfR6$7L1Pe*uC~N7cc$}dp91ZFjJI_h;+YP zrzr!G*p^hwVvF59i56$LfA*piD3RcBzok_&n`Z~U`(Js#LHvCcL_$XZh4oo@wt#x%mtIwo%q{cALGOkTWJU`y&smU(S z&kq>8yw*sROc*4mSs&a(FS!M!HxMd82oEk`rlfywjG@R=rqcrs_MhX%-jnR_KgoQ)3$8%P2r0{UmEuqauAJ5ctPQ77=W2R? zQU7X6Hc6q`UVk0^cLF$1pGtcrT6#CVtdb)|lPys( znXJ|fgzF8f)z$btIE~3DiV10Y$ZURt(dYux*%juqUGn@8Fo<$v3?*X^%34+FScxGE zl~Q0)=ejw#s$-I9lO)=}IrkCk%$F;PrvU~lXE-`IpGv8J!Pw&6H%3?btGx}v9Q|c` z6?K8-Dp28&b?BlJovQYgRr?$%p(w2O^sRhd(8A`N&kDXeKZ4zTG>(B1tZK!IQmpBS zfz}MPVxS{pd~TYLfe9k1D6Fr%SXhg(kULB69I464tz}YV%&qmi*_wj6HO!19ca%d~uxXpQ61XNnrEGZ=eBKHq0LdlFCxnIJ`iQazL!AVh>#1Eh=)GC|3RCcjPB zu8T?>+A=n4U4}7paE81XQ<#*Zm|}_vMKQscIj-EKyxv+->GJvkTUNPQqDuyWT&Q<3 zDW8F&f1I^gJw1siR(iK0g`PQQe^CmZ)rp7IkKb_L4JCiu)?%A`nF{;jqBG6-84 z0ag8~>R;H_z{sz`FKVl}N^lHcH{fbkw!50eY*=QJ@~^G#v8oJMDae5%C461^lsYYB z;k{_Lw-R%;sPn6-!}c~M5|)|M;rPId>0E7aRdGqrWc_%D?e*6pQ2)u={Cg2%^eZ3# zBMpNA^rB71vlm8EMt{%P;_X)tpI=$&ZJ@l&!`Z4X{&)$ziBh3)8kQaeMN0TTgJH~dxAY4BjO zPWcH_O&%4tu>zB8{?>id^jK@mEAM<0v<;gX=uL|$?M;d^&|#}^f-D7B<-x5kzf_@b z7yk9dzh&5)1{D&T}n%)YL*Ou zZWln`zSpo_%|viDu)3~*;Gzp5r#9Hadk`$lA40%%Vcy`GTj!$!5=B0lza3#??>w~)h@vL*^XQYZ! zYv!#hwvuq4x%eT@ZM}xc^xD-p8s@o8zd0-BYGb&Klu@~@;-aG1I09G%SFjSH1+G@Q zZ8LC#+YadPzmdhtdI?x&Nu z_iIIw^DCeD;o|`YaCz?$?l|=pq|}cYTiiLz#t#9k4!7F5?ZM@%{}znj07TtcS$9M& zQtA^mgV>B_on27rN1-Jf4jjQ?G~dbAZ^OFx3&SJYV(~bhX%9l!-k_~P7_D-g%Chh3 z)z2bmQ+O{lx?gK$yI2(6?SS+$r|2?p9k0kQ-)x~7Iw1s`!+Tln-wN zkN@C8A4!%xl#a=Z`9uiy)4*pgA3V$M;d9kkd(?|1)E_vZTHre7^ICibVytz;&99aY zXuG3Ze6uhJ%c81vvpH_omo0TS2iG$3+*Sc@t^pp`uA3Pvp_AT=Q|Eu zA;=sny<1ru-iZ+6)8PJ+5OQ)%4=*wpz{Trd;Lg+E$Sl3SuVwP2wZ%Ipv%QU8G^C%b zwFjg8PqGLijsX`h30!U|U^(XV1xsxy0G7;F+)csNT`;hOXoC=Yqbkt7e*N3#NEe*1 z%CKnK=@!J-BDm@g%AB|^xE9?WZUJyN2UmBXhskM9u04Q~(Rt_GeNo} z(g6l=;rheeb^06l;xj-0+|I^>FBCTWhVksiuon;M#jC- zi13{57|<;j4=)C;MGI;PEU2p|FIosK%W}c$3|HYC1Fptka8$d-JJGf%yDS<4lNPwz zeZEXAw$9^@2WEGY4w6%Bt-TVhl55VnAM5v4{^LBI@UJ_1SNJ#j`6~myFHdF`C{OCQ+Y$vI>-n{SqJ@Cw4+{(y=F71F`(K$BWYA9<6qC0DZiD=4xxO zx>kDwg7yGa=jr;I+SY3$Ds9l7@OpJ?MZ2WwGT;NPbqWjMT6(YUB{Ns|xM6a-1Xpj@ zIr|g2ng1(e3Vu0oNi2KZ6kq^X_P@l=`pf9|Pd{3i$)&>N?>L%XA8HxVkJn1}3Oh2? zyE-@0(N&I{DaVO9w~(u|c+CancPS79F;srh)@5j2ONiEKg8Q;8n+WapfDg0>jM$R1 zOGKIrgzhqMxe{Ei3*w*;f}HxV1zhbubbw^Fe=8@~9w?d1{=_^R|MN&C?!~|bF9Bcx zSN0!ad+i>QXyZ|1=g$~x-!z)t*Z{*YS*v$LtDT20rXzI*v^}uPL~0vcT^~}tWIX`F zLU^WozOG0Phbid%#rpb-=U5b9i+StebwuahTN5FeunT%*>(%k#3LUm>lcU{#%b2SU zwK<29F&o2s*j&92C8Ot@b3c}5^M4iT#Qn-k02dr%MJ{`M{;R(L%-ti;{?aciss0ey zhp+5E%QIIWVUixU2WZJ~gcG+t>w`|asTE2m_`1k6&3JT~a=B994d!+y$FX9u1^D)i zZ`UhbbayPYtIM)WbfT7sZ;4+=S0Qh2H`oC0=1$b^^}Q2yYwgvnuiT9k>S62bANs#P z`iFm6De;oQ1;^QzH+j7I<$sdt{CSk@-({`&E7rO1=|%nI)W+?cTsei5x~02>ofvxF z!<(tVwM=qollJrMJoU+BsHw>t(p7YJ5jT@)E%n9(T;=T>u@&TpjLu(cdi_ zxEFPWgksEvSEXh~=SmgpMZnd9mTScB;&m2*6(09u;h(A>g;4aOE!GBi(2uteLY#Nb z{dMc?&k4ljmp=9bzbl-)BpqzoTiXF0fdQE+kWcP|dEa3Zum30u|qCb(wxJJ&symanTl_;n*1AJ@V) zyN~4zy?EYIlsk(Q5xsbejp2Q)5APw-Yh!T#65OAhr{>p`6iYqyFZJ=eX0Yn<+PnV% zhzNn`Sy#LZ_%U$rP*V2>@hWTm6AY3SB9*jt71!2fUJ66KnD}eQI~%3%IObY*t?qf- z=MDzZh2Jh`v^KaF#c1>X80Mm3u7(wF=C|vz{i5CqKF+IUpI&@|ete1~+CU0D1NRY! z`#a9L-w=rO>eY2V`t*1HXW`>N8(`JrjW7EX6!{)PL~G#Qlg?+0!0qy42v+{sw#BaPV|?bUk_lS%iS-(>UMJS8D496mI=GKI=YGMu;y0CyM$`0wU;p&~ z@Oy%b-!p(!kJsP(KjPdCq>K~iiU)vi2Ki3s+yfwXgpiRG8m%;1mVZ^`bIAP?y2Ta7 ztw%?EbcTZk7Fi-X%Ryfdc&>A<1>LV!pyKq>e_W_4{OdwNhr~Jw%{@Cv+9)8aO7Czqf zs{e`4JpC)&f9u!vt<7%*_aN{PhzFeG4g@;_)FBXDXOax>}8#=!$-@DdX!oYoaT$Kq@rRjw01j1A! zh44$rov+U&XF_lUTm^9+cntWW2bX){*6nxApT77@{QAQ`{$C6O|3v}TeZ1j;AHmKH zQQXtknstZU!nrdDP7CB25T}ITB#09Z)&*h}Tp#EQArjAiB5}^ivS*cFF)0MTf&&O8 zL{J)J*0t|>u2vLAY!OegP$O*VZA>=-|>maTo#1(!6vYU>t_rTlT0000bbVXQnWMOn=I%9HWVRU5xGB7eWEif`IFg8>%I65&hIx#aV zFfckWFs6NUf&c&jC3HntbYx+4WjbwdWNBu305UK#HZ3qREig7zF*rIgGCDCcD=;uR zFfi93%sv1B04Q`tSaf7zbY(hpX>Db5bYX3905UK#HZ3qREio}vF*77M`q002ovPDHLkV1j1yS=#^r diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d2487fe04bb50689fdf2e027823eee1a0c9d86b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37065 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 JPDHLkV1is5mn{GQ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2f..00000000 --- a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b..00000000 --- a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7..00000000 --- a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/ios/Runner/Base.lproj/Main.storyboard b/mobile/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516..00000000 --- a/mobile/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist deleted file mode 100644 index f3a46535..00000000 --- a/mobile/ios/Runner/Info.plist +++ /dev/null @@ -1,75 +0,0 @@ - - - - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Mobile - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - mobile - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsLocalNetworking - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - flutter - UISceneDelegateClassName - FlutterSceneDelegate - UISceneStoryboardFile - Main - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/mobile/ios/Runner/Runner-Bridging-Header.h b/mobile/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a56..00000000 --- a/mobile/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/mobile/ios/RunnerTests/RunnerTests.swift b/mobile/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b1..00000000 --- a/mobile/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart deleted file mode 100644 index 25f205da..00000000 --- a/mobile/lib/main.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -import 'package:provider/provider.dart'; - -import 'src/providers/connection_provider.dart'; -import 'src/providers/workspace_provider.dart'; -import 'src/screens/server_list_screen.dart'; -import 'src/screens/pairing_screen.dart'; -import 'src/screens/workspace_screen.dart'; -import 'src/theme/app_theme.dart'; -import 'src/rust/frb_generated.dart'; - -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - - await RustLib.init( - externalLibrary: Platform.isIOS - ? ExternalLibrary.process(iKnowHowToUseIt: true) - : null, - ); - - // Edge-to-edge, dark status bar - SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: Brightness.dark, - statusBarIconBrightness: Brightness.light, - systemNavigationBarColor: OkenaColors.background, - )); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - - runApp(const OkenaApp()); -} - -class OkenaApp extends StatelessWidget { - const OkenaApp({super.key}); - - @override - Widget build(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => ConnectionProvider()), - ChangeNotifierProxyProvider( - create: (ctx) => - WorkspaceProvider(ctx.read()), - update: (_, connection, previous) => - previous ?? WorkspaceProvider(connection), - ), - ], - child: MaterialApp( - title: 'Okena', - theme: ThemeData.dark(useMaterial3: true).copyWith( - scaffoldBackgroundColor: OkenaColors.background, - colorScheme: const ColorScheme.dark( - primary: OkenaColors.accent, - surface: OkenaColors.surface, - error: OkenaColors.error, - ), - appBarTheme: const AppBarTheme( - backgroundColor: Colors.transparent, - elevation: 0, - scrolledUnderElevation: 0, - ), - bottomSheetTheme: const BottomSheetThemeData( - backgroundColor: OkenaColors.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - dragHandleColor: OkenaColors.textTertiary, - showDragHandle: true, - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: OkenaColors.surfaceElevated, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: OkenaColors.border, width: 0.5), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: OkenaColors.accent, width: 1), - ), - labelStyle: OkenaTypography.callout, - hintStyle: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - filledButtonTheme: FilledButtonThemeData( - style: FilledButton.styleFrom( - backgroundColor: OkenaColors.accent, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - textStyle: OkenaTypography.headline, - ), - ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - foregroundColor: OkenaColors.textPrimary, - side: const BorderSide(color: OkenaColors.border), - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - textStyle: OkenaTypography.headline, - ), - ), - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), - TargetPlatform.android: CupertinoPageTransitionsBuilder(), - }, - ), - ), - home: const AppRouter(), - ), - ); - } -} - -class AppRouter extends StatelessWidget { - const AppRouter({super.key}); - - @override - Widget build(BuildContext context) { - final connection = context.watch(); - - Widget child; - if (connection.isConnected) { - child = const WorkspaceScreen(key: ValueKey('workspace')); - } else if (connection.activeServer != null) { - child = const PairingScreen(key: ValueKey('pairing')); - } else { - child = const ServerListScreen(key: ValueKey('servers')); - } - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.02), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - )), - child: child, - ), - ); - }, - child: child, - ); - } -} diff --git a/mobile/lib/src/models/layout_node.dart b/mobile/lib/src/models/layout_node.dart deleted file mode 100644 index 2bfb449f..00000000 --- a/mobile/lib/src/models/layout_node.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:convert'; - -/// Sealed layout node hierarchy matching ApiLayoutNode from the server. -sealed class LayoutNode { - const LayoutNode(); - - static LayoutNode? fromJson(String jsonStr) { - try { - final map = json.decode(jsonStr) as Map; - return _parse(map); - } catch (_) { - return null; - } - } - - static LayoutNode? _parse(Map map) { - final type = map['type'] as String?; - switch (type) { - case 'terminal': - return TerminalNode( - terminalId: map['terminal_id'] as String?, - minimized: map['minimized'] as bool? ?? false, - detached: map['detached'] as bool? ?? false, - ); - case 'split': - final children = (map['children'] as List?) - ?.map((c) => _parse(c as Map)) - .whereType() - .toList() ?? - []; - final sizes = (map['sizes'] as List?) - ?.map((s) => (s as num).toDouble()) - .toList() ?? - []; - return SplitNode( - direction: map['direction'] == 'vertical' - ? SplitDirection.vertical - : SplitDirection.horizontal, - sizes: sizes, - children: children, - ); - case 'tabs': - final children = (map['children'] as List?) - ?.map((c) => _parse(c as Map)) - .whereType() - .toList() ?? - []; - return TabsNode( - activeTab: map['active_tab'] as int? ?? 0, - children: children, - ); - default: - return null; - } - } -} - -class TerminalNode extends LayoutNode { - final String? terminalId; - final bool minimized; - final bool detached; - - const TerminalNode({ - this.terminalId, - this.minimized = false, - this.detached = false, - }); -} - -class SplitNode extends LayoutNode { - final SplitDirection direction; - final List sizes; - final List children; - - const SplitNode({ - required this.direction, - required this.sizes, - required this.children, - }); -} - -class TabsNode extends LayoutNode { - final int activeTab; - final List children; - - const TabsNode({ - required this.activeTab, - required this.children, - }); -} - -enum SplitDirection { horizontal, vertical } diff --git a/mobile/lib/src/models/saved_server.dart b/mobile/lib/src/models/saved_server.dart deleted file mode 100644 index aaf2e968..00000000 --- a/mobile/lib/src/models/saved_server.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:convert'; - -class SavedServer { - final String host; - final int port; - final String? label; - final String? token; - - const SavedServer({ - required this.host, - required this.port, - this.label, - this.token, - }); - - SavedServer copyWith({String? token}) => SavedServer( - host: host, - port: port, - label: label, - token: token ?? this.token, - ); - - String get displayName => label ?? '$host:$port'; - - Map toJson() => { - 'host': host, - 'port': port, - if (label != null) 'label': label, - if (token != null) 'token': token, - }; - - factory SavedServer.fromJson(Map json) => SavedServer( - host: json['host'] as String, - port: json['port'] as int, - label: json['label'] as String?, - token: json['token'] as String?, - ); - - static List listFromJson(String jsonString) { - final list = jsonDecode(jsonString) as List; - return list - .map((e) => SavedServer.fromJson(e as Map)) - .toList(); - } - - static String listToJson(List servers) => - jsonEncode(servers.map((s) => s.toJson()).toList()); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SavedServer && host == other.host && port == other.port; - - @override - int get hashCode => Object.hash(host, port); -} diff --git a/mobile/lib/src/providers/connection_provider.dart b/mobile/lib/src/providers/connection_provider.dart deleted file mode 100644 index 8420a5b7..00000000 --- a/mobile/lib/src/providers/connection_provider.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../models/saved_server.dart'; -import '../../src/rust/api/connection.dart' as ffi; - -const _kSavedServersKey = 'saved_servers'; - -class ConnectionProvider extends ChangeNotifier { - List _servers = []; - SavedServer? _activeServer; - String? _connId; - ffi.ConnectionStatus _status = const ffi.ConnectionStatus.disconnected(); - Timer? _pollTimer; - - List get servers => _servers; - SavedServer? get activeServer => _activeServer; - String? get connId => _connId; - ffi.ConnectionStatus get status => _status; - - bool get isConnected => _status is ffi.ConnectionStatus_Connected; - bool get isPairing => _status is ffi.ConnectionStatus_Pairing; - bool get isConnecting => _status is ffi.ConnectionStatus_Connecting; - bool get isDisconnected => - _status is ffi.ConnectionStatus_Disconnected && _activeServer == null; - - ConnectionProvider() { - _loadServers(); - } - - Future _loadServers() async { - final prefs = await SharedPreferences.getInstance(); - final json = prefs.getString(_kSavedServersKey); - if (json != null) { - try { - _servers = SavedServer.listFromJson(json); - notifyListeners(); - } catch (_) { - // Corrupted data — start fresh - } - } - } - - Future _saveServers() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_kSavedServersKey, SavedServer.listToJson(_servers)); - } - - void addServer(SavedServer server) { - if (!_servers.contains(server)) { - _servers.add(server); - _saveServers(); - notifyListeners(); - } - } - - void removeServer(SavedServer server) { - _servers.remove(server); - _saveServers(); - notifyListeners(); - } - - void connectTo(SavedServer server) { - // Disconnect existing connection first - if (_connId != null) { - ffi.disconnect(connId: _connId!); - _stopPolling(); - } - - _activeServer = server; - _connId = ffi.connect( - host: server.host, port: server.port, savedToken: server.token); - _status = const ffi.ConnectionStatus.connecting(); - notifyListeners(); - _startPolling(fast: true); - } - - Future pair(String code) async { - if (_connId == null) return; - try { - await ffi.pair(connId: _connId!, code: code); - } catch (e) { - _status = ffi.ConnectionStatus.error(message: e.toString()); - notifyListeners(); - } - } - - void disconnect() { - if (_connId != null) { - ffi.disconnect(connId: _connId!); - } - _stopPolling(); - _connId = null; - _activeServer = null; - _status = const ffi.ConnectionStatus.disconnected(); - notifyListeners(); - } - - void _persistToken() { - if (_connId == null || _activeServer == null) return; - final token = ffi.getToken(connId: _connId!); - if (token != null && token != _activeServer!.token) { - final updated = _activeServer!.copyWith(token: token); - final idx = _servers.indexOf(_activeServer!); - if (idx >= 0) { - _servers[idx] = updated; - } - _activeServer = updated; - _saveServers(); - } - } - - void _startPolling({bool fast = false}) { - _pollTimer?.cancel(); - _pollTimer = Timer.periodic( - Duration(milliseconds: fast ? 500 : 2000), - (_) => _pollStatus(), - ); - } - - void _stopPolling() { - _pollTimer?.cancel(); - _pollTimer = null; - } - - void _pollStatus() { - if (_connId == null) return; - final oldStatus = _status; - _status = ffi.connectionStatus(connId: _connId!); - - // Switch to slow polling once connected, and persist the token - if (_status is ffi.ConnectionStatus_Connected && - oldStatus is! ffi.ConnectionStatus_Connected) { - _stopPolling(); - _startPolling(fast: false); - _persistToken(); - } - - // Stop polling on disconnect or error - if (_status is ffi.ConnectionStatus_Disconnected || - _status is ffi.ConnectionStatus_Error) { - _stopPolling(); - } - - if (_status != oldStatus) { - notifyListeners(); - } - } - - @override - void dispose() { - _stopPolling(); - if (_connId != null) { - ffi.disconnect(connId: _connId!); - } - super.dispose(); - } -} diff --git a/mobile/lib/src/providers/workspace_provider.dart b/mobile/lib/src/providers/workspace_provider.dart deleted file mode 100644 index 99b9f235..00000000 --- a/mobile/lib/src/providers/workspace_provider.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import 'connection_provider.dart'; -import '../../src/rust/api/state.dart' as ffi; -import '../../src/rust/api/connection.dart' as conn_ffi; - -class WorkspaceProvider extends ChangeNotifier { - final ConnectionProvider _connection; - List _projects = []; - List _folders = []; - List _projectOrder = []; - ffi.FullscreenInfo? _fullscreenTerminal; - String? _selectedProjectId; - String? _selectedTerminalId; - Set? _previousTerminalIds; - Timer? _pollTimer; - double _secondsSinceActivity = 0; - - List get projects => _projects; - List get folders => _folders; - List get projectOrder => _projectOrder; - ffi.FullscreenInfo? get fullscreenTerminal => _fullscreenTerminal; - String? get selectedProjectId => _selectedProjectId; - String? get selectedTerminalId => _selectedTerminalId; - double get secondsSinceActivity => _secondsSinceActivity; - - ffi.ProjectInfo? get selectedProject { - if (_selectedProjectId == null) return _projects.firstOrNull; - return _projects - .where((p) => p.id == _selectedProjectId) - .firstOrNull ?? _projects.firstOrNull; - } - - WorkspaceProvider(this._connection) { - _connection.addListener(_onConnectionChanged); - if (_connection.isConnected) { - _startPolling(); - } - } - - void selectProject(String projectId) { - _selectedProjectId = projectId; - _selectedTerminalId = null; - _previousTerminalIds = null; - notifyListeners(); - } - - void selectTerminal(String terminalId) { - _selectedTerminalId = terminalId; - notifyListeners(); - } - - /// Get the layout JSON for the selected project. - String? getProjectLayoutJson() { - final connId = _connection.connId; - final projectId = _selectedProjectId ?? selectedProject?.id; - if (connId == null || projectId == null) return null; - return ffi.getProjectLayoutJson(connId: connId, projectId: projectId); - } - - void _onConnectionChanged() { - if (_connection.isConnected) { - _startPolling(); - } else { - _stopPolling(); - _projects = []; - _folders = []; - _projectOrder = []; - _fullscreenTerminal = null; - _selectedProjectId = null; - _selectedTerminalId = null; - notifyListeners(); - } - } - - void _startPolling() { - _pollTimer?.cancel(); - _pollTimer = Timer.periodic( - const Duration(seconds: 1), - (_) => _pollState(), - ); - // Immediate first poll - _pollState(); - } - - void _stopPolling() { - _pollTimer?.cancel(); - _pollTimer = null; - } - - void _pollState() { - final connId = _connection.connId; - if (connId == null) return; - - final newProjects = ffi.getProjects(connId: connId); - final focusedId = ffi.getFocusedProjectId(connId: connId); - final newFolders = ffi.getFolders(connId: connId); - final newProjectOrder = ffi.getProjectOrder(connId: connId); - final newFullscreen = ffi.getFullscreenTerminal(connId: connId); - - bool changed = false; - - if (!_projectListEquals(newProjects, _projects)) { - _projects = newProjects; - changed = true; - } - - if (!listEquals(newFolders.map((f) => f.id).toList(), - _folders.map((f) => f.id).toList())) { - _folders = newFolders; - changed = true; - } - - if (!listEquals(newProjectOrder, _projectOrder)) { - _projectOrder = newProjectOrder; - changed = true; - } - - if (newFullscreen?.terminalId != _fullscreenTerminal?.terminalId) { - _fullscreenTerminal = newFullscreen; - changed = true; - } - - // Auto-select the focused project if we don't have a selection - if (_selectedProjectId == null && focusedId != null) { - _selectedProjectId = focusedId; - changed = true; - } - - // Auto-select terminal: pick newly added terminal, or first if current gone - final project = selectedProject; - if (project != null && project.terminalIds.isNotEmpty) { - if (_selectedTerminalId == null || - !project.terminalIds.contains(_selectedTerminalId)) { - _selectedTerminalId = project.terminalIds.first; - changed = true; - } else if (_previousTerminalIds != null) { - // Find newly added terminals - final newIds = project.terminalIds - .where((id) => !_previousTerminalIds!.contains(id)) - .toList(); - if (newIds.isNotEmpty) { - _selectedTerminalId = newIds.last; - changed = true; - } - } - _previousTerminalIds = Set.of(project.terminalIds); - } else { - _previousTerminalIds = null; - if (_selectedTerminalId != null) { - _selectedTerminalId = null; - changed = true; - } - } - - // Poll connection health - final newActivity = conn_ffi.secondsSinceActivity(connId: connId); - if ((_secondsSinceActivity < 3) != (newActivity < 3) || - (_secondsSinceActivity < 10) != (newActivity < 10)) { - changed = true; - } - _secondsSinceActivity = newActivity; - - if (changed) { - notifyListeners(); - } - } - - bool _projectListEquals( - List a, List b) { - if (a.length != b.length) return false; - for (int i = 0; i < a.length; i++) { - if (a[i].id != b[i].id || a[i].name != b[i].name) return false; - if (!listEquals(a[i].terminalIds, b[i].terminalIds)) return false; - // Check git status changes - if (a[i].gitBranch != b[i].gitBranch) return false; - if (a[i].gitLinesAdded != b[i].gitLinesAdded) return false; - if (a[i].gitLinesRemoved != b[i].gitLinesRemoved) return false; - // Check services changes - if (a[i].services.length != b[i].services.length) return false; - for (int j = 0; j < a[i].services.length; j++) { - if (a[i].services[j].name != b[i].services[j].name || - a[i].services[j].status != b[i].services[j].status) { - return false; - } - } - if (a[i].folderColor != b[i].folderColor) return false; - } - return true; - } - - @override - void dispose() { - _connection.removeListener(_onConnectionChanged); - _stopPolling(); - super.dispose(); - } -} diff --git a/mobile/lib/src/rust/api/connection.dart b/mobile/lib/src/rust/api/connection.dart deleted file mode 100644 index ad0cd424..00000000 --- a/mobile/lib/src/rust/api/connection.dart +++ /dev/null @@ -1,53 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -import 'package:freezed_annotation/freezed_annotation.dart' hide protected; -part 'connection.freezed.dart'; - -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`, `from` - -/// Connect to an Okena remote server. Returns a connection ID. -/// If a saved token is provided, it will be used to skip pairing. -String connect({required String host, required int port, String? savedToken}) => - RustLib.instance.api.crateApiConnectionConnect( - host: host, - port: port, - savedToken: savedToken, - ); - -/// Get the current auth token for a connection (if paired). -String? getToken({required String connId}) => - RustLib.instance.api.crateApiConnectionGetToken(connId: connId); - -/// Pair with the server using a pairing code. -Future pair({required String connId, required String code}) => - RustLib.instance.api.crateApiConnectionPair(connId: connId, code: code); - -/// Disconnect from a server. -void disconnect({required String connId}) => - RustLib.instance.api.crateApiConnectionDisconnect(connId: connId); - -/// Get current connection status. -ConnectionStatus connectionStatus({required String connId}) => - RustLib.instance.api.crateApiConnectionConnectionStatus(connId: connId); - -/// Get seconds since last WS activity (terminal output). -/// Returns a large value if the connection doesn't exist. -double secondsSinceActivity({required String connId}) => - RustLib.instance.api.crateApiConnectionSecondsSinceActivity(connId: connId); - -@freezed -sealed class ConnectionStatus with _$ConnectionStatus { - const ConnectionStatus._(); - - const factory ConnectionStatus.disconnected() = ConnectionStatus_Disconnected; - const factory ConnectionStatus.connecting() = ConnectionStatus_Connecting; - const factory ConnectionStatus.connected() = ConnectionStatus_Connected; - const factory ConnectionStatus.pairing() = ConnectionStatus_Pairing; - const factory ConnectionStatus.error({required String message}) = - ConnectionStatus_Error; -} diff --git a/mobile/lib/src/rust/api/connection.freezed.dart b/mobile/lib/src/rust/api/connection.freezed.dart deleted file mode 100644 index 512cf6e9..00000000 --- a/mobile/lib/src/rust/api/connection.freezed.dart +++ /dev/null @@ -1,386 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'connection.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$ConnectionStatus { - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus()'; -} - - -} - -/// @nodoc -class $ConnectionStatusCopyWith<$Res> { -$ConnectionStatusCopyWith(ConnectionStatus _, $Res Function(ConnectionStatus) __); -} - - -/// Adds pattern-matching-related methods to [ConnectionStatus]. -extension ConnectionStatusPatterns on ConnectionStatus { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap({TResult Function( ConnectionStatus_Disconnected value)? disconnected,TResult Function( ConnectionStatus_Connecting value)? connecting,TResult Function( ConnectionStatus_Connected value)? connected,TResult Function( ConnectionStatus_Pairing value)? pairing,TResult Function( ConnectionStatus_Error value)? error,required TResult orElse(),}){ -final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected(_that);case ConnectionStatus_Connecting() when connecting != null: -return connecting(_that);case ConnectionStatus_Connected() when connected != null: -return connected(_that);case ConnectionStatus_Pairing() when pairing != null: -return pairing(_that);case ConnectionStatus_Error() when error != null: -return error(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map({required TResult Function( ConnectionStatus_Disconnected value) disconnected,required TResult Function( ConnectionStatus_Connecting value) connecting,required TResult Function( ConnectionStatus_Connected value) connected,required TResult Function( ConnectionStatus_Pairing value) pairing,required TResult Function( ConnectionStatus_Error value) error,}){ -final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected(): -return disconnected(_that);case ConnectionStatus_Connecting(): -return connecting(_that);case ConnectionStatus_Connected(): -return connected(_that);case ConnectionStatus_Pairing(): -return pairing(_that);case ConnectionStatus_Error(): -return error(_that);} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull({TResult? Function( ConnectionStatus_Disconnected value)? disconnected,TResult? Function( ConnectionStatus_Connecting value)? connecting,TResult? Function( ConnectionStatus_Connected value)? connected,TResult? Function( ConnectionStatus_Pairing value)? pairing,TResult? Function( ConnectionStatus_Error value)? error,}){ -final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected(_that);case ConnectionStatus_Connecting() when connecting != null: -return connecting(_that);case ConnectionStatus_Connected() when connected != null: -return connected(_that);case ConnectionStatus_Pairing() when pairing != null: -return pairing(_that);case ConnectionStatus_Error() when error != null: -return error(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen({TResult Function()? disconnected,TResult Function()? connecting,TResult Function()? connected,TResult Function()? pairing,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected();case ConnectionStatus_Connecting() when connecting != null: -return connecting();case ConnectionStatus_Connected() when connected != null: -return connected();case ConnectionStatus_Pairing() when pairing != null: -return pairing();case ConnectionStatus_Error() when error != null: -return error(_that.message);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when({required TResult Function() disconnected,required TResult Function() connecting,required TResult Function() connected,required TResult Function() pairing,required TResult Function( String message) error,}) {final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected(): -return disconnected();case ConnectionStatus_Connecting(): -return connecting();case ConnectionStatus_Connected(): -return connected();case ConnectionStatus_Pairing(): -return pairing();case ConnectionStatus_Error(): -return error(_that.message);} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull({TResult? Function()? disconnected,TResult? Function()? connecting,TResult? Function()? connected,TResult? Function()? pairing,TResult? Function( String message)? error,}) {final _that = this; -switch (_that) { -case ConnectionStatus_Disconnected() when disconnected != null: -return disconnected();case ConnectionStatus_Connecting() when connecting != null: -return connecting();case ConnectionStatus_Connected() when connected != null: -return connected();case ConnectionStatus_Pairing() when pairing != null: -return pairing();case ConnectionStatus_Error() when error != null: -return error(_that.message);case _: - return null; - -} -} - -} - -/// @nodoc - - -class ConnectionStatus_Disconnected extends ConnectionStatus { - const ConnectionStatus_Disconnected(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Disconnected); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.disconnected()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Connecting extends ConnectionStatus { - const ConnectionStatus_Connecting(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Connecting); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.connecting()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Connected extends ConnectionStatus { - const ConnectionStatus_Connected(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Connected); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.connected()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Pairing extends ConnectionStatus { - const ConnectionStatus_Pairing(): super._(); - - - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Pairing); -} - - -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ConnectionStatus.pairing()'; -} - - -} - - - - -/// @nodoc - - -class ConnectionStatus_Error extends ConnectionStatus { - const ConnectionStatus_Error({required this.message}): super._(); - - - final String message; - -/// Create a copy of ConnectionStatus -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$ConnectionStatus_ErrorCopyWith get copyWith => _$ConnectionStatus_ErrorCopyWithImpl(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStatus_Error&&(identical(other.message, message) || other.message == message)); -} - - -@override -int get hashCode => Object.hash(runtimeType,message); - -@override -String toString() { - return 'ConnectionStatus.error(message: $message)'; -} - - -} - -/// @nodoc -abstract mixin class $ConnectionStatus_ErrorCopyWith<$Res> implements $ConnectionStatusCopyWith<$Res> { - factory $ConnectionStatus_ErrorCopyWith(ConnectionStatus_Error value, $Res Function(ConnectionStatus_Error) _then) = _$ConnectionStatus_ErrorCopyWithImpl; -@useResult -$Res call({ - String message -}); - - - - -} -/// @nodoc -class _$ConnectionStatus_ErrorCopyWithImpl<$Res> - implements $ConnectionStatus_ErrorCopyWith<$Res> { - _$ConnectionStatus_ErrorCopyWithImpl(this._self, this._then); - - final ConnectionStatus_Error _self; - final $Res Function(ConnectionStatus_Error) _then; - -/// Create a copy of ConnectionStatus -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') $Res call({Object? message = null,}) { - return _then(ConnectionStatus_Error( -message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable -as String, - )); -} - - -} - -// dart format on diff --git a/mobile/lib/src/rust/api/state.dart b/mobile/lib/src/rust/api/state.dart deleted file mode 100644 index 2c55dec2..00000000 --- a/mobile/lib/src/rust/api/state.dart +++ /dev/null @@ -1,567 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// These functions are ignored because they are not marked as `pub`: `collect_layout_ids_vec` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt` - -/// Get all projects from the cached remote state. -List getProjects({required String connId}) => - RustLib.instance.api.crateApiStateGetProjects(connId: connId); - -/// Get the focused project ID from the cached remote state. -String? getFocusedProjectId({required String connId}) => - RustLib.instance.api.crateApiStateGetFocusedProjectId(connId: connId); - -/// Get folders from the cached remote state. -List getFolders({required String connId}) => - RustLib.instance.api.crateApiStateGetFolders(connId: connId); - -/// Get the project order from the cached remote state. -List getProjectOrder({required String connId}) => - RustLib.instance.api.crateApiStateGetProjectOrder(connId: connId); - -/// Get fullscreen terminal info. -FullscreenInfo? getFullscreenTerminal({required String connId}) => - RustLib.instance.api.crateApiStateGetFullscreenTerminal(connId: connId); - -/// Get layout JSON for a project. -String? getProjectLayoutJson({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateGetProjectLayoutJson( - connId: connId, - projectId: projectId, -); - -/// Check if a terminal has unprocessed output (dirty flag). -bool isDirty({required String connId, required String terminalId}) => RustLib - .instance - .api - .crateApiStateIsDirty(connId: connId, terminalId: terminalId); - -/// Send a special key (e.g. "Enter", "Tab", "Escape") to a terminal. -/// -/// The key name is deserialized from JSON (e.g. `"Enter"`, `"CtrlC"`, `"ArrowUp"`). -Future sendSpecialKey({ - required String connId, - required String terminalId, - required String key, -}) => RustLib.instance.api.crateApiStateSendSpecialKey( - connId: connId, - terminalId: terminalId, - key: key, -); - -/// Get all terminal IDs from the cached remote state (flat list). -List getAllTerminalIds({required String connId}) => - RustLib.instance.api.crateApiStateGetAllTerminalIds(connId: connId); - -/// Create a new terminal in the given project. -Future createTerminal({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateCreateTerminal( - connId: connId, - projectId: projectId, -); - -/// Close a terminal in the given project. -Future closeTerminal({ - required String connId, - required String projectId, - required String terminalId, -}) => RustLib.instance.api.crateApiStateCloseTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, -); - -/// Close multiple terminals in a project. -Future closeTerminals({ - required String connId, - required String projectId, - required List terminalIds, -}) => RustLib.instance.api.crateApiStateCloseTerminals( - connId: connId, - projectId: projectId, - terminalIds: terminalIds, -); - -/// Rename a terminal. -Future renameTerminal({ - required String connId, - required String projectId, - required String terminalId, - required String name, -}) => RustLib.instance.api.crateApiStateRenameTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - name: name, -); - -/// Focus a terminal. -Future focusTerminal({ - required String connId, - required String projectId, - required String terminalId, -}) => RustLib.instance.api.crateApiStateFocusTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, -); - -/// Toggle minimized state of a terminal. -Future toggleMinimized({ - required String connId, - required String projectId, - required String terminalId, -}) => RustLib.instance.api.crateApiStateToggleMinimized( - connId: connId, - projectId: projectId, - terminalId: terminalId, -); - -/// Set/clear fullscreen terminal. -Future setFullscreen({ - required String connId, - required String projectId, - String? terminalId, -}) => RustLib.instance.api.crateApiStateSetFullscreen( - connId: connId, - projectId: projectId, - terminalId: terminalId, -); - -/// Split a terminal pane. -Future splitTerminal({ - required String connId, - required String projectId, - required Uint64List path, - required String direction, -}) => RustLib.instance.api.crateApiStateSplitTerminal( - connId: connId, - projectId: projectId, - path: path, - direction: direction, -); - -/// Run a command in a terminal (presses Enter automatically). -Future runCommand({ - required String connId, - required String terminalId, - required String command, -}) => RustLib.instance.api.crateApiStateRunCommand( - connId: connId, - terminalId: terminalId, - command: command, -); - -/// Read terminal content as text. -Future readContent({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiStateReadContent( - connId: connId, - terminalId: terminalId, -); - -/// Get detailed git status for a project. -Future gitStatus({required String connId, required String projectId}) => - RustLib.instance.api.crateApiStateGitStatus( - connId: connId, - projectId: projectId, - ); - -/// Get git diff summary for a project. -Future gitDiffSummary({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateGitDiffSummary( - connId: connId, - projectId: projectId, -); - -/// Get git diff for a project. Mode: "working_tree", "staged". -Future gitDiff({ - required String connId, - required String projectId, - required String mode, -}) => RustLib.instance.api.crateApiStateGitDiff( - connId: connId, - projectId: projectId, - mode: mode, -); - -/// Get git branches for a project. -Future gitBranches({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateGitBranches( - connId: connId, - projectId: projectId, -); - -/// Start a service. -Future startService({ - required String connId, - required String projectId, - required String serviceName, -}) => RustLib.instance.api.crateApiStateStartService( - connId: connId, - projectId: projectId, - serviceName: serviceName, -); - -/// Stop a service. -Future stopService({ - required String connId, - required String projectId, - required String serviceName, -}) => RustLib.instance.api.crateApiStateStopService( - connId: connId, - projectId: projectId, - serviceName: serviceName, -); - -/// Restart a service. -Future restartService({ - required String connId, - required String projectId, - required String serviceName, -}) => RustLib.instance.api.crateApiStateRestartService( - connId: connId, - projectId: projectId, - serviceName: serviceName, -); - -/// Start all services in a project. -Future startAllServices({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateStartAllServices( - connId: connId, - projectId: projectId, -); - -/// Stop all services in a project. -Future stopAllServices({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateStopAllServices( - connId: connId, - projectId: projectId, -); - -/// Reload services config for a project. -Future reloadServices({ - required String connId, - required String projectId, -}) => RustLib.instance.api.crateApiStateReloadServices( - connId: connId, - projectId: projectId, -); - -/// Add a new project. -Future addProject({ - required String connId, - required String name, - required String path, -}) => RustLib.instance.api.crateApiStateAddProject( - connId: connId, - name: name, - path: path, -); - -/// Set project color. -Future setProjectColor({ - required String connId, - required String projectId, - required String color, -}) => RustLib.instance.api.crateApiStateSetProjectColor( - connId: connId, - projectId: projectId, - color: color, -); - -/// Set folder color. -Future setFolderColor({ - required String connId, - required String folderId, - required String color, -}) => RustLib.instance.api.crateApiStateSetFolderColor( - connId: connId, - folderId: folderId, - color: color, -); - -/// Reorder a project within a folder. -Future reorderProjectInFolder({ - required String connId, - required String folderId, - required String projectId, - required BigInt newIndex, -}) => RustLib.instance.api.crateApiStateReorderProjectInFolder( - connId: connId, - folderId: folderId, - projectId: projectId, - newIndex: newIndex, -); - -/// Update split sizes for a split pane. -Future updateSplitSizes({ - required String connId, - required String projectId, - required Uint64List path, - required List sizes, -}) => RustLib.instance.api.crateApiStateUpdateSplitSizes( - connId: connId, - projectId: projectId, - path: path, - sizes: sizes, -); - -/// Add a new tab to a tab group. -Future addTab({ - required String connId, - required String projectId, - required Uint64List path, - required bool inGroup, -}) => RustLib.instance.api.crateApiStateAddTab( - connId: connId, - projectId: projectId, - path: path, - inGroup: inGroup, -); - -/// Set the active tab in a tab group. -Future setActiveTab({ - required String connId, - required String projectId, - required Uint64List path, - required BigInt index, -}) => RustLib.instance.api.crateApiStateSetActiveTab( - connId: connId, - projectId: projectId, - path: path, - index: index, -); - -/// Move a tab within a tab group. -Future moveTab({ - required String connId, - required String projectId, - required Uint64List path, - required BigInt fromIndex, - required BigInt toIndex, -}) => RustLib.instance.api.crateApiStateMoveTab( - connId: connId, - projectId: projectId, - path: path, - fromIndex: fromIndex, - toIndex: toIndex, -); - -/// Move a terminal into a tab group. -Future moveTerminalToTabGroup({ - required String connId, - required String projectId, - required String terminalId, - required Uint64List targetPath, - BigInt? position, - String? targetProjectId, -}) => RustLib.instance.api.crateApiStateMoveTerminalToTabGroup( - connId: connId, - projectId: projectId, - terminalId: terminalId, - targetPath: targetPath, - position: position, - targetProjectId: targetProjectId, -); - -/// Move a pane to a drop zone relative to another terminal. -Future movePaneTo({ - required String connId, - required String projectId, - required String terminalId, - required String targetProjectId, - required String targetTerminalId, - required String zone, -}) => RustLib.instance.api.crateApiStateMovePaneTo( - connId: connId, - projectId: projectId, - terminalId: terminalId, - targetProjectId: targetProjectId, - targetTerminalId: targetTerminalId, - zone: zone, -); - -/// Get file contents from git (working tree or staged). -Future gitFileContents({ - required String connId, - required String projectId, - required String filePath, - required String mode, -}) => RustLib.instance.api.crateApiStateGitFileContents( - connId: connId, - projectId: projectId, - filePath: filePath, - mode: mode, -); - -/// FFI-friendly folder info. -class FolderInfo { - final String id; - final String name; - final List projectIds; - final String folderColor; - - const FolderInfo({ - required this.id, - required this.name, - required this.projectIds, - required this.folderColor, - }); - - @override - int get hashCode => - id.hashCode ^ name.hashCode ^ projectIds.hashCode ^ folderColor.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FolderInfo && - runtimeType == other.runtimeType && - id == other.id && - name == other.name && - projectIds == other.projectIds && - folderColor == other.folderColor; -} - -/// FFI-friendly fullscreen info. -class FullscreenInfo { - final String projectId; - final String terminalId; - - const FullscreenInfo({required this.projectId, required this.terminalId}); - - @override - int get hashCode => projectId.hashCode ^ terminalId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FullscreenInfo && - runtimeType == other.runtimeType && - projectId == other.projectId && - terminalId == other.terminalId; -} - -/// Flat FFI-friendly project info. -class ProjectInfo { - final String id; - final String name; - final String path; - final bool showInOverview; - final List terminalIds; - final Map terminalNames; - final String? gitBranch; - final int gitLinesAdded; - final int gitLinesRemoved; - final List services; - final String folderColor; - - const ProjectInfo({ - required this.id, - required this.name, - required this.path, - required this.showInOverview, - required this.terminalIds, - required this.terminalNames, - this.gitBranch, - required this.gitLinesAdded, - required this.gitLinesRemoved, - required this.services, - required this.folderColor, - }); - - @override - int get hashCode => - id.hashCode ^ - name.hashCode ^ - path.hashCode ^ - showInOverview.hashCode ^ - terminalIds.hashCode ^ - terminalNames.hashCode ^ - gitBranch.hashCode ^ - gitLinesAdded.hashCode ^ - gitLinesRemoved.hashCode ^ - services.hashCode ^ - folderColor.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ProjectInfo && - runtimeType == other.runtimeType && - id == other.id && - name == other.name && - path == other.path && - showInOverview == other.showInOverview && - terminalIds == other.terminalIds && - terminalNames == other.terminalNames && - gitBranch == other.gitBranch && - gitLinesAdded == other.gitLinesAdded && - gitLinesRemoved == other.gitLinesRemoved && - services == other.services && - folderColor == other.folderColor; -} - -/// FFI-friendly service info. -class ServiceInfo { - final String name; - final String status; - final String? terminalId; - final Uint16List ports; - final int? exitCode; - final String kind; - final bool isExtra; - - const ServiceInfo({ - required this.name, - required this.status, - this.terminalId, - required this.ports, - this.exitCode, - required this.kind, - required this.isExtra, - }); - - @override - int get hashCode => - name.hashCode ^ - status.hashCode ^ - terminalId.hashCode ^ - ports.hashCode ^ - exitCode.hashCode ^ - kind.hashCode ^ - isExtra.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ServiceInfo && - runtimeType == other.runtimeType && - name == other.name && - status == other.status && - terminalId == other.terminalId && - ports == other.ports && - exitCode == other.exitCode && - kind == other.kind && - isExtra == other.isExtra; -} diff --git a/mobile/lib/src/rust/api/terminal.dart b/mobile/lib/src/rust/api/terminal.dart deleted file mode 100644 index 54fb41e2..00000000 --- a/mobile/lib/src/rust/api/terminal.dart +++ /dev/null @@ -1,268 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import - -import '../frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt` - -/// Get the visible terminal cells for rendering. -List getVisibleCells({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetVisibleCells( - connId: connId, - terminalId: terminalId, -); - -/// Get the current cursor state. -CursorState getCursor({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalGetCursor( - connId: connId, - terminalId: terminalId, - ); - -/// Scroll the terminal display (positive = up, negative = down). -void scroll({ - required String connId, - required String terminalId, - required int delta, -}) => RustLib.instance.api.crateApiTerminalScroll( - connId: connId, - terminalId: terminalId, - delta: delta, -); - -/// Get scroll info: total lines, visible lines, display offset. -ScrollInfo getScrollInfo({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetScrollInfo( - connId: connId, - terminalId: terminalId, -); - -/// Start a character-level selection at col/row. -void startSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalStartSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Start a word (semantic) selection at col/row. -void startWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalStartWordSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Extend the current selection to col/row. -void updateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, -}) => RustLib.instance.api.crateApiTerminalUpdateSelection( - connId: connId, - terminalId: terminalId, - col: col, - row: row, -); - -/// Clear the current selection. -void clearSelection({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalClearSelection( - connId: connId, - terminalId: terminalId, - ); - -/// Get the selected text, if any. -String? getSelectedText({required String connId, required String terminalId}) => - RustLib.instance.api.crateApiTerminalGetSelectedText( - connId: connId, - terminalId: terminalId, - ); - -/// Get selection bounds for rendering. -SelectionBounds? getSelectionBounds({ - required String connId, - required String terminalId, -}) => RustLib.instance.api.crateApiTerminalGetSelectionBounds( - connId: connId, - terminalId: terminalId, -); - -/// Send text input to a terminal. -Future sendText({ - required String connId, - required String terminalId, - required String text, -}) => RustLib.instance.api.crateApiTerminalSendText( - connId: connId, - terminalId: terminalId, - text: text, -); - -/// Resize a terminal. -void resizeTerminal({ - required String connId, - required String terminalId, - required int cols, - required int rows, -}) => RustLib.instance.api.crateApiTerminalResizeTerminal( - connId: connId, - terminalId: terminalId, - cols: cols, - rows: rows, -); - -/// Resize only the local alacritty terminal — does NOT send a WS resize message to the server. -/// Used when mobile adapts to the server's terminal size. -void resizeLocal({ - required String connId, - required String terminalId, - required int cols, - required int rows, -}) => RustLib.instance.api.crateApiTerminalResizeLocal( - connId: connId, - terminalId: terminalId, - cols: cols, - rows: rows, -); - -/// Cell data for FFI transfer (flat, no pointers). -class CellData { - /// The character in this cell. - final String character; - - /// Foreground color as ARGB packed u32. - final int fg; - - /// Background color as ARGB packed u32. - final int bg; - - /// Flags: bold(1) | italic(2) | underline(4) | strikethrough(8) | inverse(16) | dim(32). - final int flags; - - const CellData({ - required this.character, - required this.fg, - required this.bg, - required this.flags, - }); - - @override - int get hashCode => - character.hashCode ^ fg.hashCode ^ bg.hashCode ^ flags.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CellData && - runtimeType == other.runtimeType && - character == other.character && - fg == other.fg && - bg == other.bg && - flags == other.flags; -} - -/// Cursor shape variants. -enum CursorShape { block, underline, beam } - -/// Cursor state for FFI transfer. -class CursorState { - final int col; - final int row; - final CursorShape shape; - final bool visible; - - const CursorState({ - required this.col, - required this.row, - required this.shape, - required this.visible, - }); - - @override - int get hashCode => - col.hashCode ^ row.hashCode ^ shape.hashCode ^ visible.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CursorState && - runtimeType == other.runtimeType && - col == other.col && - row == other.row && - shape == other.shape && - visible == other.visible; -} - -/// Scroll info for FFI transfer. -class ScrollInfo { - final int totalLines; - final int visibleLines; - final int displayOffset; - - const ScrollInfo({ - required this.totalLines, - required this.visibleLines, - required this.displayOffset, - }); - - @override - int get hashCode => - totalLines.hashCode ^ visibleLines.hashCode ^ displayOffset.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ScrollInfo && - runtimeType == other.runtimeType && - totalLines == other.totalLines && - visibleLines == other.visibleLines && - displayOffset == other.displayOffset; -} - -/// Selection bounds for FFI transfer. -class SelectionBounds { - final int startCol; - final int startRow; - final int endCol; - final int endRow; - - const SelectionBounds({ - required this.startCol, - required this.startRow, - required this.endCol, - required this.endRow, - }); - - @override - int get hashCode => - startCol.hashCode ^ startRow.hashCode ^ endCol.hashCode ^ endRow.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SelectionBounds && - runtimeType == other.runtimeType && - startCol == other.startCol && - startRow == other.startRow && - endCol == other.endCol && - endRow == other.endRow; -} diff --git a/mobile/lib/src/rust/frb_generated.dart b/mobile/lib/src/rust/frb_generated.dart deleted file mode 100644 index 76a052b6..00000000 --- a/mobile/lib/src/rust/frb_generated.dart +++ /dev/null @@ -1,3632 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -import 'api/connection.dart'; -import 'api/state.dart'; -import 'api/terminal.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.dart'; -import 'frb_generated.io.dart' - if (dart.library.js_interop) 'frb_generated.web.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; - -/// Main entrypoint of the Rust API -class RustLib extends BaseEntrypoint { - @internal - static final instance = RustLib._(); - - RustLib._(); - - /// Initialize flutter_rust_bridge - static Future init({ - RustLibApi? api, - BaseHandler? handler, - ExternalLibrary? externalLibrary, - bool forceSameCodegenVersion = true, - }) async { - await instance.initImpl( - api: api, - handler: handler, - externalLibrary: externalLibrary, - forceSameCodegenVersion: forceSameCodegenVersion, - ); - } - - /// Initialize flutter_rust_bridge in mock mode. - /// No libraries for FFI are loaded. - static void initMock({required RustLibApi api}) { - instance.initMockImpl(api: api); - } - - /// Dispose flutter_rust_bridge - /// - /// The call to this function is optional, since flutter_rust_bridge (and everything else) - /// is automatically disposed when the app stops. - static void dispose() => instance.disposeImpl(); - - @override - ApiImplConstructor get apiImplConstructor => - RustLibApiImpl.new; - - @override - WireConstructor get wireConstructor => - RustLibWire.fromExternalLibrary; - - @override - Future executeRustInitializers() async { - await api.crateApiConnectionInitApp(); - } - - @override - ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => - kDefaultExternalLibraryLoaderConfig; - - @override - String get codegenVersion => '2.11.1'; - - @override - int get rustContentHash => 632182563; - - static const kDefaultExternalLibraryLoaderConfig = - ExternalLibraryLoaderConfig( - stem: 'okena_mobile_native', - ioDirectory: 'native/target/release/', - webPrefix: 'pkg/', - ); -} - -abstract class RustLibApi extends BaseApi { - Future crateApiStateAddProject({ - required String connId, - required String name, - required String path, - }); - - Future crateApiStateAddTab({ - required String connId, - required String projectId, - required Uint64List path, - required bool inGroup, - }); - - void crateApiTerminalClearSelection({ - required String connId, - required String terminalId, - }); - - Future crateApiStateCloseTerminal({ - required String connId, - required String projectId, - required String terminalId, - }); - - Future crateApiStateCloseTerminals({ - required String connId, - required String projectId, - required List terminalIds, - }); - - String crateApiConnectionConnect({ - required String host, - required int port, - String? savedToken, - }); - - ConnectionStatus crateApiConnectionConnectionStatus({required String connId}); - - Future crateApiStateCreateTerminal({ - required String connId, - required String projectId, - }); - - void crateApiConnectionDisconnect({required String connId}); - - Future crateApiStateFocusTerminal({ - required String connId, - required String projectId, - required String terminalId, - }); - - List crateApiStateGetAllTerminalIds({required String connId}); - - CursorState crateApiTerminalGetCursor({ - required String connId, - required String terminalId, - }); - - String? crateApiStateGetFocusedProjectId({required String connId}); - - List crateApiStateGetFolders({required String connId}); - - FullscreenInfo? crateApiStateGetFullscreenTerminal({required String connId}); - - String? crateApiStateGetProjectLayoutJson({ - required String connId, - required String projectId, - }); - - List crateApiStateGetProjectOrder({required String connId}); - - List crateApiStateGetProjects({required String connId}); - - ScrollInfo crateApiTerminalGetScrollInfo({ - required String connId, - required String terminalId, - }); - - String? crateApiTerminalGetSelectedText({ - required String connId, - required String terminalId, - }); - - SelectionBounds? crateApiTerminalGetSelectionBounds({ - required String connId, - required String terminalId, - }); - - String? crateApiConnectionGetToken({required String connId}); - - List crateApiTerminalGetVisibleCells({ - required String connId, - required String terminalId, - }); - - Future crateApiStateGitBranches({ - required String connId, - required String projectId, - }); - - Future crateApiStateGitDiff({ - required String connId, - required String projectId, - required String mode, - }); - - Future crateApiStateGitDiffSummary({ - required String connId, - required String projectId, - }); - - Future crateApiStateGitFileContents({ - required String connId, - required String projectId, - required String filePath, - required String mode, - }); - - Future crateApiStateGitStatus({ - required String connId, - required String projectId, - }); - - Future crateApiConnectionInitApp(); - - bool crateApiStateIsDirty({ - required String connId, - required String terminalId, - }); - - Future crateApiStateMovePaneTo({ - required String connId, - required String projectId, - required String terminalId, - required String targetProjectId, - required String targetTerminalId, - required String zone, - }); - - Future crateApiStateMoveTab({ - required String connId, - required String projectId, - required Uint64List path, - required BigInt fromIndex, - required BigInt toIndex, - }); - - Future crateApiStateMoveTerminalToTabGroup({ - required String connId, - required String projectId, - required String terminalId, - required Uint64List targetPath, - BigInt? position, - String? targetProjectId, - }); - - Future crateApiConnectionPair({ - required String connId, - required String code, - }); - - Future crateApiStateReadContent({ - required String connId, - required String terminalId, - }); - - Future crateApiStateReloadServices({ - required String connId, - required String projectId, - }); - - Future crateApiStateRenameTerminal({ - required String connId, - required String projectId, - required String terminalId, - required String name, - }); - - Future crateApiStateReorderProjectInFolder({ - required String connId, - required String folderId, - required String projectId, - required BigInt newIndex, - }); - - void crateApiTerminalResizeLocal({ - required String connId, - required String terminalId, - required int cols, - required int rows, - }); - - void crateApiTerminalResizeTerminal({ - required String connId, - required String terminalId, - required int cols, - required int rows, - }); - - Future crateApiStateRestartService({ - required String connId, - required String projectId, - required String serviceName, - }); - - Future crateApiStateRunCommand({ - required String connId, - required String terminalId, - required String command, - }); - - void crateApiTerminalScroll({ - required String connId, - required String terminalId, - required int delta, - }); - - double crateApiConnectionSecondsSinceActivity({required String connId}); - - Future crateApiStateSendSpecialKey({ - required String connId, - required String terminalId, - required String key, - }); - - Future crateApiTerminalSendText({ - required String connId, - required String terminalId, - required String text, - }); - - Future crateApiStateSetActiveTab({ - required String connId, - required String projectId, - required Uint64List path, - required BigInt index, - }); - - Future crateApiStateSetFolderColor({ - required String connId, - required String folderId, - required String color, - }); - - Future crateApiStateSetFullscreen({ - required String connId, - required String projectId, - String? terminalId, - }); - - Future crateApiStateSetProjectColor({ - required String connId, - required String projectId, - required String color, - }); - - Future crateApiStateSplitTerminal({ - required String connId, - required String projectId, - required Uint64List path, - required String direction, - }); - - Future crateApiStateStartAllServices({ - required String connId, - required String projectId, - }); - - void crateApiTerminalStartSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); - - Future crateApiStateStartService({ - required String connId, - required String projectId, - required String serviceName, - }); - - void crateApiTerminalStartWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); - - Future crateApiStateStopAllServices({ - required String connId, - required String projectId, - }); - - Future crateApiStateStopService({ - required String connId, - required String projectId, - required String serviceName, - }); - - Future crateApiStateToggleMinimized({ - required String connId, - required String projectId, - required String terminalId, - }); - - void crateApiTerminalUpdateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }); - - Future crateApiStateUpdateSplitSizes({ - required String connId, - required String projectId, - required Uint64List path, - required List sizes, - }); -} - -class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { - RustLibApiImpl({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @override - Future crateApiStateAddProject({ - required String connId, - required String name, - required String path, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(name, serializer); - sse_encode_String(path, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 1, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateAddProjectConstMeta, - argValues: [connId, name, path], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateAddProjectConstMeta => const TaskConstMeta( - debugName: "add_project", - argNames: ["connId", "name", "path"], - ); - - @override - Future crateApiStateAddTab({ - required String connId, - required String projectId, - required Uint64List path, - required bool inGroup, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_list_prim_usize_strict(path, serializer); - sse_encode_bool(inGroup, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 2, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateAddTabConstMeta, - argValues: [connId, projectId, path, inGroup], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateAddTabConstMeta => const TaskConstMeta( - debugName: "add_tab", - argNames: ["connId", "projectId", "path", "inGroup"], - ); - - @override - void crateApiTerminalClearSelection({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalClearSelectionConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalClearSelectionConstMeta => - const TaskConstMeta( - debugName: "clear_selection", - argNames: ["connId", "terminalId"], - ); - - @override - Future crateApiStateCloseTerminal({ - required String connId, - required String projectId, - required String terminalId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(terminalId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 4, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateCloseTerminalConstMeta, - argValues: [connId, projectId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateCloseTerminalConstMeta => const TaskConstMeta( - debugName: "close_terminal", - argNames: ["connId", "projectId", "terminalId"], - ); - - @override - Future crateApiStateCloseTerminals({ - required String connId, - required String projectId, - required List terminalIds, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_list_String(terminalIds, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 5, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateCloseTerminalsConstMeta, - argValues: [connId, projectId, terminalIds], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateCloseTerminalsConstMeta => - const TaskConstMeta( - debugName: "close_terminals", - argNames: ["connId", "projectId", "terminalIds"], - ); - - @override - String crateApiConnectionConnect({ - required String host, - required int port, - String? savedToken, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(host, serializer); - sse_encode_u_16(port, serializer); - sse_encode_opt_String(savedToken, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionConnectConstMeta, - argValues: [host, port, savedToken], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionConnectConstMeta => const TaskConstMeta( - debugName: "connect", - argNames: ["host", "port", "savedToken"], - ); - - @override - ConnectionStatus crateApiConnectionConnectionStatus({ - required String connId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_connection_status, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionConnectionStatusConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionConnectionStatusConstMeta => - const TaskConstMeta(debugName: "connection_status", argNames: ["connId"]); - - @override - Future crateApiStateCreateTerminal({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 8, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateCreateTerminalConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateCreateTerminalConstMeta => - const TaskConstMeta( - debugName: "create_terminal", - argNames: ["connId", "projectId"], - ); - - @override - void crateApiConnectionDisconnect({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionDisconnectConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionDisconnectConstMeta => - const TaskConstMeta(debugName: "disconnect", argNames: ["connId"]); - - @override - Future crateApiStateFocusTerminal({ - required String connId, - required String projectId, - required String terminalId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(terminalId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 10, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateFocusTerminalConstMeta, - argValues: [connId, projectId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateFocusTerminalConstMeta => const TaskConstMeta( - debugName: "focus_terminal", - argNames: ["connId", "projectId", "terminalId"], - ); - - @override - List crateApiStateGetAllTerminalIds({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_String, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetAllTerminalIdsConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetAllTerminalIdsConstMeta => - const TaskConstMeta( - debugName: "get_all_terminal_ids", - argNames: ["connId"], - ); - - @override - CursorState crateApiTerminalGetCursor({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_cursor_state, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetCursorConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetCursorConstMeta => const TaskConstMeta( - debugName: "get_cursor", - argNames: ["connId", "terminalId"], - ); - - @override - String? crateApiStateGetFocusedProjectId({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetFocusedProjectIdConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetFocusedProjectIdConstMeta => - const TaskConstMeta( - debugName: "get_focused_project_id", - argNames: ["connId"], - ); - - @override - List crateApiStateGetFolders({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_folder_info, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetFoldersConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetFoldersConstMeta => - const TaskConstMeta(debugName: "get_folders", argNames: ["connId"]); - - @override - FullscreenInfo? crateApiStateGetFullscreenTerminal({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_box_autoadd_fullscreen_info, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetFullscreenTerminalConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetFullscreenTerminalConstMeta => - const TaskConstMeta( - debugName: "get_fullscreen_terminal", - argNames: ["connId"], - ); - - @override - String? crateApiStateGetProjectLayoutJson({ - required String connId, - required String projectId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetProjectLayoutJsonConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetProjectLayoutJsonConstMeta => - const TaskConstMeta( - debugName: "get_project_layout_json", - argNames: ["connId", "projectId"], - ); - - @override - List crateApiStateGetProjectOrder({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_String, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetProjectOrderConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetProjectOrderConstMeta => - const TaskConstMeta(debugName: "get_project_order", argNames: ["connId"]); - - @override - List crateApiStateGetProjects({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_project_info, - decodeErrorData: null, - ), - constMeta: kCrateApiStateGetProjectsConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGetProjectsConstMeta => - const TaskConstMeta(debugName: "get_projects", argNames: ["connId"]); - - @override - ScrollInfo crateApiTerminalGetScrollInfo({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_scroll_info, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetScrollInfoConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetScrollInfoConstMeta => - const TaskConstMeta( - debugName: "get_scroll_info", - argNames: ["connId", "terminalId"], - ); - - @override - String? crateApiTerminalGetSelectedText({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetSelectedTextConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetSelectedTextConstMeta => - const TaskConstMeta( - debugName: "get_selected_text", - argNames: ["connId", "terminalId"], - ); - - @override - SelectionBounds? crateApiTerminalGetSelectionBounds({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_box_autoadd_selection_bounds, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetSelectionBoundsConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetSelectionBoundsConstMeta => - const TaskConstMeta( - debugName: "get_selection_bounds", - argNames: ["connId", "terminalId"], - ); - - @override - String? crateApiConnectionGetToken({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_String, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionGetTokenConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionGetTokenConstMeta => - const TaskConstMeta(debugName: "get_token", argNames: ["connId"]); - - @override - List crateApiTerminalGetVisibleCells({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_cell_data, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalGetVisibleCellsConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalGetVisibleCellsConstMeta => - const TaskConstMeta( - debugName: "get_visible_cells", - argNames: ["connId", "terminalId"], - ); - - @override - Future crateApiStateGitBranches({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 24, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateGitBranchesConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGitBranchesConstMeta => const TaskConstMeta( - debugName: "git_branches", - argNames: ["connId", "projectId"], - ); - - @override - Future crateApiStateGitDiff({ - required String connId, - required String projectId, - required String mode, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(mode, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 25, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateGitDiffConstMeta, - argValues: [connId, projectId, mode], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGitDiffConstMeta => const TaskConstMeta( - debugName: "git_diff", - argNames: ["connId", "projectId", "mode"], - ); - - @override - Future crateApiStateGitDiffSummary({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 26, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateGitDiffSummaryConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGitDiffSummaryConstMeta => - const TaskConstMeta( - debugName: "git_diff_summary", - argNames: ["connId", "projectId"], - ); - - @override - Future crateApiStateGitFileContents({ - required String connId, - required String projectId, - required String filePath, - required String mode, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(filePath, serializer); - sse_encode_String(mode, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 27, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateGitFileContentsConstMeta, - argValues: [connId, projectId, filePath, mode], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGitFileContentsConstMeta => - const TaskConstMeta( - debugName: "git_file_contents", - argNames: ["connId", "projectId", "filePath", "mode"], - ); - - @override - Future crateApiStateGitStatus({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 28, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateGitStatusConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateGitStatusConstMeta => const TaskConstMeta( - debugName: "git_status", - argNames: ["connId", "projectId"], - ); - - @override - Future crateApiConnectionInitApp() { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 29, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionInitAppConstMeta, - argValues: [], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionInitAppConstMeta => - const TaskConstMeta(debugName: "init_app", argNames: []); - - @override - bool crateApiStateIsDirty({ - required String connId, - required String terminalId, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_bool, - decodeErrorData: null, - ), - constMeta: kCrateApiStateIsDirtyConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateIsDirtyConstMeta => const TaskConstMeta( - debugName: "is_dirty", - argNames: ["connId", "terminalId"], - ); - - @override - Future crateApiStateMovePaneTo({ - required String connId, - required String projectId, - required String terminalId, - required String targetProjectId, - required String targetTerminalId, - required String zone, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(targetProjectId, serializer); - sse_encode_String(targetTerminalId, serializer); - sse_encode_String(zone, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 31, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateMovePaneToConstMeta, - argValues: [ - connId, - projectId, - terminalId, - targetProjectId, - targetTerminalId, - zone, - ], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateMovePaneToConstMeta => const TaskConstMeta( - debugName: "move_pane_to", - argNames: [ - "connId", - "projectId", - "terminalId", - "targetProjectId", - "targetTerminalId", - "zone", - ], - ); - - @override - Future crateApiStateMoveTab({ - required String connId, - required String projectId, - required Uint64List path, - required BigInt fromIndex, - required BigInt toIndex, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_list_prim_usize_strict(path, serializer); - sse_encode_usize(fromIndex, serializer); - sse_encode_usize(toIndex, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 32, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateMoveTabConstMeta, - argValues: [connId, projectId, path, fromIndex, toIndex], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateMoveTabConstMeta => const TaskConstMeta( - debugName: "move_tab", - argNames: ["connId", "projectId", "path", "fromIndex", "toIndex"], - ); - - @override - Future crateApiStateMoveTerminalToTabGroup({ - required String connId, - required String projectId, - required String terminalId, - required Uint64List targetPath, - BigInt? position, - String? targetProjectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_list_prim_usize_strict(targetPath, serializer); - sse_encode_opt_box_autoadd_usize(position, serializer); - sse_encode_opt_String(targetProjectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 33, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateMoveTerminalToTabGroupConstMeta, - argValues: [ - connId, - projectId, - terminalId, - targetPath, - position, - targetProjectId, - ], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateMoveTerminalToTabGroupConstMeta => - const TaskConstMeta( - debugName: "move_terminal_to_tab_group", - argNames: [ - "connId", - "projectId", - "terminalId", - "targetPath", - "position", - "targetProjectId", - ], - ); - - @override - Future crateApiConnectionPair({ - required String connId, - required String code, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(code, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 34, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiConnectionPairConstMeta, - argValues: [connId, code], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionPairConstMeta => - const TaskConstMeta(debugName: "pair", argNames: ["connId", "code"]); - - @override - Future crateApiStateReadContent({ - required String connId, - required String terminalId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 35, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateReadContentConstMeta, - argValues: [connId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateReadContentConstMeta => const TaskConstMeta( - debugName: "read_content", - argNames: ["connId", "terminalId"], - ); - - @override - Future crateApiStateReloadServices({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 36, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateReloadServicesConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateReloadServicesConstMeta => - const TaskConstMeta( - debugName: "reload_services", - argNames: ["connId", "projectId"], - ); - - @override - Future crateApiStateRenameTerminal({ - required String connId, - required String projectId, - required String terminalId, - required String name, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(name, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 37, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateRenameTerminalConstMeta, - argValues: [connId, projectId, terminalId, name], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateRenameTerminalConstMeta => - const TaskConstMeta( - debugName: "rename_terminal", - argNames: ["connId", "projectId", "terminalId", "name"], - ); - - @override - Future crateApiStateReorderProjectInFolder({ - required String connId, - required String folderId, - required String projectId, - required BigInt newIndex, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(folderId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_usize(newIndex, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 38, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateReorderProjectInFolderConstMeta, - argValues: [connId, folderId, projectId, newIndex], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateReorderProjectInFolderConstMeta => - const TaskConstMeta( - debugName: "reorder_project_in_folder", - argNames: ["connId", "folderId", "projectId", "newIndex"], - ); - - @override - void crateApiTerminalResizeLocal({ - required String connId, - required String terminalId, - required int cols, - required int rows, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(cols, serializer); - sse_encode_u_16(rows, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalResizeLocalConstMeta, - argValues: [connId, terminalId, cols, rows], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalResizeLocalConstMeta => - const TaskConstMeta( - debugName: "resize_local", - argNames: ["connId", "terminalId", "cols", "rows"], - ); - - @override - void crateApiTerminalResizeTerminal({ - required String connId, - required String terminalId, - required int cols, - required int rows, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(cols, serializer); - sse_encode_u_16(rows, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalResizeTerminalConstMeta, - argValues: [connId, terminalId, cols, rows], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalResizeTerminalConstMeta => - const TaskConstMeta( - debugName: "resize_terminal", - argNames: ["connId", "terminalId", "cols", "rows"], - ); - - @override - Future crateApiStateRestartService({ - required String connId, - required String projectId, - required String serviceName, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(serviceName, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 41, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateRestartServiceConstMeta, - argValues: [connId, projectId, serviceName], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateRestartServiceConstMeta => - const TaskConstMeta( - debugName: "restart_service", - argNames: ["connId", "projectId", "serviceName"], - ); - - @override - Future crateApiStateRunCommand({ - required String connId, - required String terminalId, - required String command, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(command, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 42, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateRunCommandConstMeta, - argValues: [connId, terminalId, command], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateRunCommandConstMeta => const TaskConstMeta( - debugName: "run_command", - argNames: ["connId", "terminalId", "command"], - ); - - @override - void crateApiTerminalScroll({ - required String connId, - required String terminalId, - required int delta, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_i_32(delta, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalScrollConstMeta, - argValues: [connId, terminalId, delta], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalScrollConstMeta => const TaskConstMeta( - debugName: "scroll", - argNames: ["connId", "terminalId", "delta"], - ); - - @override - double crateApiConnectionSecondsSinceActivity({required String connId}) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_f_64, - decodeErrorData: null, - ), - constMeta: kCrateApiConnectionSecondsSinceActivityConstMeta, - argValues: [connId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiConnectionSecondsSinceActivityConstMeta => - const TaskConstMeta( - debugName: "seconds_since_activity", - argNames: ["connId"], - ); - - @override - Future crateApiStateSendSpecialKey({ - required String connId, - required String terminalId, - required String key, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(key, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 45, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateSendSpecialKeyConstMeta, - argValues: [connId, terminalId, key], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateSendSpecialKeyConstMeta => - const TaskConstMeta( - debugName: "send_special_key", - argNames: ["connId", "terminalId", "key"], - ); - - @override - Future crateApiTerminalSendText({ - required String connId, - required String terminalId, - required String text, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_String(text, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 46, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiTerminalSendTextConstMeta, - argValues: [connId, terminalId, text], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalSendTextConstMeta => const TaskConstMeta( - debugName: "send_text", - argNames: ["connId", "terminalId", "text"], - ); - - @override - Future crateApiStateSetActiveTab({ - required String connId, - required String projectId, - required Uint64List path, - required BigInt index, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_list_prim_usize_strict(path, serializer); - sse_encode_usize(index, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 47, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateSetActiveTabConstMeta, - argValues: [connId, projectId, path, index], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateSetActiveTabConstMeta => const TaskConstMeta( - debugName: "set_active_tab", - argNames: ["connId", "projectId", "path", "index"], - ); - - @override - Future crateApiStateSetFolderColor({ - required String connId, - required String folderId, - required String color, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(folderId, serializer); - sse_encode_String(color, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 48, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateSetFolderColorConstMeta, - argValues: [connId, folderId, color], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateSetFolderColorConstMeta => - const TaskConstMeta( - debugName: "set_folder_color", - argNames: ["connId", "folderId", "color"], - ); - - @override - Future crateApiStateSetFullscreen({ - required String connId, - required String projectId, - String? terminalId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_opt_String(terminalId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 49, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateSetFullscreenConstMeta, - argValues: [connId, projectId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateSetFullscreenConstMeta => const TaskConstMeta( - debugName: "set_fullscreen", - argNames: ["connId", "projectId", "terminalId"], - ); - - @override - Future crateApiStateSetProjectColor({ - required String connId, - required String projectId, - required String color, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(color, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 50, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateSetProjectColorConstMeta, - argValues: [connId, projectId, color], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateSetProjectColorConstMeta => - const TaskConstMeta( - debugName: "set_project_color", - argNames: ["connId", "projectId", "color"], - ); - - @override - Future crateApiStateSplitTerminal({ - required String connId, - required String projectId, - required Uint64List path, - required String direction, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_list_prim_usize_strict(path, serializer); - sse_encode_String(direction, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 51, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateSplitTerminalConstMeta, - argValues: [connId, projectId, path, direction], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateSplitTerminalConstMeta => const TaskConstMeta( - debugName: "split_terminal", - argNames: ["connId", "projectId", "path", "direction"], - ); - - @override - Future crateApiStateStartAllServices({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 52, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateStartAllServicesConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateStartAllServicesConstMeta => - const TaskConstMeta( - debugName: "start_all_services", - argNames: ["connId", "projectId"], - ); - - @override - void crateApiTerminalStartSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 53)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalStartSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalStartSelectionConstMeta => - const TaskConstMeta( - debugName: "start_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @override - Future crateApiStateStartService({ - required String connId, - required String projectId, - required String serviceName, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(serviceName, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 54, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateStartServiceConstMeta, - argValues: [connId, projectId, serviceName], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateStartServiceConstMeta => const TaskConstMeta( - debugName: "start_service", - argNames: ["connId", "projectId", "serviceName"], - ); - - @override - void crateApiTerminalStartWordSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalStartWordSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalStartWordSelectionConstMeta => - const TaskConstMeta( - debugName: "start_word_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @override - Future crateApiStateStopAllServices({ - required String connId, - required String projectId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 56, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateStopAllServicesConstMeta, - argValues: [connId, projectId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateStopAllServicesConstMeta => - const TaskConstMeta( - debugName: "stop_all_services", - argNames: ["connId", "projectId"], - ); - - @override - Future crateApiStateStopService({ - required String connId, - required String projectId, - required String serviceName, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(serviceName, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 57, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateStopServiceConstMeta, - argValues: [connId, projectId, serviceName], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateStopServiceConstMeta => const TaskConstMeta( - debugName: "stop_service", - argNames: ["connId", "projectId", "serviceName"], - ); - - @override - Future crateApiStateToggleMinimized({ - required String connId, - required String projectId, - required String terminalId, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_String(terminalId, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 58, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateToggleMinimizedConstMeta, - argValues: [connId, projectId, terminalId], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateToggleMinimizedConstMeta => - const TaskConstMeta( - debugName: "toggle_minimized", - argNames: ["connId", "projectId", "terminalId"], - ); - - @override - void crateApiTerminalUpdateSelection({ - required String connId, - required String terminalId, - required int col, - required int row, - }) { - return handler.executeSync( - SyncTask( - callFfi: () { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(terminalId, serializer); - sse_encode_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 59)!; - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: null, - ), - constMeta: kCrateApiTerminalUpdateSelectionConstMeta, - argValues: [connId, terminalId, col, row], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiTerminalUpdateSelectionConstMeta => - const TaskConstMeta( - debugName: "update_selection", - argNames: ["connId", "terminalId", "col", "row"], - ); - - @override - Future crateApiStateUpdateSplitSizes({ - required String connId, - required String projectId, - required Uint64List path, - required List sizes, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(connId, serializer); - sse_encode_String(projectId, serializer); - sse_encode_list_prim_usize_strict(path, serializer); - sse_encode_list_prim_f_32_loose(sizes, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 60, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: kCrateApiStateUpdateSplitSizesConstMeta, - argValues: [connId, projectId, path, sizes], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateApiStateUpdateSplitSizesConstMeta => - const TaskConstMeta( - debugName: "update_split_sizes", - argNames: ["connId", "projectId", "path", "sizes"], - ); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return AnyhowException(raw as String); - } - - @protected - Map dco_decode_Map_String_String_None(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return Map.fromEntries( - dco_decode_list_record_string_string( - raw, - ).map((e) => MapEntry(e.$1, e.$2)), - ); - } - - @protected - String dco_decode_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as String; - } - - @protected - bool dco_decode_bool(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as bool; - } - - @protected - FullscreenInfo dco_decode_box_autoadd_fullscreen_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_fullscreen_info(raw); - } - - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_selection_bounds(raw); - } - - @protected - int dco_decode_box_autoadd_u_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - BigInt dco_decode_box_autoadd_usize(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_usize(raw); - } - - @protected - CellData dco_decode_cell_data(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return CellData( - character: dco_decode_String(arr[0]), - fg: dco_decode_u_32(arr[1]), - bg: dco_decode_u_32(arr[2]), - flags: dco_decode_u_8(arr[3]), - ); - } - - @protected - ConnectionStatus dco_decode_connection_status(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - switch (raw[0]) { - case 0: - return ConnectionStatus_Disconnected(); - case 1: - return ConnectionStatus_Connecting(); - case 2: - return ConnectionStatus_Connected(); - case 3: - return ConnectionStatus_Pairing(); - case 4: - return ConnectionStatus_Error(message: dco_decode_String(raw[1])); - default: - throw Exception("unreachable"); - } - } - - @protected - CursorShape dco_decode_cursor_shape(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return CursorShape.values[raw as int]; - } - - @protected - CursorState dco_decode_cursor_state(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return CursorState( - col: dco_decode_u_16(arr[0]), - row: dco_decode_u_16(arr[1]), - shape: dco_decode_cursor_shape(arr[2]), - visible: dco_decode_bool(arr[3]), - ); - } - - @protected - double dco_decode_f_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as double; - } - - @protected - double dco_decode_f_64(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as double; - } - - @protected - FolderInfo dco_decode_folder_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return FolderInfo( - id: dco_decode_String(arr[0]), - name: dco_decode_String(arr[1]), - projectIds: dco_decode_list_String(arr[2]), - folderColor: dco_decode_String(arr[3]), - ); - } - - @protected - FullscreenInfo dco_decode_fullscreen_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 2) - throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); - return FullscreenInfo( - projectId: dco_decode_String(arr[0]), - terminalId: dco_decode_String(arr[1]), - ); - } - - @protected - int dco_decode_i_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - List dco_decode_list_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_String).toList(); - } - - @protected - List dco_decode_list_cell_data(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_cell_data).toList(); - } - - @protected - List dco_decode_list_folder_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_folder_info).toList(); - } - - @protected - List dco_decode_list_prim_f_32_loose(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as List; - } - - @protected - Float32List dco_decode_list_prim_f_32_strict(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Float32List; - } - - @protected - Uint16List dco_decode_list_prim_u_16_strict(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Uint16List; - } - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Uint8List; - } - - @protected - Uint64List dco_decode_list_prim_usize_strict(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as Uint64List; - } - - @protected - List dco_decode_list_project_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_project_info).toList(); - } - - @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_record_string_string).toList(); - } - - @protected - List dco_decode_list_service_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_service_info).toList(); - } - - @protected - String? dco_decode_opt_String(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_String(raw); - } - - @protected - FullscreenInfo? dco_decode_opt_box_autoadd_fullscreen_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_box_autoadd_fullscreen_info(raw); - } - - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_box_autoadd_selection_bounds(raw); - } - - @protected - int? dco_decode_opt_box_autoadd_u_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_box_autoadd_u_32(raw); - } - - @protected - BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_box_autoadd_usize(raw); - } - - @protected - ProjectInfo dco_decode_project_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 11) - throw Exception('unexpected arr length: expect 11 but see ${arr.length}'); - return ProjectInfo( - id: dco_decode_String(arr[0]), - name: dco_decode_String(arr[1]), - path: dco_decode_String(arr[2]), - showInOverview: dco_decode_bool(arr[3]), - terminalIds: dco_decode_list_String(arr[4]), - terminalNames: dco_decode_Map_String_String_None(arr[5]), - gitBranch: dco_decode_opt_String(arr[6]), - gitLinesAdded: dco_decode_u_32(arr[7]), - gitLinesRemoved: dco_decode_u_32(arr[8]), - services: dco_decode_list_service_info(arr[9]), - folderColor: dco_decode_String(arr[10]), - ); - } - - @protected - (String, String) dco_decode_record_string_string(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 2) { - throw Exception('Expected 2 elements, got ${arr.length}'); - } - return (dco_decode_String(arr[0]), dco_decode_String(arr[1])); - } - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 3) - throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); - return ScrollInfo( - totalLines: dco_decode_u_32(arr[0]), - visibleLines: dco_decode_u_32(arr[1]), - displayOffset: dco_decode_u_32(arr[2]), - ); - } - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); - return SelectionBounds( - startCol: dco_decode_u_16(arr[0]), - startRow: dco_decode_i_32(arr[1]), - endCol: dco_decode_u_16(arr[2]), - endRow: dco_decode_i_32(arr[3]), - ); - } - - @protected - ServiceInfo dco_decode_service_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 7) - throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); - return ServiceInfo( - name: dco_decode_String(arr[0]), - status: dco_decode_String(arr[1]), - terminalId: dco_decode_opt_String(arr[2]), - ports: dco_decode_list_prim_u_16_strict(arr[3]), - exitCode: dco_decode_opt_box_autoadd_u_32(arr[4]), - kind: dco_decode_String(arr[5]), - isExtra: dco_decode_bool(arr[6]), - ); - } - - @protected - int dco_decode_u_16(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - int dco_decode_u_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - int dco_decode_u_8(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - - @protected - void dco_decode_unit(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return; - } - - @protected - BigInt dco_decode_usize(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dcoDecodeU64(raw); - } - - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_String(deserializer); - return AnyhowException(inner); - } - - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_list_record_string_string(deserializer); - return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); - } - - @protected - String sse_decode_String(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_list_prim_u_8_strict(deserializer); - return utf8.decoder.convert(inner); - } - - @protected - bool sse_decode_bool(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint8() != 0; - } - - @protected - FullscreenInfo sse_decode_box_autoadd_fullscreen_info( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_fullscreen_info(deserializer)); - } - - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_selection_bounds(deserializer)); - } - - @protected - int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_u_32(deserializer)); - } - - @protected - BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_usize(deserializer)); - } - - @protected - CellData sse_decode_cell_data(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_character = sse_decode_String(deserializer); - var var_fg = sse_decode_u_32(deserializer); - var var_bg = sse_decode_u_32(deserializer); - var var_flags = sse_decode_u_8(deserializer); - return CellData( - character: var_character, - fg: var_fg, - bg: var_bg, - flags: var_flags, - ); - } - - @protected - ConnectionStatus sse_decode_connection_status(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var tag_ = sse_decode_i_32(deserializer); - switch (tag_) { - case 0: - return ConnectionStatus_Disconnected(); - case 1: - return ConnectionStatus_Connecting(); - case 2: - return ConnectionStatus_Connected(); - case 3: - return ConnectionStatus_Pairing(); - case 4: - var var_message = sse_decode_String(deserializer); - return ConnectionStatus_Error(message: var_message); - default: - throw UnimplementedError(''); - } - } - - @protected - CursorShape sse_decode_cursor_shape(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_i_32(deserializer); - return CursorShape.values[inner]; - } - - @protected - CursorState sse_decode_cursor_state(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_col = sse_decode_u_16(deserializer); - var var_row = sse_decode_u_16(deserializer); - var var_shape = sse_decode_cursor_shape(deserializer); - var var_visible = sse_decode_bool(deserializer); - return CursorState( - col: var_col, - row: var_row, - shape: var_shape, - visible: var_visible, - ); - } - - @protected - double sse_decode_f_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getFloat32(); - } - - @protected - double sse_decode_f_64(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getFloat64(); - } - - @protected - FolderInfo sse_decode_folder_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_id = sse_decode_String(deserializer); - var var_name = sse_decode_String(deserializer); - var var_projectIds = sse_decode_list_String(deserializer); - var var_folderColor = sse_decode_String(deserializer); - return FolderInfo( - id: var_id, - name: var_name, - projectIds: var_projectIds, - folderColor: var_folderColor, - ); - } - - @protected - FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_projectId = sse_decode_String(deserializer); - var var_terminalId = sse_decode_String(deserializer); - return FullscreenInfo(projectId: var_projectId, terminalId: var_terminalId); - } - - @protected - int sse_decode_i_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getInt32(); - } - - @protected - List sse_decode_list_String(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_String(deserializer)); - } - return ans_; - } - - @protected - List sse_decode_list_cell_data(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_cell_data(deserializer)); - } - return ans_; - } - - @protected - List sse_decode_list_folder_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_folder_info(deserializer)); - } - return ans_; - } - - @protected - List sse_decode_list_prim_f_32_loose(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getFloat32List(len_); - } - - @protected - Float32List sse_decode_list_prim_f_32_strict(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getFloat32List(len_); - } - - @protected - Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getUint16List(len_); - } - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getUint8List(len_); - } - - @protected - Uint64List sse_decode_list_prim_usize_strict(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var len_ = sse_decode_i_32(deserializer); - return deserializer.buffer.getUint64List(len_); - } - - @protected - List sse_decode_list_project_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_project_info(deserializer)); - } - return ans_; - } - - @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = <(String, String)>[]; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_record_string_string(deserializer)); - } - return ans_; - } - - @protected - List sse_decode_list_service_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_service_info(deserializer)); - } - return ans_; - } - - @protected - String? sse_decode_opt_String(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_String(deserializer)); - } else { - return null; - } - } - - @protected - FullscreenInfo? sse_decode_opt_box_autoadd_fullscreen_info( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_fullscreen_info(deserializer)); - } else { - return null; - } - } - - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_selection_bounds(deserializer)); - } else { - return null; - } - } - - @protected - int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_u_32(deserializer)); - } else { - return null; - } - } - - @protected - BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_usize(deserializer)); - } else { - return null; - } - } - - @protected - ProjectInfo sse_decode_project_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_id = sse_decode_String(deserializer); - var var_name = sse_decode_String(deserializer); - var var_path = sse_decode_String(deserializer); - var var_showInOverview = sse_decode_bool(deserializer); - var var_terminalIds = sse_decode_list_String(deserializer); - var var_terminalNames = sse_decode_Map_String_String_None(deserializer); - var var_gitBranch = sse_decode_opt_String(deserializer); - var var_gitLinesAdded = sse_decode_u_32(deserializer); - var var_gitLinesRemoved = sse_decode_u_32(deserializer); - var var_services = sse_decode_list_service_info(deserializer); - var var_folderColor = sse_decode_String(deserializer); - return ProjectInfo( - id: var_id, - name: var_name, - path: var_path, - showInOverview: var_showInOverview, - terminalIds: var_terminalIds, - terminalNames: var_terminalNames, - gitBranch: var_gitBranch, - gitLinesAdded: var_gitLinesAdded, - gitLinesRemoved: var_gitLinesRemoved, - services: var_services, - folderColor: var_folderColor, - ); - } - - @protected - (String, String) sse_decode_record_string_string( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_field0 = sse_decode_String(deserializer); - var var_field1 = sse_decode_String(deserializer); - return (var_field0, var_field1); - } - - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_totalLines = sse_decode_u_32(deserializer); - var var_visibleLines = sse_decode_u_32(deserializer); - var var_displayOffset = sse_decode_u_32(deserializer); - return ScrollInfo( - totalLines: var_totalLines, - visibleLines: var_visibleLines, - displayOffset: var_displayOffset, - ); - } - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_startCol = sse_decode_u_16(deserializer); - var var_startRow = sse_decode_i_32(deserializer); - var var_endCol = sse_decode_u_16(deserializer); - var var_endRow = sse_decode_i_32(deserializer); - return SelectionBounds( - startCol: var_startCol, - startRow: var_startRow, - endCol: var_endCol, - endRow: var_endRow, - ); - } - - @protected - ServiceInfo sse_decode_service_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_name = sse_decode_String(deserializer); - var var_status = sse_decode_String(deserializer); - var var_terminalId = sse_decode_opt_String(deserializer); - var var_ports = sse_decode_list_prim_u_16_strict(deserializer); - var var_exitCode = sse_decode_opt_box_autoadd_u_32(deserializer); - var var_kind = sse_decode_String(deserializer); - var var_isExtra = sse_decode_bool(deserializer); - return ServiceInfo( - name: var_name, - status: var_status, - terminalId: var_terminalId, - ports: var_ports, - exitCode: var_exitCode, - kind: var_kind, - isExtra: var_isExtra, - ); - } - - @protected - int sse_decode_u_16(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint16(); - } - - @protected - int sse_decode_u_32(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint32(); - } - - @protected - int sse_decode_u_8(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint8(); - } - - @protected - void sse_decode_unit(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - } - - @protected - BigInt sse_decode_usize(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getBigUint64(); - } - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.message, serializer); - } - - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_record_string_string( - self.entries.map((e) => (e.key, e.value)).toList(), - serializer, - ); - } - - @protected - void sse_encode_String(String self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); - } - - @protected - void sse_encode_bool(bool self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self ? 1 : 0); - } - - @protected - void sse_encode_box_autoadd_fullscreen_info( - FullscreenInfo self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_fullscreen_info(self, serializer); - } - - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_selection_bounds(self, serializer); - } - - @protected - void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_32(self, serializer); - } - - @protected - void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize(self, serializer); - } - - @protected - void sse_encode_cell_data(CellData self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.character, serializer); - sse_encode_u_32(self.fg, serializer); - sse_encode_u_32(self.bg, serializer); - sse_encode_u_8(self.flags, serializer); - } - - @protected - void sse_encode_connection_status( - ConnectionStatus self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - switch (self) { - case ConnectionStatus_Disconnected(): - sse_encode_i_32(0, serializer); - case ConnectionStatus_Connecting(): - sse_encode_i_32(1, serializer); - case ConnectionStatus_Connected(): - sse_encode_i_32(2, serializer); - case ConnectionStatus_Pairing(): - sse_encode_i_32(3, serializer); - case ConnectionStatus_Error(message: final message): - sse_encode_i_32(4, serializer); - sse_encode_String(message, serializer); - } - } - - @protected - void sse_encode_cursor_shape(CursorShape self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.index, serializer); - } - - @protected - void sse_encode_cursor_state(CursorState self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_16(self.col, serializer); - sse_encode_u_16(self.row, serializer); - sse_encode_cursor_shape(self.shape, serializer); - sse_encode_bool(self.visible, serializer); - } - - @protected - void sse_encode_f_32(double self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putFloat32(self); - } - - @protected - void sse_encode_f_64(double self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putFloat64(self); - } - - @protected - void sse_encode_folder_info(FolderInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.id, serializer); - sse_encode_String(self.name, serializer); - sse_encode_list_String(self.projectIds, serializer); - sse_encode_String(self.folderColor, serializer); - } - - @protected - void sse_encode_fullscreen_info( - FullscreenInfo self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.projectId, serializer); - sse_encode_String(self.terminalId, serializer); - } - - @protected - void sse_encode_i_32(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putInt32(self); - } - - @protected - void sse_encode_list_String(List self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_String(item, serializer); - } - } - - @protected - void sse_encode_list_cell_data( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_cell_data(item, serializer); - } - } - - @protected - void sse_encode_list_folder_info( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_folder_info(item, serializer); - } - } - - @protected - void sse_encode_list_prim_f_32_loose( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putFloat32List( - self is Float32List ? self : Float32List.fromList(self), - ); - } - - @protected - void sse_encode_list_prim_f_32_strict( - Float32List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putFloat32List(self); - } - - @protected - void sse_encode_list_prim_u_16_strict( - Uint16List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putUint16List(self); - } - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putUint8List(self); - } - - @protected - void sse_encode_list_prim_usize_strict( - Uint64List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - serializer.buffer.putUint64List(self); - } - - @protected - void sse_encode_list_project_info( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_project_info(item, serializer); - } - } - - @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_record_string_string(item, serializer); - } - } - - @protected - void sse_encode_list_service_info( - List self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_service_info(item, serializer); - } - } - - @protected - void sse_encode_opt_String(String? self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_String(self, serializer); - } - } - - @protected - void sse_encode_opt_box_autoadd_fullscreen_info( - FullscreenInfo? self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_fullscreen_info(self, serializer); - } - } - - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_selection_bounds(self, serializer); - } - } - - @protected - void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_u_32(self, serializer); - } - } - - @protected - void sse_encode_opt_box_autoadd_usize( - BigInt? self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_usize(self, serializer); - } - } - - @protected - void sse_encode_project_info(ProjectInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.id, serializer); - sse_encode_String(self.name, serializer); - sse_encode_String(self.path, serializer); - sse_encode_bool(self.showInOverview, serializer); - sse_encode_list_String(self.terminalIds, serializer); - sse_encode_Map_String_String_None(self.terminalNames, serializer); - sse_encode_opt_String(self.gitBranch, serializer); - sse_encode_u_32(self.gitLinesAdded, serializer); - sse_encode_u_32(self.gitLinesRemoved, serializer); - sse_encode_list_service_info(self.services, serializer); - sse_encode_String(self.folderColor, serializer); - } - - @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.$1, serializer); - sse_encode_String(self.$2, serializer); - } - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_32(self.totalLines, serializer); - sse_encode_u_32(self.visibleLines, serializer); - sse_encode_u_32(self.displayOffset, serializer); - } - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_u_16(self.startCol, serializer); - sse_encode_i_32(self.startRow, serializer); - sse_encode_u_16(self.endCol, serializer); - sse_encode_i_32(self.endRow, serializer); - } - - @protected - void sse_encode_service_info(ServiceInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.name, serializer); - sse_encode_String(self.status, serializer); - sse_encode_opt_String(self.terminalId, serializer); - sse_encode_list_prim_u_16_strict(self.ports, serializer); - sse_encode_opt_box_autoadd_u_32(self.exitCode, serializer); - sse_encode_String(self.kind, serializer); - sse_encode_bool(self.isExtra, serializer); - } - - @protected - void sse_encode_u_16(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint16(self); - } - - @protected - void sse_encode_u_32(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint32(self); - } - - @protected - void sse_encode_u_8(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self); - } - - @protected - void sse_encode_unit(void self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - } - - @protected - void sse_encode_usize(BigInt self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putBigUint64(self); - } -} diff --git a/mobile/lib/src/rust/frb_generated.io.dart b/mobile/lib/src/rust/frb_generated.io.dart deleted file mode 100644 index ac9322e6..00000000 --- a/mobile/lib/src/rust/frb_generated.io.dart +++ /dev/null @@ -1,495 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -import 'api/connection.dart'; -import 'api/state.dart'; -import 'api/terminal.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi' as ffi; -import 'frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; - -abstract class RustLibApiImplPlatform extends BaseApiImpl { - RustLibApiImplPlatform({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw); - - @protected - Map dco_decode_Map_String_String_None(dynamic raw); - - @protected - String dco_decode_String(dynamic raw); - - @protected - bool dco_decode_bool(dynamic raw); - - @protected - FullscreenInfo dco_decode_box_autoadd_fullscreen_info(dynamic raw); - - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); - - @protected - int dco_decode_box_autoadd_u_32(dynamic raw); - - @protected - BigInt dco_decode_box_autoadd_usize(dynamic raw); - - @protected - CellData dco_decode_cell_data(dynamic raw); - - @protected - ConnectionStatus dco_decode_connection_status(dynamic raw); - - @protected - CursorShape dco_decode_cursor_shape(dynamic raw); - - @protected - CursorState dco_decode_cursor_state(dynamic raw); - - @protected - double dco_decode_f_32(dynamic raw); - - @protected - double dco_decode_f_64(dynamic raw); - - @protected - FolderInfo dco_decode_folder_info(dynamic raw); - - @protected - FullscreenInfo dco_decode_fullscreen_info(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - List dco_decode_list_String(dynamic raw); - - @protected - List dco_decode_list_cell_data(dynamic raw); - - @protected - List dco_decode_list_folder_info(dynamic raw); - - @protected - List dco_decode_list_prim_f_32_loose(dynamic raw); - - @protected - Float32List dco_decode_list_prim_f_32_strict(dynamic raw); - - @protected - Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); - - @protected - Uint64List dco_decode_list_prim_usize_strict(dynamic raw); - - @protected - List dco_decode_list_project_info(dynamic raw); - - @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw); - - @protected - List dco_decode_list_service_info(dynamic raw); - - @protected - String? dco_decode_opt_String(dynamic raw); - - @protected - FullscreenInfo? dco_decode_opt_box_autoadd_fullscreen_info(dynamic raw); - - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); - - @protected - int? dco_decode_opt_box_autoadd_u_32(dynamic raw); - - @protected - BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); - - @protected - ProjectInfo dco_decode_project_info(dynamic raw); - - @protected - (String, String) dco_decode_record_string_string(dynamic raw); - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw); - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw); - - @protected - ServiceInfo dco_decode_service_info(dynamic raw); - - @protected - int dco_decode_u_16(dynamic raw); - - @protected - int dco_decode_u_32(dynamic raw); - - @protected - int dco_decode_u_8(dynamic raw); - - @protected - void dco_decode_unit(dynamic raw); - - @protected - BigInt dco_decode_usize(dynamic raw); - - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - FullscreenInfo sse_decode_box_autoadd_fullscreen_info( - SseDeserializer deserializer, - ); - - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); - - @protected - BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); - - @protected - CellData sse_decode_cell_data(SseDeserializer deserializer); - - @protected - ConnectionStatus sse_decode_connection_status(SseDeserializer deserializer); - - @protected - CursorShape sse_decode_cursor_shape(SseDeserializer deserializer); - - @protected - CursorState sse_decode_cursor_state(SseDeserializer deserializer); - - @protected - double sse_decode_f_32(SseDeserializer deserializer); - - @protected - double sse_decode_f_64(SseDeserializer deserializer); - - @protected - FolderInfo sse_decode_folder_info(SseDeserializer deserializer); - - @protected - FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - List sse_decode_list_String(SseDeserializer deserializer); - - @protected - List sse_decode_list_cell_data(SseDeserializer deserializer); - - @protected - List sse_decode_list_folder_info(SseDeserializer deserializer); - - @protected - List sse_decode_list_prim_f_32_loose(SseDeserializer deserializer); - - @protected - Float32List sse_decode_list_prim_f_32_strict(SseDeserializer deserializer); - - @protected - Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - Uint64List sse_decode_list_prim_usize_strict(SseDeserializer deserializer); - - @protected - List sse_decode_list_project_info(SseDeserializer deserializer); - - @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ); - - @protected - List sse_decode_list_service_info(SseDeserializer deserializer); - - @protected - String? sse_decode_opt_String(SseDeserializer deserializer); - - @protected - FullscreenInfo? sse_decode_opt_box_autoadd_fullscreen_info( - SseDeserializer deserializer, - ); - - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); - - @protected - BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); - - @protected - ProjectInfo sse_decode_project_info(SseDeserializer deserializer); - - @protected - (String, String) sse_decode_record_string_string( - SseDeserializer deserializer, - ); - - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); - - @protected - ServiceInfo sse_decode_service_info(SseDeserializer deserializer); - - @protected - int sse_decode_u_16(SseDeserializer deserializer); - - @protected - int sse_decode_u_32(SseDeserializer deserializer); - - @protected - int sse_decode_u_8(SseDeserializer deserializer); - - @protected - void sse_decode_unit(SseDeserializer deserializer); - - @protected - BigInt sse_decode_usize(SseDeserializer deserializer); - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ); - - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_bool(bool self, SseSerializer serializer); - - @protected - void sse_encode_box_autoadd_fullscreen_info( - FullscreenInfo self, - SseSerializer serializer, - ); - - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); - - @protected - void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); - - @protected - void sse_encode_cell_data(CellData self, SseSerializer serializer); - - @protected - void sse_encode_connection_status( - ConnectionStatus self, - SseSerializer serializer, - ); - - @protected - void sse_encode_cursor_shape(CursorShape self, SseSerializer serializer); - - @protected - void sse_encode_cursor_state(CursorState self, SseSerializer serializer); - - @protected - void sse_encode_f_32(double self, SseSerializer serializer); - - @protected - void sse_encode_f_64(double self, SseSerializer serializer); - - @protected - void sse_encode_folder_info(FolderInfo self, SseSerializer serializer); - - @protected - void sse_encode_fullscreen_info( - FullscreenInfo self, - SseSerializer serializer, - ); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_String(List self, SseSerializer serializer); - - @protected - void sse_encode_list_cell_data(List self, SseSerializer serializer); - - @protected - void sse_encode_list_folder_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_f_32_loose( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_f_32_strict( - Float32List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_u_16_strict( - Uint16List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_usize_strict( - Uint64List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_project_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_service_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_String(String? self, SseSerializer serializer); - - @protected - void sse_encode_opt_box_autoadd_fullscreen_info( - FullscreenInfo? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); - - @protected - void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); - - @protected - void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); - - @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ); - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_service_info(ServiceInfo self, SseSerializer serializer); - - @protected - void sse_encode_u_16(int self, SseSerializer serializer); - - @protected - void sse_encode_u_32(int self, SseSerializer serializer); - - @protected - void sse_encode_u_8(int self, SseSerializer serializer); - - @protected - void sse_encode_unit(void self, SseSerializer serializer); - - @protected - void sse_encode_usize(BigInt self, SseSerializer serializer); -} - -// Section: wire_class - -class RustLibWire implements BaseWire { - factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => - RustLibWire(lib.ffiDynamicLibrary); - - /// Holds the symbol lookup function. - final ffi.Pointer Function(String symbolName) - _lookup; - - /// The symbols are looked up in [dynamicLibrary]. - RustLibWire(ffi.DynamicLibrary dynamicLibrary) - : _lookup = dynamicLibrary.lookup; -} diff --git a/mobile/lib/src/rust/frb_generated.web.dart b/mobile/lib/src/rust/frb_generated.web.dart deleted file mode 100644 index 8fd5550d..00000000 --- a/mobile/lib/src/rust/frb_generated.web.dart +++ /dev/null @@ -1,495 +0,0 @@ -// This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.11.1. - -// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field - -// Static analysis wrongly picks the IO variant, thus ignore this -// ignore_for_file: argument_type_not_assignable - -import 'api/connection.dart'; -import 'api/state.dart'; -import 'api/terminal.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'frb_generated.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; - -abstract class RustLibApiImplPlatform extends BaseApiImpl { - RustLibApiImplPlatform({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); - - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw); - - @protected - Map dco_decode_Map_String_String_None(dynamic raw); - - @protected - String dco_decode_String(dynamic raw); - - @protected - bool dco_decode_bool(dynamic raw); - - @protected - FullscreenInfo dco_decode_box_autoadd_fullscreen_info(dynamic raw); - - @protected - SelectionBounds dco_decode_box_autoadd_selection_bounds(dynamic raw); - - @protected - int dco_decode_box_autoadd_u_32(dynamic raw); - - @protected - BigInt dco_decode_box_autoadd_usize(dynamic raw); - - @protected - CellData dco_decode_cell_data(dynamic raw); - - @protected - ConnectionStatus dco_decode_connection_status(dynamic raw); - - @protected - CursorShape dco_decode_cursor_shape(dynamic raw); - - @protected - CursorState dco_decode_cursor_state(dynamic raw); - - @protected - double dco_decode_f_32(dynamic raw); - - @protected - double dco_decode_f_64(dynamic raw); - - @protected - FolderInfo dco_decode_folder_info(dynamic raw); - - @protected - FullscreenInfo dco_decode_fullscreen_info(dynamic raw); - - @protected - int dco_decode_i_32(dynamic raw); - - @protected - List dco_decode_list_String(dynamic raw); - - @protected - List dco_decode_list_cell_data(dynamic raw); - - @protected - List dco_decode_list_folder_info(dynamic raw); - - @protected - List dco_decode_list_prim_f_32_loose(dynamic raw); - - @protected - Float32List dco_decode_list_prim_f_32_strict(dynamic raw); - - @protected - Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); - - @protected - Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); - - @protected - Uint64List dco_decode_list_prim_usize_strict(dynamic raw); - - @protected - List dco_decode_list_project_info(dynamic raw); - - @protected - List<(String, String)> dco_decode_list_record_string_string(dynamic raw); - - @protected - List dco_decode_list_service_info(dynamic raw); - - @protected - String? dco_decode_opt_String(dynamic raw); - - @protected - FullscreenInfo? dco_decode_opt_box_autoadd_fullscreen_info(dynamic raw); - - @protected - SelectionBounds? dco_decode_opt_box_autoadd_selection_bounds(dynamic raw); - - @protected - int? dco_decode_opt_box_autoadd_u_32(dynamic raw); - - @protected - BigInt? dco_decode_opt_box_autoadd_usize(dynamic raw); - - @protected - ProjectInfo dco_decode_project_info(dynamic raw); - - @protected - (String, String) dco_decode_record_string_string(dynamic raw); - - @protected - ScrollInfo dco_decode_scroll_info(dynamic raw); - - @protected - SelectionBounds dco_decode_selection_bounds(dynamic raw); - - @protected - ServiceInfo dco_decode_service_info(dynamic raw); - - @protected - int dco_decode_u_16(dynamic raw); - - @protected - int dco_decode_u_32(dynamic raw); - - @protected - int dco_decode_u_8(dynamic raw); - - @protected - void dco_decode_unit(dynamic raw); - - @protected - BigInt dco_decode_usize(dynamic raw); - - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - - @protected - Map sse_decode_Map_String_String_None( - SseDeserializer deserializer, - ); - - @protected - String sse_decode_String(SseDeserializer deserializer); - - @protected - bool sse_decode_bool(SseDeserializer deserializer); - - @protected - FullscreenInfo sse_decode_box_autoadd_fullscreen_info( - SseDeserializer deserializer, - ); - - @protected - SelectionBounds sse_decode_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); - - @protected - BigInt sse_decode_box_autoadd_usize(SseDeserializer deserializer); - - @protected - CellData sse_decode_cell_data(SseDeserializer deserializer); - - @protected - ConnectionStatus sse_decode_connection_status(SseDeserializer deserializer); - - @protected - CursorShape sse_decode_cursor_shape(SseDeserializer deserializer); - - @protected - CursorState sse_decode_cursor_state(SseDeserializer deserializer); - - @protected - double sse_decode_f_32(SseDeserializer deserializer); - - @protected - double sse_decode_f_64(SseDeserializer deserializer); - - @protected - FolderInfo sse_decode_folder_info(SseDeserializer deserializer); - - @protected - FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer); - - @protected - int sse_decode_i_32(SseDeserializer deserializer); - - @protected - List sse_decode_list_String(SseDeserializer deserializer); - - @protected - List sse_decode_list_cell_data(SseDeserializer deserializer); - - @protected - List sse_decode_list_folder_info(SseDeserializer deserializer); - - @protected - List sse_decode_list_prim_f_32_loose(SseDeserializer deserializer); - - @protected - Float32List sse_decode_list_prim_f_32_strict(SseDeserializer deserializer); - - @protected - Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); - - @protected - Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); - - @protected - Uint64List sse_decode_list_prim_usize_strict(SseDeserializer deserializer); - - @protected - List sse_decode_list_project_info(SseDeserializer deserializer); - - @protected - List<(String, String)> sse_decode_list_record_string_string( - SseDeserializer deserializer, - ); - - @protected - List sse_decode_list_service_info(SseDeserializer deserializer); - - @protected - String? sse_decode_opt_String(SseDeserializer deserializer); - - @protected - FullscreenInfo? sse_decode_opt_box_autoadd_fullscreen_info( - SseDeserializer deserializer, - ); - - @protected - SelectionBounds? sse_decode_opt_box_autoadd_selection_bounds( - SseDeserializer deserializer, - ); - - @protected - int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); - - @protected - BigInt? sse_decode_opt_box_autoadd_usize(SseDeserializer deserializer); - - @protected - ProjectInfo sse_decode_project_info(SseDeserializer deserializer); - - @protected - (String, String) sse_decode_record_string_string( - SseDeserializer deserializer, - ); - - @protected - ScrollInfo sse_decode_scroll_info(SseDeserializer deserializer); - - @protected - SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); - - @protected - ServiceInfo sse_decode_service_info(SseDeserializer deserializer); - - @protected - int sse_decode_u_16(SseDeserializer deserializer); - - @protected - int sse_decode_u_32(SseDeserializer deserializer); - - @protected - int sse_decode_u_8(SseDeserializer deserializer); - - @protected - void sse_decode_unit(SseDeserializer deserializer); - - @protected - BigInt sse_decode_usize(SseDeserializer deserializer); - - @protected - void sse_encode_AnyhowException( - AnyhowException self, - SseSerializer serializer, - ); - - @protected - void sse_encode_Map_String_String_None( - Map self, - SseSerializer serializer, - ); - - @protected - void sse_encode_String(String self, SseSerializer serializer); - - @protected - void sse_encode_bool(bool self, SseSerializer serializer); - - @protected - void sse_encode_box_autoadd_fullscreen_info( - FullscreenInfo self, - SseSerializer serializer, - ); - - @protected - void sse_encode_box_autoadd_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); - - @protected - void sse_encode_box_autoadd_usize(BigInt self, SseSerializer serializer); - - @protected - void sse_encode_cell_data(CellData self, SseSerializer serializer); - - @protected - void sse_encode_connection_status( - ConnectionStatus self, - SseSerializer serializer, - ); - - @protected - void sse_encode_cursor_shape(CursorShape self, SseSerializer serializer); - - @protected - void sse_encode_cursor_state(CursorState self, SseSerializer serializer); - - @protected - void sse_encode_f_32(double self, SseSerializer serializer); - - @protected - void sse_encode_f_64(double self, SseSerializer serializer); - - @protected - void sse_encode_folder_info(FolderInfo self, SseSerializer serializer); - - @protected - void sse_encode_fullscreen_info( - FullscreenInfo self, - SseSerializer serializer, - ); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); - - @protected - void sse_encode_list_String(List self, SseSerializer serializer); - - @protected - void sse_encode_list_cell_data(List self, SseSerializer serializer); - - @protected - void sse_encode_list_folder_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_f_32_loose( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_f_32_strict( - Float32List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_u_16_strict( - Uint16List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_u_8_strict( - Uint8List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_prim_usize_strict( - Uint64List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_project_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_record_string_string( - List<(String, String)> self, - SseSerializer serializer, - ); - - @protected - void sse_encode_list_service_info( - List self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_String(String? self, SseSerializer serializer); - - @protected - void sse_encode_opt_box_autoadd_fullscreen_info( - FullscreenInfo? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_box_autoadd_selection_bounds( - SelectionBounds? self, - SseSerializer serializer, - ); - - @protected - void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); - - @protected - void sse_encode_opt_box_autoadd_usize(BigInt? self, SseSerializer serializer); - - @protected - void sse_encode_project_info(ProjectInfo self, SseSerializer serializer); - - @protected - void sse_encode_record_string_string( - (String, String) self, - SseSerializer serializer, - ); - - @protected - void sse_encode_scroll_info(ScrollInfo self, SseSerializer serializer); - - @protected - void sse_encode_selection_bounds( - SelectionBounds self, - SseSerializer serializer, - ); - - @protected - void sse_encode_service_info(ServiceInfo self, SseSerializer serializer); - - @protected - void sse_encode_u_16(int self, SseSerializer serializer); - - @protected - void sse_encode_u_32(int self, SseSerializer serializer); - - @protected - void sse_encode_u_8(int self, SseSerializer serializer); - - @protected - void sse_encode_unit(void self, SseSerializer serializer); - - @protected - void sse_encode_usize(BigInt self, SseSerializer serializer); -} - -// Section: wire_class - -class RustLibWire implements BaseWire { - RustLibWire.fromExternalLibrary(ExternalLibrary lib); -} - -@JS('wasm_bindgen') -external RustLibWasmModule get wasmModule; - -@JS() -@anonymous -extension type RustLibWasmModule._(JSObject _) implements JSObject {} diff --git a/mobile/lib/src/screens/pairing_screen.dart b/mobile/lib/src/screens/pairing_screen.dart deleted file mode 100644 index 82062cb8..00000000 --- a/mobile/lib/src/screens/pairing_screen.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -import '../providers/connection_provider.dart'; -import '../theme/app_theme.dart'; -import '../widgets/status_indicator.dart'; -import '../../src/rust/api/connection.dart'; - -class PairingScreen extends StatefulWidget { - const PairingScreen({super.key}); - - @override - State createState() => _PairingScreenState(); -} - -class _PairingScreenState extends State { - final _codeController = TextEditingController(); - bool _submitting = false; - - @override - void dispose() { - _codeController.dispose(); - super.dispose(); - } - - Future _submitCode() async { - final code = _codeController.text.trim(); - if (code.isEmpty) return; - - HapticFeedback.mediumImpact(); - setState(() => _submitting = true); - final provider = context.read(); - await provider.pair(code); - if (mounted) { - setState(() => _submitting = false); - } - } - - @override - Widget build(BuildContext context) { - final provider = context.watch(); - final showCodeInput = provider.isPairing; - final isError = provider.status is ConnectionStatus_Error; - final errorMessage = isError - ? (provider.status as ConnectionStatus_Error).message - : null; - - return Scaffold( - backgroundColor: OkenaColors.background, - body: SafeArea( - child: Column( - children: [ - // Header - Padding( - padding: const EdgeInsets.fromLTRB(4, 4, 16, 0), - child: Row( - children: [ - IconButton( - icon: const Icon( - CupertinoIcons.chevron_back, - color: OkenaColors.accent, - size: 22, - ), - onPressed: () => provider.disconnect(), - ), - const SizedBox(width: 4), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - provider.activeServer?.displayName ?? 'Server', - style: OkenaTypography.headline, - ), - const SizedBox(height: 1), - Text( - 'Connecting', - style: OkenaTypography.caption2.copyWith( - color: OkenaColors.textTertiary, - ), - ), - ], - ), - ), - ], - ), - ), - // Body - Expanded( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: StatusIndicator(status: provider.status), - ), - const SizedBox(height: 40), - if (!showCodeInput && !isError) ...[ - const Center( - child: CupertinoActivityIndicator( - radius: 14, - color: OkenaColors.textSecondary, - ), - ), - const SizedBox(height: 20), - Text( - 'Connecting to server...', - textAlign: TextAlign.center, - style: OkenaTypography.body.copyWith( - color: OkenaColors.textSecondary, - ), - ), - ], - if (showCodeInput) ...[ - Text( - 'Pair with Server', - textAlign: TextAlign.center, - style: OkenaTypography.largeTitle, - ), - const SizedBox(height: 8), - Text( - 'Check the Okena desktop app for the pairing code.', - textAlign: TextAlign.center, - style: OkenaTypography.body.copyWith( - color: OkenaColors.textSecondary, - ), - ), - const SizedBox(height: 32), - TextField( - controller: _codeController, - decoration: const InputDecoration( - hintText: 'XXXX-XXXX', - ), - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 28, - letterSpacing: 8, - fontFamily: 'JetBrainsMono', - fontWeight: FontWeight.w500, - color: OkenaColors.textPrimary, - ), - textCapitalization: TextCapitalization.characters, - keyboardType: TextInputType.text, - autofocus: true, - onSubmitted: (_) => _submitCode(), - ), - const SizedBox(height: 24), - FilledButton( - onPressed: _submitting ? null : _submitCode, - child: _submitting - ? const CupertinoActivityIndicator( - radius: 10, - color: Colors.white, - ) - : const Text('Pair'), - ), - ], - if (isError) ...[ - Center( - child: Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: OkenaColors.error.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - CupertinoIcons.xmark_circle, - size: 32, - color: OkenaColors.error, - ), - ), - ), - const SizedBox(height: 20), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: OkenaColors.error.withOpacity(0.08), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: OkenaColors.error.withOpacity(0.2), - width: 0.5, - ), - ), - child: Text( - errorMessage ?? 'Connection failed', - textAlign: TextAlign.center, - style: OkenaTypography.body.copyWith( - color: OkenaColors.error, - ), - ), - ), - const SizedBox(height: 24), - OutlinedButton( - onPressed: () { - final server = provider.activeServer; - if (server != null) { - provider.disconnect(); - provider.connectTo(server); - } - }, - child: const Text('Try Again'), - ), - ], - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/src/screens/server_list_screen.dart b/mobile/lib/src/screens/server_list_screen.dart deleted file mode 100644 index cac98005..00000000 --- a/mobile/lib/src/screens/server_list_screen.dart +++ /dev/null @@ -1,278 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -import '../models/saved_server.dart'; -import '../providers/connection_provider.dart'; -import '../theme/app_theme.dart'; - -class ServerListScreen extends StatelessWidget { - const ServerListScreen({super.key}); - - @override - Widget build(BuildContext context) { - final provider = context.watch(); - - return Scaffold( - backgroundColor: OkenaColors.background, - body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Padding( - padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Servers', style: OkenaTypography.largeTitle), - GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - _showAddServerSheet(context); - }, - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: OkenaColors.surfaceElevated, - shape: BoxShape.circle, - border: Border.all(color: OkenaColors.border, width: 0.5), - ), - child: const Icon( - CupertinoIcons.plus, - color: OkenaColors.accent, - size: 18, - ), - ), - ), - ], - ), - ), - // Content - Expanded( - child: provider.servers.isEmpty - ? _buildEmptyState(context) - : _buildServerList(context, provider), - ), - ], - ), - ), - ); - } - - Widget _buildEmptyState(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - OkenaColors.accent.withOpacity(0.15), - OkenaColors.accent.withOpacity(0.0), - ], - ), - ), - child: Icon( - Icons.terminal_rounded, - size: 36, - color: OkenaColors.accent.withOpacity(0.8), - ), - ), - const SizedBox(height: 20), - const Text('No servers yet', style: OkenaTypography.title), - const SizedBox(height: 8), - Text( - 'Add a server to get started', - style: OkenaTypography.body.copyWith(color: OkenaColors.textSecondary), - ), - const SizedBox(height: 28), - SizedBox( - width: 180, - child: FilledButton( - onPressed: () => _showAddServerSheet(context), - child: const Text('Add Server'), - ), - ), - ], - ), - ); - } - - Widget _buildServerList(BuildContext context, ConnectionProvider provider) { - return ListView.builder( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), - itemCount: provider.servers.length, - itemBuilder: (context, index) { - final server = provider.servers[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Dismissible( - key: ValueKey('${server.host}:${server.port}'), - direction: DismissDirection.endToStart, - background: Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 24), - decoration: BoxDecoration( - color: OkenaColors.error.withOpacity(0.15), - borderRadius: BorderRadius.circular(14), - ), - child: const Icon(CupertinoIcons.delete, color: OkenaColors.error, size: 20), - ), - onDismissed: (_) => provider.removeServer(server), - child: GestureDetector( - onTap: () { - HapticFeedback.selectionClick(); - provider.connectTo(server); - }, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: OkenaColors.surface, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: OkenaColors.border, width: 0.5), - ), - child: Row( - children: [ - // Letter avatar - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: OkenaColors.accent.withOpacity(0.12), - borderRadius: BorderRadius.circular(10), - ), - alignment: Alignment.center, - child: Text( - server.displayName[0].toUpperCase(), - style: OkenaTypography.headline.copyWith( - color: OkenaColors.accent, - ), - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - server.displayName, - style: OkenaTypography.body.copyWith( - fontWeight: FontWeight.w500, - ), - ), - if (server.label != null) ...[ - const SizedBox(height: 3), - Text( - '${server.host}:${server.port}', - style: OkenaTypography.caption.copyWith( - fontFamily: 'JetBrainsMono', - color: OkenaColors.textTertiary, - ), - ), - ], - ], - ), - ), - const Icon( - CupertinoIcons.chevron_right, - color: OkenaColors.textTertiary, - size: 16, - ), - ], - ), - ), - ), - ), - ); - }, - ); - } - - void _showAddServerSheet(BuildContext context) { - final hostController = TextEditingController(); - final portController = TextEditingController(text: '19100'); - final provider = context.read(); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: OkenaColors.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) => Padding( - padding: EdgeInsets.only( - left: 24, - right: 24, - top: 8, - bottom: MediaQuery.of(ctx).viewInsets.bottom + 24, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Drag handle - Center( - child: Container( - width: 36, - height: 4, - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - color: OkenaColors.textTertiary.withOpacity(0.4), - borderRadius: BorderRadius.circular(2), - ), - ), - ), - const Text('Add Server', style: OkenaTypography.title), - const SizedBox(height: 4), - Text( - 'Enter the host and port of your Okena desktop app', - style: OkenaTypography.callout.copyWith(color: OkenaColors.textTertiary), - ), - const SizedBox(height: 24), - TextField( - controller: hostController, - decoration: const InputDecoration( - labelText: 'Host', - hintText: '192.168.1.100', - ), - autofocus: true, - keyboardType: TextInputType.url, - style: OkenaTypography.body, - ), - const SizedBox(height: 14), - TextField( - controller: portController, - decoration: const InputDecoration( - labelText: 'Port', - ), - keyboardType: TextInputType.number, - style: OkenaTypography.body, - ), - const SizedBox(height: 24), - FilledButton( - onPressed: () { - final host = hostController.text.trim(); - final port = - int.tryParse(portController.text.trim()) ?? 19100; - if (host.isNotEmpty) { - HapticFeedback.mediumImpact(); - provider.addServer(SavedServer(host: host, port: port)); - Navigator.of(ctx).pop(); - } - }, - child: const Text('Add Server'), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/src/screens/workspace_screen.dart b/mobile/lib/src/screens/workspace_screen.dart deleted file mode 100644 index 73c7285e..00000000 --- a/mobile/lib/src/screens/workspace_screen.dart +++ /dev/null @@ -1,1369 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' show Uint64List; -import 'package:provider/provider.dart'; - -import '../providers/connection_provider.dart'; -import '../providers/workspace_provider.dart'; -import '../rust/api/state.dart' as state_ffi; -import '../widgets/project_drawer.dart'; -import '../widgets/key_toolbar.dart'; -import '../widgets/terminal_view.dart'; - -class WorkspaceScreen extends StatefulWidget { - const WorkspaceScreen({super.key}); - - @override - State createState() => _WorkspaceScreenState(); -} - -class _WorkspaceScreenState extends State { - final KeyModifiers _modifiers = KeyModifiers(); - - @override - void dispose() { - _modifiers.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final workspace = context.watch(); - final connection = context.watch(); - final project = workspace.selectedProject; - final connId = connection.connId; - final selectedTerminalId = workspace.selectedTerminalId; - - return Scaffold( - appBar: AppBar( - title: _ProjectSwitcher( - projects: workspace.projects, - selectedProjectId: workspace.selectedProjectId, - onSelect: (id) => workspace.selectProject(id), - ), - leading: Builder( - builder: (ctx) => IconButton( - icon: const Icon(Icons.menu), - onPressed: () => Scaffold.of(ctx).openDrawer(), - ), - ), - actions: [ - // Connection quality indicator - if (connId != null) - Padding( - padding: const EdgeInsets.only(right: 4), - child: _ConnectionDot( - secondsSinceActivity: workspace.secondsSinceActivity, - ), - ), - // Git status button - if (connId != null && project != null && project.gitBranch != null) - _GitButton(connId: connId, project: project), - // Services button - if (connId != null && - project != null && - project.services.isNotEmpty) - _ServicesButton(connId: connId, project: project), - // Fullscreen toggle - if (connId != null && project != null && selectedTerminalId != null) - IconButton( - icon: Icon( - workspace.fullscreenTerminal != null - ? Icons.fullscreen_exit - : Icons.fullscreen, - size: 20, - ), - tooltip: workspace.fullscreenTerminal != null - ? 'Exit Fullscreen' - : 'Fullscreen', - onPressed: () { - if (workspace.fullscreenTerminal != null) { - state_ffi.setFullscreen( - connId: connId, - projectId: project.id, - terminalId: null, - ); - } else { - state_ffi.setFullscreen( - connId: connId, - projectId: project.id, - terminalId: selectedTerminalId, - ); - } - }, - ), - // More actions (split, minimize, new terminal) - if (connId != null && project != null) - PopupMenuButton( - icon: const Icon(Icons.add, size: 22), - tooltip: 'Terminal actions', - itemBuilder: (ctx) => [ - const PopupMenuItem( - value: 'new', - child: ListTile( - leading: Icon(Icons.add, size: 20), - title: Text('New Terminal'), - dense: true, - contentPadding: EdgeInsets.zero, - ), - ), - if (selectedTerminalId != null) ...[ - const PopupMenuItem( - value: 'split_vertical', - child: ListTile( - leading: Icon(Icons.vertical_split, size: 20), - title: Text('Split Vertical'), - dense: true, - contentPadding: EdgeInsets.zero, - ), - ), - const PopupMenuItem( - value: 'split_horizontal', - child: ListTile( - leading: Icon(Icons.horizontal_split, size: 20), - title: Text('Split Horizontal'), - dense: true, - contentPadding: EdgeInsets.zero, - ), - ), - const PopupMenuItem( - value: 'minimize', - child: ListTile( - leading: Icon(Icons.minimize, size: 20), - title: Text('Minimize'), - dense: true, - contentPadding: EdgeInsets.zero, - ), - ), - ], - ], - onSelected: (value) { - switch (value) { - case 'new': - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); - break; - case 'split_vertical': - state_ffi.splitTerminal( - connId: connId, - projectId: project.id, - path: Uint64List.fromList([]), - direction: 'vertical', - ); - break; - case 'split_horizontal': - state_ffi.splitTerminal( - connId: connId, - projectId: project.id, - path: Uint64List.fromList([]), - direction: 'horizontal', - ); - break; - case 'minimize': - if (selectedTerminalId != null) { - state_ffi.toggleMinimized( - connId: connId, - projectId: project.id, - terminalId: selectedTerminalId, - ); - } - break; - } - }, - ), - ], - ), - drawer: const ProjectDrawer(), - body: connId == null || project == null - ? const Center(child: Text('No project selected')) - : selectedTerminalId == null - ? Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'No terminals', - style: TextStyle(color: Colors.grey), - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: () { - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); - }, - icon: const Icon(Icons.add), - label: const Text('New Terminal'), - ), - ], - ), - ) - : Column( - children: [ - if (project.terminalIds.length > 1) - _TerminalTabBar( - terminalIds: project.terminalIds, - terminalNames: project.terminalNames, - selectedTerminalId: selectedTerminalId, - projectId: project.id, - connId: connId, - onSelect: (id) => workspace.selectTerminal(id), - ), - Expanded( - child: TerminalView( - connId: connId, - terminalId: selectedTerminalId, - modifiers: _modifiers, - ), - ), - KeyToolbar( - connId: connId, - terminalId: selectedTerminalId, - modifiers: _modifiers, - ), - ], - ), - ); - } -} - -/// Tappable project name in AppBar that opens a dropdown to switch projects. -class _ProjectSwitcher extends StatelessWidget { - final List projects; - final String? selectedProjectId; - final ValueChanged onSelect; - - const _ProjectSwitcher({ - required this.projects, - required this.selectedProjectId, - required this.onSelect, - }); - - @override - Widget build(BuildContext context) { - final selected = projects - .where((p) => p.id == selectedProjectId) - .firstOrNull ?? - projects.firstOrNull; - final name = selected?.name ?? 'No Project'; - - if (projects.length <= 1) { - return Text(name); - } - - return GestureDetector( - onTap: () => _showProjectMenu(context), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - name, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 4), - const Icon(Icons.arrow_drop_down, size: 20), - ], - ), - ); - } - - void _showProjectMenu(BuildContext context) { - final RenderBox button = context.findRenderObject() as RenderBox; - final overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - final position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(Offset(0, button.size.height), ancestor: overlay), - button.localToGlobal(button.size.bottomRight(Offset.zero), - ancestor: overlay), - ), - Offset.zero & overlay.size, - ); - - showMenu( - context: context, - position: position, - items: projects.map((p) { - return PopupMenuItem( - value: p.id, - child: Row( - children: [ - Icon( - Icons.folder, - size: 18, - color: p.id == selectedProjectId - ? Theme.of(context).colorScheme.primary - : null, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - p.name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: p.id == selectedProjectId - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ), - if (p.gitBranch != null) ...[ - const SizedBox(width: 8), - Icon(Icons.commit, size: 14, color: Colors.grey[500]), - const SizedBox(width: 2), - Text( - p.gitBranch!, - style: TextStyle(fontSize: 11, color: Colors.grey[500]), - ), - ], - ], - ), - ); - }).toList(), - ).then((value) { - if (value != null) { - onSelect(value); - } - }); - } -} - -/// Horizontal tab bar showing terminals in the current project. -class _TerminalTabBar extends StatelessWidget { - final List terminalIds; - final Map terminalNames; - final String selectedTerminalId; - final String projectId; - final String connId; - final ValueChanged onSelect; - - const _TerminalTabBar({ - required this.terminalIds, - required this.terminalNames, - required this.selectedTerminalId, - required this.projectId, - required this.connId, - required this.onSelect, - }); - - @override - Widget build(BuildContext context) { - return Container( - height: 36, - color: const Color(0xFF252526), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: terminalIds.length, - padding: const EdgeInsets.symmetric(horizontal: 4), - itemBuilder: (context, index) { - final tid = terminalIds[index]; - final isSelected = tid == selectedTerminalId; - final name = terminalNames[tid] ?? 'Terminal ${index + 1}'; - - return GestureDetector( - onTap: () => onSelect(tid), - onLongPress: () => _showTabMenu(context, tid, name), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF3C3C3C) - : Colors.transparent, - borderRadius: BorderRadius.circular(4), - ), - alignment: Alignment.center, - child: Text( - name, - style: TextStyle( - color: isSelected ? Colors.white : Colors.white54, - fontSize: 12, - fontFamily: 'JetBrainsMono', - fontWeight: - isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - ); - }, - ), - ); - } - - void _showTabMenu(BuildContext context, String terminalId, String name) { - showModalBottomSheet( - context: context, - builder: (ctx) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text(name, - style: Theme.of(context).textTheme.titleMedium), - ), - ListTile( - leading: const Icon(Icons.edit), - title: const Text('Rename'), - onTap: () { - Navigator.of(ctx).pop(); - _showRenameDialog(context, terminalId, name); - }, - ), - ListTile( - leading: const Icon(Icons.vertical_split), - title: const Text('Split Vertical'), - onTap: () { - Navigator.of(ctx).pop(); - state_ffi.splitTerminal( - connId: connId, - projectId: projectId, - path: Uint64List.fromList([]), - direction: 'vertical', - ); - }, - ), - ListTile( - leading: const Icon(Icons.horizontal_split), - title: const Text('Split Horizontal'), - onTap: () { - Navigator.of(ctx).pop(); - state_ffi.splitTerminal( - connId: connId, - projectId: projectId, - path: Uint64List.fromList([]), - direction: 'horizontal', - ); - }, - ), - ListTile( - leading: const Icon(Icons.minimize), - title: const Text('Minimize'), - onTap: () { - Navigator.of(ctx).pop(); - state_ffi.toggleMinimized( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - ), - ListTile( - leading: const Icon(Icons.close, color: Colors.redAccent), - title: const Text('Close', - style: TextStyle(color: Colors.redAccent)), - onTap: () { - Navigator.of(ctx).pop(); - state_ffi.closeTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - ), - ], - ), - ), - ); - } - - void _showRenameDialog( - BuildContext context, String terminalId, String currentName) { - final controller = TextEditingController(text: currentName); - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Rename Terminal'), - content: TextField( - controller: controller, - autofocus: true, - decoration: const InputDecoration(labelText: 'Name'), - onSubmitted: (_) { - Navigator.of(ctx).pop(); - state_ffi.renameTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - name: controller.text, - ); - }, - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(ctx).pop(); - state_ffi.renameTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - name: controller.text, - ); - }, - child: const Text('Rename'), - ), - ], - ), - ); - } -} - -/// Small colored dot indicating connection quality. -class _ConnectionDot extends StatelessWidget { - final double secondsSinceActivity; - - const _ConnectionDot({required this.secondsSinceActivity}); - - @override - Widget build(BuildContext context) { - final Color color; - if (secondsSinceActivity < 3) { - color = Colors.green; - } else if (secondsSinceActivity < 10) { - color = Colors.orange; - } else { - color = Colors.red; - } - - return Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - ); - } -} - -/// Git status button in the app bar. -class _GitButton extends StatelessWidget { - final String connId; - final state_ffi.ProjectInfo project; - - const _GitButton({required this.connId, required this.project}); - - @override - Widget build(BuildContext context) { - final hasChanges = project.gitLinesAdded > 0 || project.gitLinesRemoved > 0; - - return IconButton( - icon: Badge( - isLabelVisible: hasChanges, - smallSize: 8, - child: const Icon(Icons.commit, size: 20), - ), - tooltip: project.gitBranch ?? 'Git', - onPressed: () => _showGitSheet(context), - ); - } - - void _showGitSheet(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (ctx) => DraggableScrollableSheet( - initialChildSize: 0.6, - minChildSize: 0.3, - maxChildSize: 0.9, - expand: false, - builder: (ctx, scrollController) => _GitSheet( - connId: connId, - project: project, - scrollController: scrollController, - ), - ), - ); - } -} - -class _GitSheet extends StatefulWidget { - final String connId; - final state_ffi.ProjectInfo project; - final ScrollController scrollController; - - const _GitSheet({ - required this.connId, - required this.project, - required this.scrollController, - }); - - @override - State<_GitSheet> createState() => _GitSheetState(); -} - -class _GitSheetState extends State<_GitSheet> with SingleTickerProviderStateMixin { - String? _diffSummary; - String? _branches; - String? _gitStatus; - String? _workingTreeDiff; - String? _stagedDiff; - bool _loading = true; - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 4, vsync: this); - _loadData(); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - Future _loadData() async { - try { - final results = await Future.wait([ - state_ffi.gitDiffSummary( - connId: widget.connId, projectId: widget.project.id), - state_ffi.gitBranches( - connId: widget.connId, projectId: widget.project.id), - state_ffi.gitStatus( - connId: widget.connId, projectId: widget.project.id), - state_ffi.gitDiff( - connId: widget.connId, projectId: widget.project.id, mode: 'working_tree'), - state_ffi.gitDiff( - connId: widget.connId, projectId: widget.project.id, mode: 'staged'), - ]); - if (mounted) { - setState(() { - _diffSummary = results[0]; - _branches = results[1]; - _gitStatus = results[2]; - _workingTreeDiff = results[3]; - _stagedDiff = results[4]; - _loading = false; - }); - } - } catch (e) { - if (mounted) { - setState(() { - _loading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // Handle bar - Container( - margin: const EdgeInsets.only(top: 12, bottom: 8), - width: 32, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[600], - borderRadius: BorderRadius.circular(2), - ), - ), - // Header - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - const Icon(Icons.commit, size: 20), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.project.gitBranch ?? 'Unknown branch', - style: Theme.of(context).textTheme.titleMedium, - ), - Row( - children: [ - if (widget.project.gitLinesAdded > 0) - Text( - '+${widget.project.gitLinesAdded}', - style: TextStyle( - fontSize: 12, color: Colors.green[400]), - ), - if (widget.project.gitLinesAdded > 0 && - widget.project.gitLinesRemoved > 0) - const SizedBox(width: 8), - if (widget.project.gitLinesRemoved > 0) - Text( - '-${widget.project.gitLinesRemoved}', - style: TextStyle( - fontSize: 12, color: Colors.red[400]), - ), - ], - ), - ], - ), - ), - ], - ), - ), - // Tab bar - TabBar( - controller: _tabController, - isScrollable: true, - tabAlignment: TabAlignment.start, - labelStyle: const TextStyle(fontSize: 13), - tabs: const [ - Tab(text: 'Changes'), - Tab(text: 'Diff'), - Tab(text: 'Staged'), - Tab(text: 'Branches'), - ], - ), - Expanded( - child: _loading - ? const Center(child: CircularProgressIndicator()) - : TabBarView( - controller: _tabController, - children: [ - // Changes tab - ListView( - controller: widget.scrollController, - padding: const EdgeInsets.all(16), - children: [ - if (_diffSummary != null) - _DiffSummaryView( - json: _diffSummary!, - connId: widget.connId, - projectId: widget.project.id, - ), - if (_gitStatus != null) ...[ - const SizedBox(height: 16), - Text('Status', - style: Theme.of(context).textTheme.titleSmall), - const SizedBox(height: 8), - _GitStatusView(json: _gitStatus!), - ], - ], - ), - // Working tree diff tab - _DiffContentView( - diff: _workingTreeDiff, - scrollController: widget.scrollController, - ), - // Staged diff tab - _DiffContentView( - diff: _stagedDiff, - scrollController: widget.scrollController, - ), - // Branches tab - ListView( - controller: widget.scrollController, - padding: const EdgeInsets.all(16), - children: [ - if (_branches != null) _BranchesView(json: _branches!), - ], - ), - ], - ), - ), - ], - ); - } -} - -/// Renders git status JSON. -class _GitStatusView extends StatelessWidget { - final String json; - - const _GitStatusView({required this.json}); - - @override - Widget build(BuildContext context) { - try { - final data = jsonDecode(json); - if (data is Map) { - final entries = []; - - void addSection(String title, dynamic files) { - if (files is List && files.isNotEmpty) { - entries.add(Padding( - padding: const EdgeInsets.only(top: 8, bottom: 4), - child: Text( - title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.grey[400], - ), - ), - )); - for (final f in files) { - final path = f is String ? f : (f is Map ? f['path'] as String? ?? '' : f.toString()); - entries.add(Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Text( - path, - style: const TextStyle( - fontSize: 12, - fontFamily: 'JetBrainsMono', - ), - ), - )); - } - } - } - - addSection('Staged', data['staged']); - addSection('Modified', data['modified'] ?? data['unstaged']); - addSection('Untracked', data['untracked']); - - if (entries.isEmpty) { - return Text('Clean working tree', - style: TextStyle(color: Colors.grey[500])); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: entries, - ); - } - return Text(json, - style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); - } catch (_) { - return Text(json, - style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); - } - } -} - -/// Full diff content viewer with syntax-colored diff lines. -class _DiffContentView extends StatelessWidget { - final String? diff; - final ScrollController scrollController; - - const _DiffContentView({ - required this.diff, - required this.scrollController, - }); - - @override - Widget build(BuildContext context) { - if (diff == null || diff!.isEmpty) { - return Center( - child: Text('No changes', style: TextStyle(color: Colors.grey[500])), - ); - } - - final lines = diff!.split('\n'); - return ListView.builder( - controller: scrollController, - itemCount: lines.length, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - itemBuilder: (context, index) { - final line = lines[index]; - Color? bgColor; - Color textColor = Colors.grey[300]!; - - if (line.startsWith('+')) { - bgColor = Colors.green.withValues(alpha: 0.1); - textColor = Colors.green[300]!; - } else if (line.startsWith('-')) { - bgColor = Colors.red.withValues(alpha: 0.1); - textColor = Colors.red[300]!; - } else if (line.startsWith('@@')) { - textColor = Colors.cyan[300]!; - } else if (line.startsWith('diff ') || line.startsWith('index ')) { - textColor = Colors.grey[500]!; - } - - return Container( - color: bgColor, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), - child: Text( - line, - style: TextStyle( - fontSize: 11, - fontFamily: 'JetBrainsMono', - color: textColor, - ), - ), - ); - }, - ); - } -} - -class _DiffSummaryView extends StatelessWidget { - final String json; - final String? connId; - final String? projectId; - - const _DiffSummaryView({ - required this.json, - this.connId, - this.projectId, - }); - - @override - Widget build(BuildContext context) { - try { - final data = jsonDecode(json); - if (data is Map && data.containsKey('files')) { - final files = data['files'] as List? ?? []; - if (files.isEmpty) { - return Text('No changes', - style: TextStyle(color: Colors.grey[500])); - } - return Column( - children: files.map((f) { - final file = f as Map; - final path = file['path'] as String? ?? ''; - final added = file['added'] as int? ?? 0; - final removed = file['removed'] as int? ?? 0; - final status = file['status'] as String? ?? 'modified'; - - IconData icon; - Color iconColor; - switch (status) { - case 'added': - icon = Icons.add_circle_outline; - iconColor = Colors.green; - break; - case 'deleted': - icon = Icons.remove_circle_outline; - iconColor = Colors.red; - break; - default: - icon = Icons.edit; - iconColor = Colors.orange; - } - - return InkWell( - onTap: connId != null && projectId != null - ? () => _showFileContents(context, path) - : null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - Icon(icon, size: 16, color: iconColor), - const SizedBox(width: 8), - Expanded( - child: Text( - path, - style: const TextStyle( - fontSize: 12, fontFamily: 'JetBrainsMono'), - overflow: TextOverflow.ellipsis, - ), - ), - if (added > 0) - Text('+$added', - style: TextStyle( - fontSize: 11, color: Colors.green[400])), - if (added > 0 && removed > 0) const SizedBox(width: 4), - if (removed > 0) - Text('-$removed', - style: - TextStyle(fontSize: 11, color: Colors.red[400])), - ], - ), - ), - ); - }).toList(), - ); - } - // Fallback: show as plain text - return Text(json, - style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); - } catch (_) { - return Text(json, - style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); - } - } - - void _showFileContents(BuildContext context, String filePath) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (ctx) => DraggableScrollableSheet( - initialChildSize: 0.8, - minChildSize: 0.3, - maxChildSize: 0.95, - expand: false, - builder: (ctx, scrollController) => _FileContentsSheet( - connId: connId!, - projectId: projectId!, - filePath: filePath, - scrollController: scrollController, - ), - ), - ); - } -} - -class _FileContentsSheet extends StatefulWidget { - final String connId; - final String projectId; - final String filePath; - final ScrollController scrollController; - - const _FileContentsSheet({ - required this.connId, - required this.projectId, - required this.filePath, - required this.scrollController, - }); - - @override - State<_FileContentsSheet> createState() => _FileContentsSheetState(); -} - -class _FileContentsSheetState extends State<_FileContentsSheet> { - String? _contents; - String? _error; - bool _loading = true; - - @override - void initState() { - super.initState(); - _loadContents(); - } - - Future _loadContents() async { - try { - final contents = await state_ffi.gitFileContents( - connId: widget.connId, - projectId: widget.projectId, - filePath: widget.filePath, - mode: 'working_tree', - ); - if (mounted) { - setState(() { - _contents = contents; - _loading = false; - }); - } - } catch (e) { - if (mounted) { - setState(() { - _error = e.toString(); - _loading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - margin: const EdgeInsets.only(top: 12, bottom: 8), - width: 32, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[600], - borderRadius: BorderRadius.circular(2), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - const Icon(Icons.description, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - widget.filePath, - style: const TextStyle( - fontSize: 13, - fontFamily: 'JetBrainsMono', - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - const Divider(), - Expanded( - child: _loading - ? const Center(child: CircularProgressIndicator()) - : _error != null - ? Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - _error!, - style: TextStyle(color: Colors.red[400]), - ), - ), - ) - : SingleChildScrollView( - controller: widget.scrollController, - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - _contents ?? '', - style: const TextStyle( - fontSize: 12, - fontFamily: 'JetBrainsMono', - ), - ), - ), - ), - ), - ), - ], - ); - } -} - -class _BranchesView extends StatelessWidget { - final String json; - - const _BranchesView({required this.json}); - - @override - Widget build(BuildContext context) { - try { - final data = jsonDecode(json); - if (data is Map && data.containsKey('branches')) { - final branches = data['branches'] as List? ?? []; - return Column( - children: branches.map((b) { - final branch = b as Map; - final name = branch['name'] as String? ?? ''; - final isCurrent = branch['current'] as bool? ?? false; - final isRemote = branch['remote'] as bool? ?? false; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Icon( - isCurrent ? Icons.check_circle : Icons.circle_outlined, - size: 16, - color: isCurrent ? Colors.green : Colors.grey[600], - ), - const SizedBox(width: 8), - if (isRemote) - Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon(Icons.cloud, - size: 12, color: Colors.grey[500]), - ), - Expanded( - child: Text( - name, - style: TextStyle( - fontSize: 12, - fontFamily: 'JetBrainsMono', - fontWeight: - isCurrent ? FontWeight.bold : FontWeight.normal, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }).toList(), - ); - } - return Text(json, - style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); - } catch (_) { - return Text(json, - style: const TextStyle(fontSize: 12, fontFamily: 'JetBrainsMono')); - } - } -} - -/// Services button in the app bar. -class _ServicesButton extends StatelessWidget { - final String connId; - final state_ffi.ProjectInfo project; - - const _ServicesButton({required this.connId, required this.project}); - - @override - Widget build(BuildContext context) { - final running = - project.services.where((s) => s.status == 'running').length; - final total = project.services.length; - - return IconButton( - icon: Badge( - isLabelVisible: running > 0, - label: Text('$running', - style: const TextStyle(fontSize: 9)), - child: const Icon(Icons.dns, size: 20), - ), - tooltip: '$running/$total services running', - onPressed: () => _showServicesSheet(context), - ); - } - - void _showServicesSheet(BuildContext context) { - showModalBottomSheet( - context: context, - builder: (ctx) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - margin: const EdgeInsets.only(top: 12, bottom: 8), - width: 32, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[600], - borderRadius: BorderRadius.circular(2), - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - const Icon(Icons.dns, size: 20), - const SizedBox(width: 8), - Text('Services', - style: Theme.of(context).textTheme.titleMedium), - const Spacer(), - TextButton( - onPressed: () { - state_ffi.startAllServices( - connId: connId, - projectId: project.id, - ); - }, - child: const Text('Start All'), - ), - TextButton( - onPressed: () { - state_ffi.stopAllServices( - connId: connId, - projectId: project.id, - ); - }, - child: const Text('Stop All', - style: TextStyle(color: Colors.redAccent)), - ), - ], - ), - ), - const Divider(), - ...project.services.map((s) => _ServiceTile( - service: s, - connId: connId, - projectId: project.id, - )), - const SizedBox(height: 16), - ], - ), - ), - ); - } -} - -class _ServiceTile extends StatelessWidget { - final state_ffi.ServiceInfo service; - final String connId; - final String projectId; - - const _ServiceTile({ - required this.service, - required this.connId, - required this.projectId, - }); - - Color _statusColor() { - switch (service.status) { - case 'running': - return Colors.green; - case 'stopped': - return Colors.grey; - case 'crashed': - return Colors.red; - case 'starting': - case 'restarting': - return Colors.orange; - default: - return Colors.grey; - } - } - - @override - Widget build(BuildContext context) { - final color = _statusColor(); - return ListTile( - leading: Container( - width: 8, - height: 8, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ), - title: Row( - children: [ - Expanded(child: Text(service.name)), - Text( - service.status, - style: TextStyle(fontSize: 12, color: color), - ), - ], - ), - subtitle: service.ports.isNotEmpty - ? Text( - service.ports.map((p) => ':$p').join(', '), - style: TextStyle(fontSize: 11, color: Colors.grey[500]), - ) - : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (service.status == 'running') ...[ - IconButton( - icon: const Icon(Icons.restart_alt, size: 20), - tooltip: 'Restart', - onPressed: () { - state_ffi.restartService( - connId: connId, - projectId: projectId, - serviceName: service.name, - ); - }, - ), - IconButton( - icon: Icon(Icons.stop, size: 20, color: Colors.red[300]), - tooltip: 'Stop', - onPressed: () { - state_ffi.stopService( - connId: connId, - projectId: projectId, - serviceName: service.name, - ); - }, - ), - ] else ...[ - IconButton( - icon: Icon(Icons.play_arrow, size: 20, color: Colors.green[300]), - tooltip: 'Start', - onPressed: () { - state_ffi.startService( - connId: connId, - projectId: projectId, - serviceName: service.name, - ); - }, - ), - ], - ], - ), - ); - } -} diff --git a/mobile/lib/src/theme/app_theme.dart b/mobile/lib/src/theme/app_theme.dart deleted file mode 100644 index 0d9ad2b9..00000000 --- a/mobile/lib/src/theme/app_theme.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/painting.dart'; - -// ── Color system ──────────────────────────────────────────────────────── - -class OkenaColors { - OkenaColors._(); - - // Backgrounds - static const background = Color(0xFF000000); - static const surface = Color(0xFF0A0A0A); - static const surfaceElevated = Color(0xFF161616); - static const surfaceOverlay = Color(0xFF1C1C1C); - - // Borders - static const border = Color(0xFF1E1E1E); - static const borderLight = Color(0xFF2A2A2A); - - // Accent - static const accent = Color(0xFF7C7FFF); - - // Text - static const textPrimary = Color(0xFFE8E8EC); - static const textSecondary = Color(0xFF98989F); - static const textTertiary = Color(0xFF5A5A62); - - // Semantic - static const success = Color(0xFF4ADE80); - static const warning = Color(0xFFFBBF24); - static const error = Color(0xFFF87171); - - // Glass - static const glassBg = Color(0xCC0A0A0A); // 80% opacity surface - static const glassStroke = Color(0x18FFFFFF); // subtle white border - - // Key toolbar - static const keyBg = Color(0xFF161616); - static const keyBorder = Color(0xFF2A2A2A); - static const keyText = Color(0xFFB0B0B8); -} - -// ── Typography ────────────────────────────────────────────────────────── - -class OkenaTypography { - OkenaTypography._(); - - static const _fontFamily = '.SF Pro Text'; - - static const largeTitle = TextStyle( - fontFamily: _fontFamily, - fontSize: 28, - fontWeight: FontWeight.w700, - letterSpacing: -0.5, - color: OkenaColors.textPrimary, - ); - - static const title = TextStyle( - fontFamily: _fontFamily, - fontSize: 20, - fontWeight: FontWeight.w600, - letterSpacing: -0.3, - color: OkenaColors.textPrimary, - ); - - static const headline = TextStyle( - fontFamily: _fontFamily, - fontSize: 17, - fontWeight: FontWeight.w600, - color: OkenaColors.textPrimary, - ); - - static const body = TextStyle( - fontFamily: _fontFamily, - fontSize: 15, - fontWeight: FontWeight.w400, - color: OkenaColors.textPrimary, - ); - - static const callout = TextStyle( - fontFamily: _fontFamily, - fontSize: 14, - fontWeight: FontWeight.w400, - color: OkenaColors.textSecondary, - ); - - static const caption = TextStyle( - fontFamily: _fontFamily, - fontSize: 12, - fontWeight: FontWeight.w500, - color: OkenaColors.textSecondary, - ); - - static const caption2 = TextStyle( - fontFamily: _fontFamily, - fontSize: 11, - fontWeight: FontWeight.w500, - color: OkenaColors.textTertiary, - ); -} - -// ── Terminal theme ────────────────────────────────────────────────────── - -class TerminalTheme { - static const fontFamily = 'JetBrainsMono'; - static const fontFamilyFallback = [ - 'Menlo', - 'Consolas', - 'DejaVu Sans Mono', - 'monospace', - ]; - static const defaultFontSize = 13.0; - static const minFontSize = 6.0; - static const maxFontSize = 24.0; - static const defaultColumns = 80; - static const lineHeightFactor = 1.2; - - static const bgColor = Color(0xFF000000); - static const fgColor = Color(0xFFCDD6F4); - static const cursorColor = Color(0xFFF5E0DC); - static const selectionColor = Color(0x40585B70); -} diff --git a/mobile/lib/src/widgets/key_toolbar.dart b/mobile/lib/src/widgets/key_toolbar.dart deleted file mode 100644 index af3ec3a7..00000000 --- a/mobile/lib/src/widgets/key_toolbar.dart +++ /dev/null @@ -1,744 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../src/rust/api/state.dart' as state_ffi; -import '../../src/rust/api/terminal.dart' as ffi; -import '../theme/app_theme.dart'; - -/// Three-state modifier cycle: inactive -> active (one-shot) -> locked (sticky). -enum ModifierState { inactive, active, locked } - -/// Shared modifier state between [KeyToolbar] and [TerminalView]. -class KeyModifiers extends ChangeNotifier { - ModifierState _ctrl = ModifierState.inactive; - ModifierState _option = ModifierState.inactive; - ModifierState _cmd = ModifierState.inactive; - - bool get ctrl => _ctrl != ModifierState.inactive; - bool get option => _option != ModifierState.inactive; - bool get cmd => _cmd != ModifierState.inactive; - bool get hasAny => ctrl || option || cmd; - - ModifierState get ctrlState => _ctrl; - ModifierState get optionState => _option; - ModifierState get cmdState => _cmd; - - /// Cycle: inactive -> active -> locked -> inactive. - void toggleCtrl() { _ctrl = _nextState(_ctrl); notifyListeners(); } - void toggleOption() { _option = _nextState(_option); notifyListeners(); } - void toggleCmd() { _cmd = _nextState(_cmd); notifyListeners(); } - - static ModifierState _nextState(ModifierState s) => switch (s) { - ModifierState.inactive => ModifierState.active, - ModifierState.active => ModifierState.locked, - ModifierState.locked => ModifierState.inactive, - }; - - /// Reset only one-shot (active) modifiers; locked ones persist. - void reset() { - final changed = _ctrl == ModifierState.active || - _option == ModifierState.active || - _cmd == ModifierState.active; - if (!changed) return; - if (_ctrl == ModifierState.active) _ctrl = ModifierState.inactive; - if (_option == ModifierState.active) _option = ModifierState.inactive; - if (_cmd == ModifierState.active) _cmd = ModifierState.inactive; - notifyListeners(); - } -} - -const _kComposeHistoryKey = 'compose_history'; -const _kMaxHistory = 30; - -class KeyToolbar extends StatefulWidget { - final String connId; - final String? terminalId; - final KeyModifiers modifiers; - - const KeyToolbar({ - super.key, - required this.connId, - this.terminalId, - required this.modifiers, - }); - - @override - State createState() => _KeyToolbarState(); -} - -class _KeyToolbarState extends State { - KeyModifiers get _mod => widget.modifiers; - - // Arrow key name -> xterm suffix character - static const _arrowChar = { - 'ArrowUp': 'A', - 'ArrowDown': 'B', - 'ArrowRight': 'C', - 'ArrowLeft': 'D', - }; - - // Compose history - List _composeHistory = []; - - @override - void initState() { - super.initState(); - _mod.addListener(_onModChanged); - _loadComposeHistory(); - } - - @override - void didUpdateWidget(KeyToolbar old) { - super.didUpdateWidget(old); - if (old.modifiers != widget.modifiers) { - old.modifiers.removeListener(_onModChanged); - widget.modifiers.addListener(_onModChanged); - } - } - - @override - void dispose() { - _mod.removeListener(_onModChanged); - super.dispose(); - } - - void _onModChanged() { - if (mounted) setState(() {}); - } - - Future _loadComposeHistory() async { - final prefs = await SharedPreferences.getInstance(); - final history = prefs.getStringList(_kComposeHistoryKey); - if (history != null) { - setState(() => _composeHistory = history); - } - } - - Future _saveComposeHistory() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList(_kComposeHistoryKey, _composeHistory); - } - - void _addToHistory(String text) { - _composeHistory.remove(text); // dedup - _composeHistory.insert(0, text); - if (_composeHistory.length > _kMaxHistory) { - _composeHistory = _composeHistory.sublist(0, _kMaxHistory); - } - _saveComposeHistory(); - } - - void _sendSpecialKey(String key) { - final tid = widget.terminalId; - if (tid == null) return; - state_ffi.sendSpecialKey( - connId: widget.connId, - terminalId: tid, - key: key, - ); - } - - void _sendText(String text) { - final tid = widget.terminalId; - if (tid == null) return; - ffi.sendText( - connId: widget.connId, - terminalId: tid, - text: text, - ); - } - - /// Send a character key, applying any active modifiers. - void _sendCharKey(String char) { - if (_mod.hasAny) { - if (_mod.ctrl) { - final code = char.codeUnitAt(0); - if (code >= 0x61 && code <= 0x7A) { - _sendText(String.fromCharCode(code - 0x60)); - } else if (code >= 0x41 && code <= 0x5A) { - _sendText(String.fromCharCode(code - 0x40)); - } else { - _sendText(char); - } - } else { - // Option/Cmd: ESC prefix - _sendText('\x1b$char'); - } - _mod.reset(); - } else { - _sendText(char); - } - } - - /// Handle arrow from joystick, respecting modifier state. - void _handleArrow(String key) { - final arrow = _arrowChar[key]; - - if (arrow != null && _mod.hasAny) { - if (_mod.cmd && !_mod.ctrl && !_mod.option) { - switch (key) { - case 'ArrowLeft': - _sendSpecialKey('Home'); - case 'ArrowRight': - _sendSpecialKey('End'); - case 'ArrowUp': - _sendSpecialKey('PageUp'); - case 'ArrowDown': - _sendSpecialKey('PageDown'); - } - } else { - int mod = 1; - if (_mod.ctrl) mod += 4; - if (_mod.option) mod += 2; - _sendText('\x1b[1;$mod$arrow'); - } - _mod.reset(); - } else { - _sendSpecialKey(key); - if (_mod.hasAny) _mod.reset(); - } - } - - Future _paste() async { - final data = await Clipboard.getData('text/plain'); - if (data?.text != null && data!.text!.isNotEmpty) { - _sendText(data.text!); - } - } - - void _showComposeSheet() { - final controller = TextEditingController(); - bool sendEnter = true; - int historyIdx = -1; - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: OkenaColors.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) { - return StatefulBuilder( - builder: (ctx, setSheetState) { - void submit() { - final text = controller.text; - if (text.isEmpty) return; - HapticFeedback.mediumImpact(); - _sendText(text); - if (sendEnter) { - _sendSpecialKey('Enter'); - } - _addToHistory(text); - Navigator.of(ctx).pop(); - } - - void historyUp() { - if (_composeHistory.isEmpty) return; - final newIdx = (historyIdx + 1).clamp(0, _composeHistory.length - 1); - if (newIdx != historyIdx) { - setSheetState(() { - historyIdx = newIdx; - controller.text = _composeHistory[historyIdx]; - controller.selection = TextSelection.collapsed( - offset: controller.text.length, - ); - }); - } - } - - void historyDown() { - if (historyIdx <= 0) { - setSheetState(() { - historyIdx = -1; - controller.text = ''; - }); - return; - } - setSheetState(() { - historyIdx--; - controller.text = _composeHistory[historyIdx]; - controller.selection = TextSelection.collapsed( - offset: controller.text.length, - ); - }); - } - - return Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 16, - bottom: MediaQuery.of(ctx).viewInsets.bottom + 16, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Top row: history nav + enter toggle - Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_upward, size: 20), - color: _composeHistory.isNotEmpty - ? OkenaColors.textSecondary - : OkenaColors.textTertiary, - onPressed: _composeHistory.isNotEmpty ? historyUp : null, - tooltip: 'Previous command', - visualDensity: VisualDensity.compact, - ), - IconButton( - icon: const Icon(Icons.arrow_downward, size: 20), - color: historyIdx > 0 - ? OkenaColors.textSecondary - : OkenaColors.textTertiary, - onPressed: historyIdx >= 0 ? historyDown : null, - tooltip: 'Next command', - visualDensity: VisualDensity.compact, - ), - const Spacer(), - GestureDetector( - onTap: () { - setSheetState(() => sendEnter = !sendEnter); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: sendEnter - ? OkenaColors.accent - : OkenaColors.surfaceElevated, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.keyboard_return, - size: 14, - color: - sendEnter ? Colors.white : OkenaColors.textTertiary, - ), - const SizedBox(width: 4), - Text( - 'Enter', - style: TextStyle( - color: sendEnter - ? Colors.white - : OkenaColors.textTertiary, - fontSize: 12, - fontFamily: 'JetBrainsMono', - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 8), - TextField( - controller: controller, - autofocus: true, - maxLines: null, - minLines: 3, - style: const TextStyle( - color: OkenaColors.textPrimary, - fontFamily: 'JetBrainsMono', - fontSize: 14, - ), - decoration: InputDecoration( - hintText: 'Enter command...', - hintStyle: TextStyle(color: OkenaColors.textTertiary), - filled: true, - fillColor: OkenaColors.surfaceElevated, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - suffixIcon: IconButton( - icon: Icon(Icons.send, color: OkenaColors.accent), - onPressed: submit, - ), - ), - ), - ], - ), - ); - }, - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), - child: Container( - decoration: const BoxDecoration( - color: OkenaColors.glassBg, - border: Border( - top: BorderSide(color: OkenaColors.glassStroke, width: 0.5), - ), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 5), - child: Row( - children: [ - // Scrollable button row - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _Key(label: 'esc', onTap: () => _sendSpecialKey('Escape')), - _ToggleKey( - label: '\u2303', - state: _mod.ctrlState, - onTap: _mod.toggleCtrl, - ), - _ToggleKey( - label: '\u2325', - state: _mod.optionState, - onTap: _mod.toggleOption, - ), - _ToggleKey( - label: '\u2318', - state: _mod.cmdState, - onTap: _mod.toggleCmd, - ), - _Key(label: 'tab', onTap: () => _sendSpecialKey('Tab')), - const SizedBox(width: 12), - _Key(label: '~', onTap: () => _sendCharKey('~')), - _Key(label: '|', onTap: () => _sendCharKey('|')), - _Key(label: '/', onTap: () => _sendCharKey('/')), - _Key(label: '-', onTap: () => _sendCharKey('-')), - const SizedBox(width: 12), - _IconKey( - icon: Icons.edit_note_rounded, - onTap: _showComposeSheet, - ), - _IconKey( - icon: Icons.content_paste_rounded, - onTap: _paste, - ), - _IconKey( - icon: Icons.keyboard_hide_rounded, - onTap: () => FocusScope.of(context).unfocus(), - ), - ], - ), - ), - ), - const SizedBox(width: 6), - // Fixed arrow joystick - _ArrowJoystick(onArrow: _handleArrow), - ], - ), - ), - ), - ), - ), - ); - } -} - -// ── Shared key widgets ───────────────────────────────────────────────── - -class _Key extends StatelessWidget { - final String label; - final VoidCallback onTap; - - const _Key({required this.label, required this.onTap}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - constraints: const BoxConstraints(minWidth: 40), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9), - decoration: BoxDecoration( - color: OkenaColors.keyBg, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: OkenaColors.keyBorder, width: 0.5), - ), - alignment: Alignment.center, - child: Text( - label, - style: const TextStyle( - color: OkenaColors.keyText, - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ); - } -} - -class _IconKey extends StatelessWidget { - final IconData icon; - final VoidCallback onTap; - - const _IconKey({required this.icon, required this.onTap}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - constraints: const BoxConstraints(minWidth: 40), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - decoration: BoxDecoration( - color: OkenaColors.keyBg, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: OkenaColors.keyBorder, width: 0.5), - ), - alignment: Alignment.center, - child: Icon(icon, color: OkenaColors.keyText, size: 17), - ), - ), - ); - } -} - -class _ToggleKey extends StatelessWidget { - final String label; - final ModifierState state; - final VoidCallback onTap; - - const _ToggleKey({ - required this.label, - required this.state, - required this.onTap, - }); - - bool get _active => state != ModifierState.inactive; - bool get _locked => state == ModifierState.locked; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - curve: Curves.easeOutCubic, - constraints: const BoxConstraints(minWidth: 40), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7), - decoration: BoxDecoration( - color: _active ? OkenaColors.accent : OkenaColors.keyBg, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: _active ? OkenaColors.accent : OkenaColors.keyBorder, - width: 0.5, - ), - boxShadow: _active - ? [ - BoxShadow( - color: OkenaColors.accent.withOpacity(0.35), - blurRadius: 12, - spreadRadius: -2, - ), - ] - : null, - ), - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - label, - style: TextStyle( - color: _active ? Colors.white : OkenaColors.keyText, - fontSize: 16, - fontWeight: _active ? FontWeight.w700 : FontWeight.w500, - ), - ), - // Small bar indicator for locked state - AnimatedContainer( - duration: const Duration(milliseconds: 150), - curve: Curves.easeOutCubic, - width: 12, - height: 2, - margin: const EdgeInsets.only(top: 1), - decoration: BoxDecoration( - color: _locked ? Colors.white : Colors.transparent, - borderRadius: BorderRadius.circular(1), - ), - ), - ], - ), - ), - ), - ); - } -} - -// ── Arrow Joystick ───────────────────────────────────────────────────── - -class _ArrowJoystick extends StatefulWidget { - final ValueChanged onArrow; - - const _ArrowJoystick({required this.onArrow}); - - @override - State<_ArrowJoystick> createState() => _ArrowJoystickState(); -} - -class _ArrowJoystickState extends State<_ArrowJoystick> { - static const _size = 52.0; - static const _dragThreshold = 14.0; - - String? _activeDirection; - Offset _panOrigin = Offset.zero; - bool _hasMoved = false; - - void _fire(String direction) { - widget.onArrow(direction); - HapticFeedback.selectionClick(); - setState(() => _activeDirection = direction); - } - - void _onPanStart(DragStartDetails details) { - _panOrigin = details.localPosition; - _hasMoved = false; - } - - void _onPanUpdate(DragUpdateDetails details) { - final delta = details.localPosition - _panOrigin; - if (delta.distance >= _dragThreshold) { - _hasMoved = true; - final dir = delta.dx.abs() > delta.dy.abs() - ? (delta.dx > 0 ? 'ArrowRight' : 'ArrowLeft') - : (delta.dy > 0 ? 'ArrowDown' : 'ArrowUp'); - _fire(dir); - _panOrigin = details.localPosition; - } - } - - void _onPanEnd(DragEndDetails details) { - if (!_hasMoved) { - final center = const Offset(_size / 2, _size / 2); - final delta = _panOrigin - center; - if (delta.distance >= 4) { - final dir = delta.dx.abs() > delta.dy.abs() - ? (delta.dx > 0 ? 'ArrowRight' : 'ArrowLeft') - : (delta.dy > 0 ? 'ArrowDown' : 'ArrowUp'); - _fire(dir); - Future.delayed(const Duration(milliseconds: 120), () { - if (mounted) setState(() => _activeDirection = null); - }); - return; - } - } - setState(() => _activeDirection = null); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onPanStart: _onPanStart, - onPanUpdate: _onPanUpdate, - onPanEnd: _onPanEnd, - child: Container( - width: _size, - height: _size, - decoration: BoxDecoration( - color: OkenaColors.keyBg, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: OkenaColors.keyBorder, width: 0.5), - ), - child: CustomPaint( - size: const Size(_size, _size), - painter: _JoystickPainter(_activeDirection), - ), - ), - ); - } -} - -class _JoystickPainter extends CustomPainter { - final String? activeDirection; - - _JoystickPainter(this.activeDirection); - - @override - void paint(Canvas canvas, Size size) { - final center = Offset(size.width / 2, size.height / 2); - const armLength = 12.0; - const gap = 3.0; - const tipSize = 4.0; - - const dirs = { - 'ArrowUp': Offset(0, -1), - 'ArrowDown': Offset(0, 1), - 'ArrowLeft': Offset(-1, 0), - 'ArrowRight': Offset(1, 0), - }; - - for (final entry in dirs.entries) { - final isActive = activeDirection == entry.key; - final color = isActive ? OkenaColors.accent : const Color(0x61FFFFFF); - final paint = Paint() - ..color = color - ..strokeWidth = 1.5 - ..strokeCap = StrokeCap.round; - - final d = entry.value; - final armStart = center + d * gap; - final armEnd = center + d * armLength; - - paint.style = PaintingStyle.stroke; - canvas.drawLine(armStart, armEnd, paint); - - paint.style = PaintingStyle.fill; - final path = Path(); - if (d.dy != 0) { - path.moveTo(armEnd.dx, armEnd.dy); - path.lineTo(armEnd.dx - tipSize, armEnd.dy - d.dy * tipSize); - path.lineTo(armEnd.dx + tipSize, armEnd.dy - d.dy * tipSize); - } else { - path.moveTo(armEnd.dx, armEnd.dy); - path.lineTo(armEnd.dx - d.dx * tipSize, armEnd.dy - tipSize); - path.lineTo(armEnd.dx - d.dx * tipSize, armEnd.dy + tipSize); - } - path.close(); - canvas.drawPath(path, paint); - } - - canvas.drawCircle( - center, - 1.5, - Paint()..color = const Color(0x3DFFFFFF), - ); - } - - @override - bool shouldRepaint(_JoystickPainter old) => - old.activeDirection != activeDirection; -} diff --git a/mobile/lib/src/widgets/layout_renderer.dart b/mobile/lib/src/widgets/layout_renderer.dart deleted file mode 100644 index a9dd58ed..00000000 --- a/mobile/lib/src/widgets/layout_renderer.dart +++ /dev/null @@ -1,400 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' show Uint64List; - -import '../../src/rust/api/state.dart' as state_ffi; -import '../models/layout_node.dart'; -import '../theme/app_theme.dart'; -import 'key_toolbar.dart' show KeyModifiers; -import 'terminal_view.dart'; - -class LayoutRenderer extends StatelessWidget { - final String connId; - final String projectId; - final List terminalIds; - final KeyModifiers modifiers; - - const LayoutRenderer({ - super.key, - required this.connId, - required this.projectId, - required this.terminalIds, - required this.modifiers, - }); - - @override - Widget build(BuildContext context) { - final json = state_ffi.getProjectLayoutJson( - connId: connId, - projectId: projectId, - ); - - if (json != null) { - final node = LayoutNode.fromJson(json); - if (node != null) { - return _buildNode(context, node, const []); - } - } - - // Fallback: show first terminal - if (terminalIds.isEmpty) { - return const Center( - child: Text( - 'No terminals', - style: TextStyle(color: OkenaColors.textTertiary), - ), - ); - } - return TerminalView(connId: connId, terminalId: terminalIds.first, modifiers: modifiers); - } - - Widget _buildNode(BuildContext context, LayoutNode node, List path) { - return switch (node) { - TerminalNode(:final terminalId, :final minimized) => terminalId != null - ? minimized - ? _MinimizedTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ) - : TerminalView(connId: connId, terminalId: terminalId, modifiers: modifiers) - : const Center( - child: - Text('Empty terminal', style: TextStyle(color: OkenaColors.textTertiary)), - ), - SplitNode(:final direction, :final sizes, :final children) => - _buildSplit(context, direction, sizes, children, path), - TabsNode(:final activeTab, :final children) => - _buildTabs(context, activeTab, children, path), - }; - } - - Widget _buildSplit( - BuildContext context, - SplitDirection direction, - List sizes, - List children, - List path, - ) { - if (children.isEmpty) { - return const SizedBox.shrink(); - } - - // In portrait mode, force horizontal splits to vertical - final isPortrait = - MediaQuery.of(context).orientation == Orientation.portrait; - final isVertical = - direction == SplitDirection.vertical || (direction == SplitDirection.horizontal && isPortrait); - - return _ResizableSplit( - connId: connId, - projectId: projectId, - path: path, - isVertical: isVertical, - sizes: sizes, - children: children, - builder: (node, index) => _buildNode(context, node, [...path, index]), - ); - } - - Widget _buildTabs( - BuildContext context, - int activeTab, - List children, - List path, - ) { - if (children.isEmpty) { - return const SizedBox.shrink(); - } - - return _TabsWidget( - connId: connId, - projectId: projectId, - path: path, - activeTab: activeTab.clamp(0, children.length - 1), - children: children, - builder: (node, index) => _buildNode(context, node, [...path, index]), - ); - } -} - -/// A minimized terminal placeholder. -class _MinimizedTerminal extends StatelessWidget { - final String connId; - final String projectId; - final String terminalId; - - const _MinimizedTerminal({ - required this.connId, - required this.projectId, - required this.terminalId, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - state_ffi.toggleMinimized( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - child: Container( - height: 36, - color: OkenaColors.surfaceElevated, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - const Icon(Icons.terminal, size: 16, color: OkenaColors.textSecondary), - const SizedBox(width: 8), - Text( - terminalId.length > 8 - ? '...${terminalId.substring(terminalId.length - 8)}' - : terminalId, - style: const TextStyle( - fontSize: 12, - color: OkenaColors.textSecondary, - fontFamily: 'JetBrainsMono', - ), - ), - const Spacer(), - const Icon(Icons.expand_more, size: 16, color: OkenaColors.textTertiary), - ], - ), - ), - ); - } -} - -/// Resizable split pane with draggable dividers. -class _ResizableSplit extends StatefulWidget { - final String connId; - final String projectId; - final List path; - final bool isVertical; - final List sizes; - final List children; - final Widget Function(LayoutNode, int) builder; - - const _ResizableSplit({ - required this.connId, - required this.projectId, - required this.path, - required this.isVertical, - required this.sizes, - required this.children, - required this.builder, - }); - - @override - State<_ResizableSplit> createState() => _ResizableSplitState(); -} - -class _ResizableSplitState extends State<_ResizableSplit> { - late List _sizes; - - @override - void initState() { - super.initState(); - _sizes = List.of(widget.sizes); - _ensureSizes(); - } - - @override - void didUpdateWidget(_ResizableSplit oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.sizes != widget.sizes) { - _sizes = List.of(widget.sizes); - _ensureSizes(); - } - } - - void _ensureSizes() { - while (_sizes.length < widget.children.length) { - _sizes.add(1.0); - } - } - - void _onDividerDrag(int dividerIndex, double delta, double totalSize) { - if (totalSize <= 0) return; - final total = _sizes.reduce((a, b) => a + b); - final fraction = delta / totalSize * total; - - setState(() { - _sizes[dividerIndex] = (_sizes[dividerIndex] + fraction).clamp(0.1, total); - _sizes[dividerIndex + 1] = (_sizes[dividerIndex + 1] - fraction).clamp(0.1, total); - }); - } - - void _onDividerDragEnd() { - state_ffi.updateSplitSizes( - connId: widget.connId, - projectId: widget.projectId, - path: Uint64List.fromList(widget.path), - sizes: _sizes.map((s) => s).toList(), - ); - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final totalSize = widget.isVertical - ? constraints.maxHeight - : constraints.maxWidth; - - final flexChildren = []; - for (int i = 0; i < widget.children.length; i++) { - final flex = i < _sizes.length ? _sizes[i].round().clamp(1, 1000) : 1; - if (i > 0) { - final dividerIndex = i - 1; - flexChildren.add( - GestureDetector( - behavior: HitTestBehavior.opaque, - onVerticalDragUpdate: widget.isVertical - ? (details) => _onDividerDrag(dividerIndex, details.delta.dy, totalSize) - : null, - onHorizontalDragUpdate: !widget.isVertical - ? (details) => _onDividerDrag(dividerIndex, details.delta.dx, totalSize) - : null, - onVerticalDragEnd: widget.isVertical ? (_) => _onDividerDragEnd() : null, - onHorizontalDragEnd: !widget.isVertical ? (_) => _onDividerDragEnd() : null, - child: MouseRegion( - cursor: widget.isVertical - ? SystemMouseCursors.resizeRow - : SystemMouseCursors.resizeColumn, - child: Container( - width: widget.isVertical ? double.infinity : 6, - height: widget.isVertical ? 6 : double.infinity, - color: Colors.transparent, - child: Center( - child: Container( - width: widget.isVertical ? 32 : 2, - height: widget.isVertical ? 2 : 32, - decoration: BoxDecoration( - color: OkenaColors.borderLight, - borderRadius: BorderRadius.circular(1), - ), - ), - ), - ), - ), - ), - ); - } - flexChildren.add( - Expanded(flex: flex, child: widget.builder(widget.children[i], i)), - ); - } - - return Flex( - direction: widget.isVertical ? Axis.vertical : Axis.horizontal, - children: flexChildren, - ); - }, - ); - } -} - -class _TabsWidget extends StatelessWidget { - final String connId; - final String projectId; - final List path; - final int activeTab; - final List children; - final Widget Function(LayoutNode, int) builder; - - const _TabsWidget({ - required this.connId, - required this.projectId, - required this.path, - required this.activeTab, - required this.children, - required this.builder, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - height: 32, - color: OkenaColors.surfaceElevated, - child: Row( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (int i = 0; i < children.length; i++) - GestureDetector( - onTap: () { - if (i != activeTab) { - state_ffi.setActiveTab( - connId: connId, - projectId: projectId, - path: Uint64List.fromList(path), - index: BigInt.from(i), - ); - } - }, - child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: i == activeTab - ? OkenaColors.accent - : Colors.transparent, - width: 2, - ), - ), - ), - child: Text( - _tabLabel(children[i], i), - style: TextStyle( - color: i == activeTab ? OkenaColors.textPrimary : OkenaColors.textSecondary, - fontSize: 12, - ), - ), - ), - ), - ], - ), - ), - ), - // Add tab button - GestureDetector( - onTap: () { - state_ffi.addTab( - connId: connId, - projectId: projectId, - path: Uint64List.fromList(path), - inGroup: true, - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Icon(Icons.add, size: 16, color: OkenaColors.textTertiary), - ), - ), - ], - ), - ), - Expanded( - child: builder(children[activeTab], activeTab), - ), - ], - ); - } - - String _tabLabel(LayoutNode node, int index) { - if (node is TerminalNode && node.terminalId != null) { - final id = node.terminalId!; - return id.length > 6 ? '...${id.substring(id.length - 6)}' : id; - } - return 'Tab ${index + 1}'; - } -} diff --git a/mobile/lib/src/widgets/project_drawer.dart b/mobile/lib/src/widgets/project_drawer.dart deleted file mode 100644 index 21ad0185..00000000 --- a/mobile/lib/src/widgets/project_drawer.dart +++ /dev/null @@ -1,991 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../providers/connection_provider.dart'; -import '../providers/workspace_provider.dart'; -import '../rust/api/state.dart' as state_ffi; -import 'status_indicator.dart'; - -class ProjectDrawer extends StatelessWidget { - const ProjectDrawer({super.key}); - - @override - Widget build(BuildContext context) { - final workspace = context.watch(); - final connection = context.watch(); - - return Drawer( - child: Column( - children: [ - DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Okena', - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 4), - if (connection.activeServer != null) - Text( - connection.activeServer!.displayName, - style: Theme.of(context).textTheme.bodySmall, - ), - const Spacer(), - StatusIndicator(status: connection.status), - ], - ), - ), - Expanded( - child: _ProjectList( - workspace: workspace, - connection: connection, - ), - ), - const Divider(height: 1), - // Add Project button - if (connection.connId != null) - ListTile( - leading: const Icon(Icons.create_new_folder), - title: const Text('Add Project'), - onTap: () => _showAddProjectDialog(context, connection.connId!), - ), - ListTile( - leading: const Icon(Icons.link_off), - title: const Text('Disconnect'), - onTap: () { - Navigator.of(context).pop(); - connection.disconnect(); - }, - ), - ], - ), - ); - } - - void _showAddProjectDialog(BuildContext context, String connId) { - final nameController = TextEditingController(); - final pathController = TextEditingController(); - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Add Project'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameController, - autofocus: true, - decoration: const InputDecoration(labelText: 'Name'), - ), - const SizedBox(height: 12), - TextField( - controller: pathController, - decoration: const InputDecoration( - labelText: 'Path', - hintText: '/home/user/project', - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - final name = nameController.text.trim(); - final path = pathController.text.trim(); - if (name.isNotEmpty && path.isNotEmpty) { - Navigator.of(ctx).pop(); - state_ffi.addProject(connId: connId, name: name, path: path); - } - }, - child: const Text('Add'), - ), - ], - ), - ); - } -} - -class _ProjectList extends StatelessWidget { - final WorkspaceProvider workspace; - final ConnectionProvider connection; - - const _ProjectList({required this.workspace, required this.connection}); - - @override - Widget build(BuildContext context) { - final folders = workspace.folders; - final projectOrder = workspace.projectOrder; - final projects = workspace.projects; - - // Build ordered list: folders and standalone projects - final List items = []; - - if (projectOrder.isNotEmpty || folders.isNotEmpty) { - // Use project_order to display in correct order - final folderMap = {for (final f in folders) f.id: f}; - final projectMap = {for (final p in projects) p.id: p}; - final displayedProjectIds = {}; - - for (final entryId in projectOrder) { - final folder = folderMap[entryId]; - if (folder != null) { - items.add(_FolderTile( - folder: folder, - projects: folder.projectIds - .map((pid) => projectMap[pid]) - .whereType() - .toList(), - workspace: workspace, - connection: connection, - )); - displayedProjectIds.addAll(folder.projectIds); - } else { - final project = projectMap[entryId]; - if (project != null) { - items.add(_ProjectTile( - project: project, - workspace: workspace, - connection: connection, - )); - displayedProjectIds.add(entryId); - } - } - } - - // Add any projects not in the order - for (final p in projects) { - if (!displayedProjectIds.contains(p.id)) { - items.add(_ProjectTile( - project: p, - workspace: workspace, - connection: connection, - )); - } - } - } else { - // No ordering info — just list projects - for (final p in projects) { - items.add(_ProjectTile( - project: p, - workspace: workspace, - connection: connection, - )); - } - } - - return ListView(children: items); - } -} - -// ── Color constants ──────────────────────────────────────────────────── - -const _colorOptions = [ - 'red', 'orange', 'yellow', 'lime', 'green', - 'teal', 'cyan', 'blue', 'purple', 'pink', -]; - -Color _folderColorToColor(String colorName) { - switch (colorName) { - case 'red': - return Colors.red; - case 'orange': - return Colors.orange; - case 'yellow': - return Colors.yellow; - case 'lime': - return Colors.lime; - case 'green': - return Colors.green; - case 'teal': - return Colors.teal; - case 'cyan': - return Colors.cyan; - case 'blue': - return Colors.blue; - case 'purple': - return Colors.purple; - case 'pink': - return Colors.pink; - default: - return Colors.grey; - } -} - -/// Color picker for projects and folders. -void _showColorPicker({ - required BuildContext context, - required String currentColor, - required ValueChanged onSelect, -}) { - showModalBottomSheet( - context: context, - builder: (ctx) => SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Choose Color', - style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 16), - Wrap( - spacing: 12, - runSpacing: 12, - children: _colorOptions.map((name) { - final color = _folderColorToColor(name); - final isSelected = name == currentColor; - return GestureDetector( - onTap: () { - Navigator.of(ctx).pop(); - onSelect(name); - }, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - border: isSelected - ? Border.all(color: Colors.white, width: 3) - : null, - ), - child: isSelected - ? const Icon(Icons.check, size: 20, color: Colors.white) - : null, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ); -} - -class _FolderTile extends StatelessWidget { - final state_ffi.FolderInfo folder; - final List projects; - final WorkspaceProvider workspace; - final ConnectionProvider connection; - - const _FolderTile({ - required this.folder, - required this.projects, - required this.workspace, - required this.connection, - }); - - @override - Widget build(BuildContext context) { - final color = _folderColorToColor(folder.folderColor); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - GestureDetector( - onLongPress: () { - final connId = connection.connId; - if (connId != null) { - _showColorPicker( - context: context, - currentColor: folder.folderColor, - onSelect: (newColor) { - state_ffi.setFolderColor( - connId: connId, - folderId: folder.id, - color: newColor, - ); - }, - ); - } - }, - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 4), - child: Row( - children: [ - Icon(Icons.folder, size: 18, color: color), - const SizedBox(width: 8), - Expanded( - child: Text( - folder.name, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: color, - letterSpacing: 0.5, - ), - ), - ), - ], - ), - ), - ), - ...projects.asMap().entries.map((entry) => _ReorderableProjectTile( - key: ValueKey('folder-${folder.id}-${entry.value.id}'), - project: entry.value, - workspace: workspace, - connection: connection, - folderId: folder.id, - index: entry.key, - totalCount: projects.length, - )), - ], - ); - } -} - -class _ReorderableProjectTile extends StatelessWidget { - final state_ffi.ProjectInfo project; - final WorkspaceProvider workspace; - final ConnectionProvider connection; - final String folderId; - final int index; - final int totalCount; - - const _ReorderableProjectTile({ - super.key, - required this.project, - required this.workspace, - required this.connection, - required this.folderId, - required this.index, - required this.totalCount, - }); - - @override - Widget build(BuildContext context) { - return _ProjectTile( - project: project, - workspace: workspace, - connection: connection, - indent: true, - onReorder: totalCount > 1 ? (newIndex) { - final connId = connection.connId; - if (connId != null) { - state_ffi.reorderProjectInFolder( - connId: connId, - folderId: folderId, - projectId: project.id, - newIndex: BigInt.from(newIndex), - ); - } - } : null, - reorderIndex: index, - reorderTotal: totalCount, - ); - } -} - -class _ProjectTile extends StatelessWidget { - final state_ffi.ProjectInfo project; - final WorkspaceProvider workspace; - final ConnectionProvider connection; - final bool indent; - final void Function(int newIndex)? onReorder; - final int? reorderIndex; - final int? reorderTotal; - - const _ProjectTile({ - required this.project, - required this.workspace, - required this.connection, - this.indent = false, - this.onReorder, - this.reorderIndex, - this.reorderTotal, - }); - - @override - Widget build(BuildContext context) { - final isSelected = project.id == workspace.selectedProjectId; - final folderColor = _folderColorToColor(project.folderColor); - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - contentPadding: EdgeInsets.only(left: indent ? 32 : 16, right: 16), - leading: Icon( - Icons.folder, - color: isSelected - ? Theme.of(context).colorScheme.primary - : folderColor, - ), - title: Text(project.name), - subtitle: _buildSubtitle(context), - selected: isSelected, - onTap: () { - workspace.selectProject(project.id); - }, - onLongPress: () { - if (onReorder != null) { - _showReorderMenu(context); - } else { - // Show color picker for standalone projects - final connId = connection.connId; - if (connId != null) { - _showColorPicker( - context: context, - currentColor: project.folderColor, - onSelect: (newColor) { - state_ffi.setProjectColor( - connId: connId, - projectId: project.id, - color: newColor, - ); - }, - ); - } - } - }, - trailing: onReorder != null - ? SizedBox( - width: 64, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (reorderIndex != null && reorderIndex! > 0) - SizedBox( - width: 32, - height: 32, - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 16, - icon: const Icon(Icons.arrow_upward, size: 16), - onPressed: () => onReorder!(reorderIndex! - 1), - ), - ), - if (reorderIndex != null && reorderTotal != null && - reorderIndex! < reorderTotal! - 1) - SizedBox( - width: 32, - height: 32, - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 16, - icon: const Icon(Icons.arrow_downward, size: 16), - onPressed: () => onReorder!(reorderIndex! + 1), - ), - ), - ], - ), - ) - : null, - ), - if (isSelected) ...[ - // Git status - if (project.gitBranch != null) - _GitStatusRow(project: project), - // Services - if (project.services.isNotEmpty) - ...project.services.map((s) => _ServiceRow( - service: s, - project: project, - connection: connection, - )), - // Terminals - ...project.terminalIds.asMap().entries.map((entry) { - final idx = entry.key; - final tid = entry.value; - final isTerminalSelected = tid == workspace.selectedTerminalId; - final name = - project.terminalNames[tid] ?? 'Terminal ${idx + 1}'; - return ListTile( - contentPadding: - const EdgeInsets.only(left: 56, right: 8), - leading: Icon( - Icons.terminal, - size: 20, - color: isTerminalSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - title: Text( - name, - style: TextStyle( - fontSize: 14, - color: isTerminalSelected - ? Theme.of(context).colorScheme.primary - : null, - ), - ), - selected: isTerminalSelected, - dense: true, - trailing: _TerminalActions( - connId: connection.connId!, - projectId: project.id, - terminalId: tid, - name: name, - ), - onTap: () { - workspace.selectTerminal(tid); - // Also focus the terminal on the server - state_ffi.focusTerminal( - connId: connection.connId!, - projectId: project.id, - terminalId: tid, - ); - Navigator.of(context).pop(); - }, - onLongPress: () { - _showTerminalMenu( - context, - connId: connection.connId!, - projectId: project.id, - terminalId: tid, - name: name, - ); - }, - ); - }), - if (connection.connId != null) - ListTile( - contentPadding: - const EdgeInsets.only(left: 56, right: 16), - leading: Icon( - Icons.add, - size: 20, - color: - Theme.of(context).colorScheme.onSurfaceVariant, - ), - title: Text( - 'New Terminal', - style: TextStyle( - fontSize: 14, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - dense: true, - onTap: () { - state_ffi.createTerminal( - connId: connection.connId!, - projectId: project.id, - ); - Navigator.of(context).pop(); - }, - ), - ], - ], - ); - } - - void _showReorderMenu(BuildContext context) { - showModalBottomSheet( - context: context, - builder: (ctx) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text(project.name, - style: Theme.of(context).textTheme.titleMedium), - ), - // Color picker option - if (connection.connId != null) - ListTile( - leading: const Icon(Icons.palette), - title: const Text('Change Color'), - onTap: () { - Navigator.of(ctx).pop(); - _showColorPicker( - context: context, - currentColor: project.folderColor, - onSelect: (newColor) { - state_ffi.setProjectColor( - connId: connection.connId!, - projectId: project.id, - color: newColor, - ); - }, - ); - }, - ), - if (reorderIndex != null && reorderIndex! > 0) - ListTile( - leading: const Icon(Icons.arrow_upward), - title: const Text('Move Up'), - onTap: () { - Navigator.of(ctx).pop(); - onReorder!(reorderIndex! - 1); - }, - ), - if (reorderIndex != null && reorderTotal != null && - reorderIndex! < reorderTotal! - 1) - ListTile( - leading: const Icon(Icons.arrow_downward), - title: const Text('Move Down'), - onTap: () { - Navigator.of(ctx).pop(); - onReorder!(reorderIndex! + 1); - }, - ), - if (reorderIndex != null && reorderIndex! > 0) - ListTile( - leading: const Icon(Icons.vertical_align_top), - title: const Text('Move to Top'), - onTap: () { - Navigator.of(ctx).pop(); - onReorder!(0); - }, - ), - if (reorderIndex != null && reorderTotal != null && - reorderIndex! < reorderTotal! - 1) - ListTile( - leading: const Icon(Icons.vertical_align_bottom), - title: const Text('Move to Bottom'), - onTap: () { - Navigator.of(ctx).pop(); - onReorder!(reorderTotal! - 1); - }, - ), - ], - ), - ), - ); - } - - Widget? _buildSubtitle(BuildContext context) { - final parts = []; - if (project.gitBranch != null) { - parts.add(Icon(Icons.commit, size: 12, color: Colors.grey[500])); - parts.add(const SizedBox(width: 2)); - parts.add(Text( - project.gitBranch!, - style: TextStyle(fontSize: 11, color: Colors.grey[500]), - )); - } - final runningServices = - project.services.where((s) => s.status == 'running').length; - if (runningServices > 0) { - if (parts.isNotEmpty) parts.add(const SizedBox(width: 8)); - parts.add(Icon(Icons.dns, size: 12, color: Colors.green[400])); - parts.add(const SizedBox(width: 2)); - parts.add(Text( - '$runningServices', - style: TextStyle(fontSize: 11, color: Colors.green[400]), - )); - } - if (parts.isEmpty) return null; - return Row(children: parts); - } - - void _showTerminalMenu( - BuildContext context, { - required String connId, - required String projectId, - required String terminalId, - required String name, - }) { - showModalBottomSheet( - context: context, - builder: (ctx) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit), - title: const Text('Rename'), - onTap: () { - Navigator.of(ctx).pop(); - _showRenameDialog(context, - connId: connId, - projectId: projectId, - terminalId: terminalId, - currentName: name); - }, - ), - ListTile( - leading: const Icon(Icons.minimize), - title: const Text('Minimize'), - onTap: () { - Navigator.of(ctx).pop(); - state_ffi.toggleMinimized( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - ), - ListTile( - leading: const Icon(Icons.close, color: Colors.redAccent), - title: - const Text('Close', style: TextStyle(color: Colors.redAccent)), - onTap: () { - Navigator.of(ctx).pop(); - Navigator.of(context).pop(); - state_ffi.closeTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - ), - ], - ), - ), - ); - } - - void _showRenameDialog( - BuildContext context, { - required String connId, - required String projectId, - required String terminalId, - required String currentName, - }) { - final controller = TextEditingController(text: currentName); - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Rename Terminal'), - content: TextField( - controller: controller, - autofocus: true, - decoration: const InputDecoration(labelText: 'Name'), - onSubmitted: (_) { - Navigator.of(ctx).pop(); - state_ffi.renameTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - name: controller.text, - ); - }, - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(ctx).pop(); - state_ffi.renameTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - name: controller.text, - ); - }, - child: const Text('Rename'), - ), - ], - ), - ); - } -} - -class _TerminalActions extends StatelessWidget { - final String connId; - final String projectId; - final String terminalId; - final String name; - - const _TerminalActions({ - required this.connId, - required this.projectId, - required this.terminalId, - required this.name, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 32, - height: 32, - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 16, - icon: const Icon(Icons.close, size: 16), - onPressed: () { - Navigator.of(context).pop(); - state_ffi.closeTerminal( - connId: connId, - projectId: projectId, - terminalId: terminalId, - ); - }, - ), - ); - } -} - -class _GitStatusRow extends StatelessWidget { - final state_ffi.ProjectInfo project; - - const _GitStatusRow({required this.project}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 56, right: 16, bottom: 4), - child: Row( - children: [ - Icon(Icons.commit, size: 16, color: Colors.grey[400]), - const SizedBox(width: 6), - Text( - project.gitBranch ?? '', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), - ), - const Spacer(), - if (project.gitLinesAdded > 0) ...[ - Text( - '+${project.gitLinesAdded}', - style: TextStyle(fontSize: 11, color: Colors.green[400]), - ), - const SizedBox(width: 4), - ], - if (project.gitLinesRemoved > 0) - Text( - '-${project.gitLinesRemoved}', - style: TextStyle(fontSize: 11, color: Colors.red[400]), - ), - ], - ), - ); - } -} - -class _ServiceRow extends StatelessWidget { - final state_ffi.ServiceInfo service; - final state_ffi.ProjectInfo project; - final ConnectionProvider connection; - - const _ServiceRow({ - required this.service, - required this.project, - required this.connection, - }); - - Color _statusColor() { - switch (service.status) { - case 'running': - return Colors.green; - case 'stopped': - return Colors.grey; - case 'crashed': - return Colors.red; - case 'starting': - case 'restarting': - return Colors.orange; - default: - return Colors.grey; - } - } - - IconData _statusIcon() { - switch (service.status) { - case 'running': - return Icons.check_circle; - case 'stopped': - return Icons.stop_circle; - case 'crashed': - return Icons.error; - case 'starting': - case 'restarting': - return Icons.hourglass_top; - default: - return Icons.help; - } - } - - @override - Widget build(BuildContext context) { - final connId = connection.connId; - final color = _statusColor(); - - return ListTile( - contentPadding: const EdgeInsets.only(left: 56, right: 8), - leading: Icon(_statusIcon(), size: 18, color: color), - title: Row( - children: [ - Expanded( - child: Text( - service.name, - style: const TextStyle(fontSize: 13), - ), - ), - if (service.ports.isNotEmpty) - Text( - service.ports.map((p) => ':$p').join(' '), - style: TextStyle(fontSize: 11, color: Colors.grey[500]), - ), - ], - ), - dense: true, - trailing: _ServiceActionButton( - service: service, - connId: connId, - projectId: project.id, - ), - ); - } -} - -class _ServiceActionButton extends StatelessWidget { - final state_ffi.ServiceInfo service; - final String? connId; - final String projectId; - - const _ServiceActionButton({ - required this.service, - required this.connId, - required this.projectId, - }); - - @override - Widget build(BuildContext context) { - if (connId == null) return const SizedBox.shrink(); - - return PopupMenuButton( - padding: EdgeInsets.zero, - iconSize: 20, - itemBuilder: (ctx) => [ - if (service.status == 'stopped' || service.status == 'crashed') - const PopupMenuItem(value: 'start', child: Text('Start')), - if (service.status == 'running') - const PopupMenuItem(value: 'stop', child: Text('Stop')), - if (service.status == 'running') - const PopupMenuItem(value: 'restart', child: Text('Restart')), - ], - onSelected: (action) { - switch (action) { - case 'start': - state_ffi.startService( - connId: connId!, - projectId: projectId, - serviceName: service.name, - ); - break; - case 'stop': - state_ffi.stopService( - connId: connId!, - projectId: projectId, - serviceName: service.name, - ); - break; - case 'restart': - state_ffi.restartService( - connId: connId!, - projectId: projectId, - serviceName: service.name, - ); - break; - } - }, - ); - } -} diff --git a/mobile/lib/src/widgets/status_indicator.dart b/mobile/lib/src/widgets/status_indicator.dart deleted file mode 100644 index a1234772..00000000 --- a/mobile/lib/src/widgets/status_indicator.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../src/rust/api/connection.dart'; -import '../theme/app_theme.dart'; - -class StatusIndicator extends StatefulWidget { - final ConnectionStatus status; - - const StatusIndicator({super.key, required this.status}); - - @override - State createState() => _StatusIndicatorState(); -} - -class _StatusIndicatorState extends State - with SingleTickerProviderStateMixin { - late AnimationController _pulseController; - late Animation _pulseAnimation; - - @override - void initState() { - super.initState(); - _pulseController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1200), - ); - _pulseAnimation = Tween(begin: 0.4, end: 1.0).animate( - CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), - ); - _updatePulse(); - } - - @override - void didUpdateWidget(StatusIndicator oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.status.runtimeType != widget.status.runtimeType) { - _updatePulse(); - } - } - - void _updatePulse() { - final shouldPulse = widget.status is ConnectionStatus_Connecting || - widget.status is ConnectionStatus_Pairing; - if (shouldPulse) { - _pulseController.repeat(reverse: true); - } else { - _pulseController.stop(); - _pulseController.value = 1.0; - } - } - - @override - void dispose() { - _pulseController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final (color, label) = switch (widget.status) { - ConnectionStatus_Disconnected() => (OkenaColors.textTertiary, 'Disconnected'), - ConnectionStatus_Connecting() => (OkenaColors.warning, 'Connecting'), - ConnectionStatus_Connected() => (OkenaColors.success, 'Connected'), - ConnectionStatus_Pairing() => (OkenaColors.accent, 'Pairing'), - ConnectionStatus_Error(:final message) => (OkenaColors.error, 'Error: $message'), - }; - - final isConnected = widget.status is ConnectionStatus_Connected; - - return AnimatedBuilder( - animation: _pulseAnimation, - builder: (context, child) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: color.withOpacity(0.1 * _pulseAnimation.value), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: color.withOpacity(0.2 * _pulseAnimation.value), - width: 0.5, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: color.withOpacity(_pulseAnimation.value), - shape: BoxShape.circle, - boxShadow: isConnected - ? [ - BoxShadow( - color: color.withOpacity(0.5), - blurRadius: 6, - spreadRadius: 1, - ), - ] - : null, - ), - ), - const SizedBox(width: 6), - Flexible( - child: Text( - label, - style: OkenaTypography.caption2.copyWith( - color: color.withOpacity(_pulseAnimation.value), - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/src/widgets/terminal_painter.dart b/mobile/lib/src/widgets/terminal_painter.dart deleted file mode 100644 index c950b654..00000000 --- a/mobile/lib/src/widgets/terminal_painter.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; - -import '../../src/rust/api/terminal.dart'; -import '../theme/app_theme.dart'; - -// Flag bitmask constants matching Rust CellData.flags -const _kBold = 1; -const _kItalic = 2; -const _kUnderline = 4; -const _kStrikethrough = 8; -const _kInverse = 16; -const _kDim = 32; - -// Mask for flags that affect text style (excludes _kInverse which is handled -// separately when computing effective fg/bg). -const _kStyleMask = _kBold | _kItalic | _kUnderline | _kStrikethrough | _kDim; - -TextDecoration flagsToDecoration(int flags) { - final decorations = []; - if (flags & _kUnderline != 0) decorations.add(TextDecoration.underline); - if (flags & _kStrikethrough != 0) { - decorations.add(TextDecoration.lineThrough); - } - if (decorations.isEmpty) return TextDecoration.none; - return TextDecoration.combine(decorations); -} - -Color argbToColor(int argb) { - // Rust sends ARGB as u32: 0xAARRGGBB - return Color(argb); -} - -class TerminalPainter extends CustomPainter { - final List cells; - final CursorState cursor; - final int cols; - final int rows; - final double cellWidth; - final double cellHeight; - final double fontSize; - final String fontFamily; - final double devicePixelRatio; - final SelectionBounds? selection; - final ScrollInfo? scrollInfo; - - TerminalPainter({ - required this.cells, - required this.cursor, - required this.cols, - required this.rows, - required this.cellWidth, - required this.cellHeight, - required this.fontSize, - required this.fontFamily, - required this.devicePixelRatio, - this.selection, - this.scrollInfo, - }); - - /// Snap a logical coordinate to device pixel boundaries. - double _snap(double v) => - (v * devicePixelRatio).roundToDouble() / devicePixelRatio; - - bool _isCellInSelection(int col, int row, SelectionBounds sel) { - // Selection bounds are in buffer coordinates; convert visual row - // to buffer row for comparison: buffer_row = visual_row - display_offset - final offset = scrollInfo?.displayOffset.toInt() ?? 0; - final bufferRow = row - offset; - - final sr = sel.startRow; - final er = sel.endRow; - final sc = sel.startCol; - final ec = sel.endCol; - - if (bufferRow < sr || bufferRow > er) return false; - if (sr == er) { - // Single line selection - return col >= sc && col <= ec; - } - if (bufferRow == sr) return col >= sc; - if (bufferRow == er) return col <= ec; - return true; // Middle line — fully selected - } - - @override - void paint(Canvas canvas, Size size) { - final bgPaint = Paint(); - - // Pass 1: Background rectangles + selection highlight - for (int i = 0; i < cells.length && i < cols * rows; i++) { - final cell = cells[i]; - final col = i % cols; - final row = i ~/ cols; - final x = _snap(col * cellWidth); - final y = _snap(row * cellHeight); - - var bgArgb = cell.bg; - var fgArgb = cell.fg; - if (cell.flags & _kInverse != 0) { - final tmp = bgArgb; - bgArgb = fgArgb; - fgArgb = tmp; - } - - final bgColor = argbToColor(bgArgb); - // Only draw non-default backgrounds - if (bgColor != OkenaColors.background && bgColor.a > 0) { - bgPaint.color = bgColor; - canvas.drawRect(Rect.fromLTWH(x, y, cellWidth, cellHeight), bgPaint); - } - - // Selection highlight - if (selection != null && _isCellInSelection(col, row, selection!)) { - bgPaint.color = const Color(0x40264F78); - canvas.drawRect(Rect.fromLTWH(x, y, cellWidth, cellHeight), bgPaint); - } - } - - // Pass 2: Text characters — batched by style runs within each row. - // Consecutive non-space cells with the same effective fg + style flags - // are concatenated into a single TextPainter call, reducing allocations - // from ~cols*rows down to the number of distinct style runs. - for (int row = 0; row < rows; row++) { - int col = 0; - while (col < cols) { - final idx = row * cols + col; - if (idx >= cells.length) break; - - final cell = cells[idx]; - if (cell.character.isEmpty || cell.character == ' ') { - col++; - continue; - } - - // Determine effective fg for the first cell of the run. - var fgArgb = cell.fg; - if (cell.flags & _kInverse != 0) fgArgb = cell.bg; - final styleFlags = cell.flags & _kStyleMask; - - final startCol = col; - final buffer = StringBuffer(); - buffer.write(cell.character); - col++; - - // Extend run with consecutive cells sharing the same style. - while (col < cols) { - final ci = row * cols + col; - if (ci >= cells.length) break; - final c = cells[ci]; - if (c.character.isEmpty || c.character == ' ') break; - - var cFg = c.fg; - if (c.flags & _kInverse != 0) cFg = c.bg; - final cStyleFlags = c.flags & _kStyleMask; - - if (cFg != fgArgb || cStyleFlags != styleFlags) break; - - buffer.write(c.character); - col++; - } - - var fgColor = argbToColor(fgArgb); - if (styleFlags & _kDim != 0) { - fgColor = fgColor.withAlpha((fgColor.a * 0.5).round()); - } - - final tp = TextPainter( - text: TextSpan( - text: buffer.toString(), - style: TextStyle( - fontFamily: fontFamily, - fontFamilyFallback: TerminalTheme.fontFamilyFallback, - fontSize: fontSize, - color: fgColor, - fontWeight: - styleFlags & _kBold != 0 ? FontWeight.bold : FontWeight.normal, - fontStyle: - styleFlags & _kItalic != 0 ? FontStyle.italic : FontStyle.normal, - decoration: flagsToDecoration(styleFlags), - decorationColor: fgColor, - ), - ), - textDirection: ui.TextDirection.ltr, - )..layout(); - - final x = _snap(startCol * cellWidth); - final y = _snap(row * cellHeight); - final dy = y + (cellHeight - tp.height) / 2; - tp.paint(canvas, Offset(x, dy)); - } - } - - // Pass 3: Cursor - if (cursor.visible && cursor.col < cols && cursor.row < rows) { - final cx = _snap(cursor.col * cellWidth); - final cy = _snap(cursor.row * cellHeight); - final cursorPaint = Paint()..color = TerminalTheme.cursorColor; - - switch (cursor.shape) { - case CursorShape.block: - cursorPaint.color = TerminalTheme.cursorColor.withAlpha(128); - canvas.drawRect( - Rect.fromLTWH(cx, cy, cellWidth, cellHeight), - cursorPaint, - ); - case CursorShape.beam: - cursorPaint.strokeWidth = 2; - canvas.drawLine( - Offset(cx, cy), - Offset(cx, cy + cellHeight), - cursorPaint, - ); - case CursorShape.underline: - cursorPaint.strokeWidth = 2; - canvas.drawLine( - Offset(cx, cy + cellHeight - 1), - Offset(cx + cellWidth, cy + cellHeight - 1), - cursorPaint, - ); - } - } - - // Pass 4: Scroll indicator - if (scrollInfo != null && scrollInfo!.totalLines > scrollInfo!.visibleLines) { - final total = scrollInfo!.totalLines; - final visible = scrollInfo!.visibleLines; - final offset = scrollInfo!.displayOffset; - - if (total > 0 && visible > 0) { - final trackHeight = size.height; - final thumbHeight = (visible / total * trackHeight).clamp(20.0, trackHeight); - // offset=0 means at bottom, max offset = total - visible - final maxOffset = total - visible; - final thumbTop = maxOffset > 0 - ? (1.0 - offset / maxOffset) * (trackHeight - thumbHeight) - : 0.0; - - final scrollPaint = Paint() - ..color = const Color(0x40FFFFFF) - ..style = PaintingStyle.fill; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(size.width - 4, thumbTop, 3, thumbHeight), - const Radius.circular(1.5), - ), - scrollPaint, - ); - } - } - } - - @override - bool shouldRepaint(TerminalPainter oldDelegate) { - return !identical(cells, oldDelegate.cells) || - cursor.col != oldDelegate.cursor.col || - cursor.row != oldDelegate.cursor.row || - cursor.shape != oldDelegate.cursor.shape || - cursor.visible != oldDelegate.cursor.visible || - cols != oldDelegate.cols || - rows != oldDelegate.rows || - fontSize != oldDelegate.fontSize; - } -} diff --git a/mobile/lib/src/widgets/terminal_view.dart b/mobile/lib/src/widgets/terminal_view.dart deleted file mode 100644 index 4df8626a..00000000 --- a/mobile/lib/src/widgets/terminal_view.dart +++ /dev/null @@ -1,619 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../../src/rust/api/terminal.dart' as ffi; -import '../../src/rust/api/state.dart' as state_ffi; -import '../theme/app_theme.dart'; -import 'key_toolbar.dart' show KeyModifiers; -import 'terminal_painter.dart'; - -// Sentinel buffer: keeps spaces in the TextField so backspace always has -// something to delete. Without this, Android's soft keyboard backspace -// is a no-op on an empty field and onChanged never fires. -const _kSentinel = ' '; // 8 spaces - -class TerminalView extends StatefulWidget { - final String connId; - final String terminalId; - final KeyModifiers modifiers; - - const TerminalView({ - super.key, - required this.connId, - required this.terminalId, - required this.modifiers, - }); - - @override - State createState() => _TerminalViewState(); -} - -class _TerminalViewState extends State { - List _cells = []; - ffi.CursorState _cursor = const ffi.CursorState( - col: 0, - row: 0, - shape: ffi.CursorShape.block, - visible: true, - ); - int _cols = 80; - int _rows = 24; - double _fontSize = TerminalTheme.defaultFontSize; - double _baseFontSize = TerminalTheme.defaultFontSize; - double _cellWidth = 0; - double _cellHeight = 0; - Timer? _refreshTimer; - - // Resize debounce - Timer? _resizeTimer; - - // Scroll state - ffi.ScrollInfo _scrollInfo = const ffi.ScrollInfo( - totalLines: 0, - visibleLines: 0, - displayOffset: 0, - ); - double _scrollAccumulator = 0; - - // Pinch-to-zoom state - final Map _pointerPositions = {}; - bool _isPinching = false; - bool _hasAutoFit = false; - bool _initialResizeSent = false; - double? _initialPinchDistance; - - // Keyboard input: TextField with its own FocusNode, delta-based tracking - late final FocusNode _inputFocusNode; - final _textController = TextEditingController(text: _kSentinel); - String _lastInputText = _kSentinel; - - // Selection state - bool _isSelecting = false; - ffi.SelectionBounds? _selection; - - // Gesture tracking for scroll vs swipe disambiguation - Offset? _dragStart; - Offset? _lastTapPosition; - - @override - void initState() { - super.initState(); - _inputFocusNode = FocusNode(onKeyEvent: _onKeyEvent); - _computeCellSize(); - _startRefreshLoop(); - } - - @override - void didUpdateWidget(TerminalView oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.connId != widget.connId || - oldWidget.terminalId != widget.terminalId) { - _isSelecting = false; - _selection = null; - _hasAutoFit = false; - _initialResizeSent = false; - _fetchCells(); - } - } - - @override - void dispose() { - _refreshTimer?.cancel(); - _resizeTimer?.cancel(); - _inputFocusNode.dispose(); - _textController.dispose(); - super.dispose(); - } - - void _computeCellSize() { - final tp = TextPainter( - text: TextSpan( - text: 'M', - style: TextStyle( - fontFamily: TerminalTheme.fontFamily, - fontFamilyFallback: TerminalTheme.fontFamilyFallback, - fontSize: _fontSize, - ), - ), - textDirection: ui.TextDirection.ltr, - )..layout(); - - _cellWidth = tp.width; - _cellHeight = tp.height * TerminalTheme.lineHeightFactor; - } - - void _startRefreshLoop() { - _refreshTimer = Timer.periodic( - const Duration(milliseconds: 33), // ~30fps - (_) => _checkDirty(), - ); - } - - void _checkDirty() { - if (!mounted) return; - final dirty = - state_ffi.isDirty(connId: widget.connId, terminalId: widget.terminalId); - if (dirty) { - _fetchCells(); - } - } - - void _fetchCells() { - if (!mounted) return; - setState(() { - _cells = ffi.getVisibleCells( - connId: widget.connId, - terminalId: widget.terminalId, - ); - _cursor = ffi.getCursor( - connId: widget.connId, - terminalId: widget.terminalId, - ); - _scrollInfo = ffi.getScrollInfo( - connId: widget.connId, - terminalId: widget.terminalId, - ); - if (_isSelecting) { - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - } - }); - } - - void _onLayout(BoxConstraints constraints) { - if (_cellWidth <= 0 || _cellHeight <= 0) return; - - // Auto-fit font size once for readable ~80 column display - if (!_hasAutoFit) { - _hasAutoFit = true; - final charWidthRatio = _cellWidth / _fontSize; - _fontSize = (constraints.maxWidth / - (TerminalTheme.defaultColumns * charWidthRatio)) - .clamp(TerminalTheme.minFontSize, TerminalTheme.maxFontSize); - _baseFontSize = _fontSize; - _computeCellSize(); - } - - // Compute cols/rows that fit the mobile screen at the current font size - final newCols = - (constraints.maxWidth / _cellWidth).floor().clamp(1, 500); - final newRows = - (constraints.maxHeight / _cellHeight).floor().clamp(1, 200); - - if (newCols != _cols || newRows != _rows) { - _cols = newCols; - _rows = newRows; - - // Resize local grid immediately for responsive rendering - ffi.resizeLocal( - connId: widget.connId, - terminalId: widget.terminalId, - cols: _cols, - rows: _rows, - ); - - if (!_initialResizeSent) { - // First resize fires immediately — no flash of garbled content - _initialResizeSent = true; - _resizeTimer?.cancel(); - ffi.resizeTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - cols: _cols, - rows: _rows, - ); - } else { - // Debounce subsequent resizes to avoid spamming during layout transitions - _resizeTimer?.cancel(); - _resizeTimer = Timer(const Duration(milliseconds: 200), () { - ffi.resizeTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - cols: _cols, - rows: _rows, - ); - }); - } - } - } - - // --- Touch to cell conversion --- - (int col, int row) _touchToCell(Offset pos) { - final col = (pos.dx / _cellWidth).floor().clamp(0, _cols - 1); - final row = (pos.dy / _cellHeight).floor().clamp(0, _rows - 1); - return (col, row); - } - - // --- Scroll handling --- - void _onVerticalDragUpdate(DragUpdateDetails details) { - if (_isPinching || _cellHeight <= 0) return; - _scrollAccumulator += details.delta.dy; - final lineDelta = (_scrollAccumulator / _cellHeight).truncate(); - if (lineDelta != 0) { - _scrollAccumulator -= lineDelta * _cellHeight; - ffi.scrollTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - delta: lineDelta, - ); - _fetchCells(); - } - } - - void _onVerticalDragEnd(DragEndDetails details) { - _scrollAccumulator = 0; - } - - // --- Selection handling --- - void _onLongPressStart(LongPressStartDetails details) { - final (col, row) = _touchToCell(details.localPosition); - ffi.startSelection( - connId: widget.connId, - terminalId: widget.terminalId, - col: col, - row: row, - ); - setState(() { - _isSelecting = true; - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - }); - } - - void _onLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (!_isSelecting) return; - final (col, row) = _touchToCell(details.localPosition); - ffi.updateSelection( - connId: widget.connId, - terminalId: widget.terminalId, - col: col, - row: row, - ); - setState(() { - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - }); - } - - void _onLongPressEnd(LongPressEndDetails details) { - if (!_isSelecting) return; - _copySelectionAndClear(); - } - - void _onDoubleTap() { - // Use the last known tap position for word selection - if (_lastTapPosition != null) { - final (col, row) = _touchToCell(_lastTapPosition!); - ffi.startWordSelection( - connId: widget.connId, - terminalId: widget.terminalId, - col: col, - row: row, - ); - setState(() { - _isSelecting = true; - _selection = ffi.getSelectionBounds( - connId: widget.connId, - terminalId: widget.terminalId, - ); - }); - _copySelectionAndClear(); - } - } - - void _onTapDown(TapDownDetails details) { - _lastTapPosition = details.localPosition; - } - - void _onTap() { - // Clear selection on single tap if one exists - if (_isSelecting) { - ffi.clearSelection( - connId: widget.connId, - terminalId: widget.terminalId, - ); - setState(() { - _isSelecting = false; - _selection = null; - }); - return; - } - _inputFocusNode.requestFocus(); - } - - void _copySelectionAndClear() { - final text = ffi.getSelectedText( - connId: widget.connId, - terminalId: widget.terminalId, - ); - if (text != null && text.isNotEmpty) { - Clipboard.setData(ClipboardData(text: text)); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied to clipboard'), - duration: Duration(seconds: 1), - ), - ); - } - } - ffi.clearSelection( - connId: widget.connId, - terminalId: widget.terminalId, - ); - setState(() { - _isSelecting = false; - _selection = null; - }); - } - - // --- Pinch-to-zoom via raw pointer events --- - - double _computePinchDistance() { - final points = _pointerPositions.values.toList(); - if (points.length < 2) return 0; - return (points[0] - points[1]).distance; - } - - void _onPointerDown(PointerDownEvent event) { - _pointerPositions[event.pointer] = event.localPosition; - if (_pointerPositions.length == 2) { - _isPinching = true; - _baseFontSize = _fontSize; - _initialPinchDistance = _computePinchDistance(); - } - } - - void _onPointerMove(PointerMoveEvent event) { - _pointerPositions[event.pointer] = event.localPosition; - if (_pointerPositions.length >= 2 && - _initialPinchDistance != null && - _initialPinchDistance! > 0) { - final scale = _computePinchDistance() / _initialPinchDistance!; - final newSize = (_baseFontSize * scale).clamp( - TerminalTheme.minFontSize, - TerminalTheme.maxFontSize, - ); - if (newSize != _fontSize) { - setState(() { - _fontSize = newSize; - _computeCellSize(); - }); - } - } - } - - void _onPointerUp(PointerUpEvent event) { - _pointerPositions.remove(event.pointer); - if (_pointerPositions.length < 2) { - _isPinching = false; - _initialPinchDistance = null; - } - } - - void _onPointerCancel(PointerCancelEvent event) { - _pointerPositions.remove(event.pointer); - if (_pointerPositions.length < 2) { - _isPinching = false; - _initialPinchDistance = null; - } - } - - void _scrollToBottom() { - final offset = ffi.getDisplayOffset( - connId: widget.connId, - terminalId: widget.terminalId, - ); - if (offset > 0) { - ffi.scrollTerminal( - connId: widget.connId, - terminalId: widget.terminalId, - delta: -offset, - ); - } - } - - void _resetSentinel() { - _textController.text = _kSentinel; - _textController.selection = - TextSelection.collapsed(offset: _kSentinel.length); - _lastInputText = _kSentinel; - } - - String _applyModifiers(String chars) { - final mod = widget.modifiers; - if (!mod.hasAny) return chars; - - final buf = StringBuffer(); - for (final ch in chars.codeUnits) { - if (mod.ctrl) { - // Control character: a-z → 0x01-0x1A, A-Z → 0x01-0x1A - if (ch >= 0x61 && ch <= 0x7A) { - buf.writeCharCode(ch - 0x60); - } else if (ch >= 0x41 && ch <= 0x5A) { - buf.writeCharCode(ch - 0x40); - } - } else if (mod.option || mod.cmd) { - // Meta/Option: ESC prefix + character - buf.write('\x1b'); - buf.writeCharCode(ch); - } - } - mod.reset(); - return buf.toString(); - } - - void _onTextChanged(String newText) { - if (newText.length > _lastInputText.length) { - // Characters added — send the delta. - // Convert \n (from soft keyboard Return) to \r (terminal Enter). - var delta = newText.substring(_lastInputText.length).replaceAll('\n', '\r'); - delta = _applyModifiers(delta); - _scrollToBottom(); - ffi.sendText( - connId: widget.connId, - terminalId: widget.terminalId, - text: delta, - ); - } else if (newText.length < _lastInputText.length) { - // Characters deleted — user pressed backspace - final deletedCount = _lastInputText.length - newText.length; - for (int i = 0; i < deletedCount; i++) { - state_ffi.sendSpecialKey( - connId: widget.connId, - terminalId: widget.terminalId, - key: 'Backspace', - ); - } - } - _lastInputText = newText; - - // Re-seed if buffer runs low (backspace ate into the sentinel) - if (newText.length < 3) { - _resetSentinel(); - } - // Reset if too long to prevent unbounded growth - if (newText.length > 200) { - _resetSentinel(); - } - } - - KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - final key = event.logicalKey; - String? specialKey; - - if (key == LogicalKeyboardKey.enter) { - specialKey = 'Enter'; - } else if (key == LogicalKeyboardKey.arrowUp) { - specialKey = 'ArrowUp'; - } else if (key == LogicalKeyboardKey.arrowDown) { - specialKey = 'ArrowDown'; - } else if (key == LogicalKeyboardKey.arrowLeft) { - specialKey = 'ArrowLeft'; - } else if (key == LogicalKeyboardKey.arrowRight) { - specialKey = 'ArrowRight'; - } else if (key == LogicalKeyboardKey.home) { - specialKey = 'Home'; - } else if (key == LogicalKeyboardKey.end) { - specialKey = 'End'; - } else if (key == LogicalKeyboardKey.pageUp) { - specialKey = 'PageUp'; - } else if (key == LogicalKeyboardKey.pageDown) { - specialKey = 'PageDown'; - } else if (key == LogicalKeyboardKey.delete) { - specialKey = 'Delete'; - } else if (key == LogicalKeyboardKey.tab) { - specialKey = 'Tab'; - } else if (key == LogicalKeyboardKey.escape) { - specialKey = 'Escape'; - } - - if (specialKey != null) { - _scrollToBottom(); - state_ffi.sendSpecialKey( - connId: widget.connId, - terminalId: widget.terminalId, - key: specialKey, - ); - return KeyEventResult.handled; - } - - // Let TextField handle normal character input via onChanged - return KeyEventResult.ignored; - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - _onLayout(constraints); - - return Listener( - onPointerDown: _onPointerDown, - onPointerMove: _onPointerMove, - onPointerUp: _onPointerUp, - onPointerCancel: _onPointerCancel, - child: GestureDetector( - onTapDown: _onTapDown, - onTap: _onTap, - onDoubleTap: _onDoubleTap, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - // Long press for selection - onLongPressStart: _onLongPressStart, - onLongPressMoveUpdate: _onLongPressMoveUpdate, - onLongPressEnd: _onLongPressEnd, - child: Container( - color: OkenaColors.background, - width: constraints.maxWidth, - height: constraints.maxHeight, - child: Stack( - children: [ - // Terminal canvas - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: TerminalPainter( - cells: _cells, - cursor: _cursor, - cols: _cols, - rows: _rows, - cellWidth: _cellWidth, - cellHeight: _cellHeight, - fontSize: _fontSize, - fontFamily: TerminalTheme.fontFamily, - devicePixelRatio: MediaQuery.devicePixelRatioOf(context), - selection: _selection, - scrollInfo: _scrollInfo, - ), - ), - // Transparent text field for soft keyboard input. - // Fills the terminal area so tapping anywhere opens the - // keyboard. Opacity > 0 keeps the iOS IME connected. - Positioned.fill( - child: Opacity( - opacity: 0.01, - child: TextField( - focusNode: _inputFocusNode, - controller: _textController, - autofocus: false, - enableSuggestions: false, - autocorrect: false, - showCursor: false, - enableInteractiveSelection: false, - onChanged: _onTextChanged, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - maxLines: null, - decoration: const InputDecoration.collapsed( - hintText: '', - ), - style: const TextStyle( - color: Colors.transparent, - fontSize: 16, - height: 1, - ), - ), - ), - ), - ], - ), - ), - ), - ); - }, - ); - } -} diff --git a/mobile/linux/.gitignore b/mobile/linux/.gitignore deleted file mode 100644 index d3896c98..00000000 --- a/mobile/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/mobile/linux/CMakeLists.txt b/mobile/linux/CMakeLists.txt deleted file mode 100644 index 9a39eb1b..00000000 --- a/mobile/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "mobile") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.mobile") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/mobile/linux/flutter/CMakeLists.txt b/mobile/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd0164..00000000 --- a/mobile/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/mobile/linux/flutter/generated_plugin_registrant.cc b/mobile/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d2..00000000 --- a/mobile/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/mobile/linux/flutter/generated_plugin_registrant.h b/mobile/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47b..00000000 --- a/mobile/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/mobile/linux/flutter/generated_plugins.cmake b/mobile/linux/flutter/generated_plugins.cmake deleted file mode 100644 index cc30e9e3..00000000 --- a/mobile/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - rust_lib_mobile -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/mobile/linux/runner/CMakeLists.txt b/mobile/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc7..00000000 --- a/mobile/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/mobile/linux/runner/main.cc b/mobile/linux/runner/main.cc deleted file mode 100644 index e7c5c543..00000000 --- a/mobile/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/mobile/linux/runner/my_application.cc b/mobile/linux/runner/my_application.cc deleted file mode 100644 index c03496ad..00000000 --- a/mobile/linux/runner/my_application.cc +++ /dev/null @@ -1,148 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Called when first Flutter frame received. -static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); -} - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "mobile"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "mobile"); - } - - gtk_window_set_default_size(window, 1280, 720); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments( - project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - GdkRGBA background_color; - // Background defaults to black, override it here if necessary, e.g. #00000000 - // for transparent. - gdk_rgba_parse(&background_color, "#000000"); - fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - // Show the window when Flutter renders. - // Requires the view to be realized so we can start rendering. - g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), - self); - gtk_widget_realize(GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, - gchar*** arguments, - int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, "flags", - G_APPLICATION_NON_UNIQUE, nullptr)); -} diff --git a/mobile/linux/runner/my_application.h b/mobile/linux/runner/my_application.h deleted file mode 100644 index db16367a..00000000 --- a/mobile/linux/runner/my_application.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, - my_application, - MY, - APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml deleted file mode 100644 index a2bdd99d..00000000 --- a/mobile/pubspec.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: mobile -description: "Okena mobile client — remote terminal access." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: ^3.10.8 - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - rust_lib_mobile: - path: rust_builder - flutter_rust_bridge: 2.11.1 - freezed_annotation: ^3.1.0 - provider: ^6.1.0 - shared_preferences: ^2.2.0 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^6.0.0 - integration_test: - sdk: flutter - build_runner: ^2.11.0 - freezed: ^3.2.5 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - fonts: - - family: JetBrainsMono - fonts: - - asset: fonts/JetBrainsMono-Regular.ttf - - asset: fonts/JetBrainsMono-Bold.ttf - weight: 700 - - asset: fonts/JetBrainsMono-Italic.ttf - style: italic - - asset: fonts/JetBrainsMono-BoldItalic.ttf - weight: 700 - style: italic diff --git a/mobile/rust_builder/.gitignore b/mobile/rust_builder/.gitignore deleted file mode 100644 index ac5aa989..00000000 --- a/mobile/rust_builder/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -build/ diff --git a/mobile/rust_builder/README.md b/mobile/rust_builder/README.md deleted file mode 100644 index 922615f9..00000000 --- a/mobile/rust_builder/README.md +++ /dev/null @@ -1 +0,0 @@ -Please ignore this folder, which is just glue to build Rust with Flutter. \ No newline at end of file diff --git a/mobile/rust_builder/android/.gitignore b/mobile/rust_builder/android/.gitignore deleted file mode 100644 index 161bdcda..00000000 --- a/mobile/rust_builder/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/mobile/rust_builder/android/build.gradle b/mobile/rust_builder/android/build.gradle deleted file mode 100644 index ad905207..00000000 --- a/mobile/rust_builder/android/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -// The Android Gradle Plugin builds the native code with the Android NDK. - -group 'com.flutter_rust_bridge.rust_lib_mobile' -version '1.0' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - // The Android Gradle Plugin knows how to build native code with the NDK. - classpath 'com.android.tools.build:gradle:7.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - if (project.android.hasProperty("namespace")) { - namespace 'com.flutter_rust_bridge.rust_lib_mobile' - } - - // Bumping the plugin compileSdkVersion requires all clients of this plugin - // to bump the version in their app. - compileSdkVersion 33 - - // Use the NDK version - // declared in /android/app/build.gradle file of the Flutter project. - // Replace it with a version number if this plugin requires a specfic NDK version. - // (e.g. ndkVersion "23.1.7779620") - ndkVersion android.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - defaultConfig { - minSdkVersion 19 - } -} - -apply from: "../cargokit/gradle/plugin.gradle" -cargokit { - manifestDir = "../../native" - libname = "okena_mobile_native" -} diff --git a/mobile/rust_builder/android/settings.gradle b/mobile/rust_builder/android/settings.gradle deleted file mode 100644 index b6346f0f..00000000 --- a/mobile/rust_builder/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'rust_lib_mobile' diff --git a/mobile/rust_builder/android/src/main/AndroidManifest.xml b/mobile/rust_builder/android/src/main/AndroidManifest.xml deleted file mode 100644 index a82d4cb8..00000000 --- a/mobile/rust_builder/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/mobile/rust_builder/cargokit/.gitignore b/mobile/rust_builder/cargokit/.gitignore deleted file mode 100644 index cf7bb868..00000000 --- a/mobile/rust_builder/cargokit/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -.dart_tool -*.iml -!pubspec.lock diff --git a/mobile/rust_builder/cargokit/LICENSE b/mobile/rust_builder/cargokit/LICENSE deleted file mode 100644 index d33a5fea..00000000 --- a/mobile/rust_builder/cargokit/LICENSE +++ /dev/null @@ -1,42 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -Copyright 2022 Matej Knopp - -================================================================================ - -MIT LICENSE - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -================================================================================ - -APACHE LICENSE, VERSION 2.0 - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - diff --git a/mobile/rust_builder/cargokit/README b/mobile/rust_builder/cargokit/README deleted file mode 100644 index 398474db..00000000 --- a/mobile/rust_builder/cargokit/README +++ /dev/null @@ -1,11 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -Experimental repository to provide glue for seamlessly integrating cargo build -with flutter plugins and packages. - -See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ -for a tutorial on how to use Cargokit. - -Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. - diff --git a/mobile/rust_builder/cargokit/build_pod.sh b/mobile/rust_builder/cargokit/build_pod.sh deleted file mode 100755 index ed0e0d98..00000000 --- a/mobile/rust_builder/cargokit/build_pod.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/sh -set -e - -BASEDIR=$(dirname "$0") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -BASEDIR=$(cd "$BASEDIR" ; pwd -P) - -# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project -NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` - -export PATH=${NEW_PATH%?} # remove trailing : - -env - -# Platform name (macosx, iphoneos, iphonesimulator) -export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME - -# Arctive architectures (arm64, armv7, x86_64), space separated. -export CARGOKIT_DARWIN_ARCHS=$ARCHS - -# Current build configuration (Debug, Release) -export CARGOKIT_CONFIGURATION=$CONFIGURATION - -# Path to directory containing Cargo.toml. -export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 - -# Temporary directory for build artifacts. -export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR - -# Output directory for final artifacts. -export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME - -# Directory to store built tool artifacts. -export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool - -# Directory inside root project. Not necessarily the top level directory of root project. -export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT - -FLUTTER_EXPORT_BUILD_ENVIRONMENT=( - "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS - "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS -) - -for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" -do - if [[ -f "$path" ]]; then - source "$path" - fi -done - -sh "$BASEDIR/run_build_tool.sh" build-pod "$@" - -# Make a symlink from built framework to phony file, which will be used as input to -# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate -# attribute on custom build phase) -ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" -ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/mobile/rust_builder/cargokit/build_tool/README.md b/mobile/rust_builder/cargokit/build_tool/README.md deleted file mode 100644 index a878c279..00000000 --- a/mobile/rust_builder/cargokit/build_tool/README.md +++ /dev/null @@ -1,5 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -A sample command-line application with an entrypoint in `bin/`, library code -in `lib/`, and example unit test in `test/`. diff --git a/mobile/rust_builder/cargokit/build_tool/analysis_options.yaml b/mobile/rust_builder/cargokit/build_tool/analysis_options.yaml deleted file mode 100644 index 0e16a8b0..00000000 --- a/mobile/rust_builder/cargokit/build_tool/analysis_options.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# This is copied from Cargokit (which is the official way to use it currently) -# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:lints/recommended.yaml - -# Uncomment the following section to specify additional rules. - -linter: - rules: - - prefer_relative_imports - - directives_ordering - -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options diff --git a/mobile/rust_builder/cargokit/build_tool/bin/build_tool.dart b/mobile/rust_builder/cargokit/build_tool/bin/build_tool.dart deleted file mode 100644 index 268eb524..00000000 --- a/mobile/rust_builder/cargokit/build_tool/bin/build_tool.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'package:build_tool/build_tool.dart' as build_tool; - -void main(List arguments) { - build_tool.runMain(arguments); -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/build_tool.dart b/mobile/rust_builder/cargokit/build_tool/lib/build_tool.dart deleted file mode 100644 index 7c1bb750..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/build_tool.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'src/build_tool.dart' as build_tool; - -Future runMain(List args) async { - return build_tool.runMain(args); -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/android_environment.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/android_environment.dart deleted file mode 100644 index 15fc9eed..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/android_environment.dart +++ /dev/null @@ -1,195 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; -import 'package:version/version.dart'; - -import 'target.dart'; -import 'util.dart'; - -class AndroidEnvironment { - AndroidEnvironment({ - required this.sdkPath, - required this.ndkVersion, - required this.minSdkVersion, - required this.targetTempDir, - required this.target, - }); - - static void clangLinkerWrapper(List args) { - final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; - if (clang == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); - } - final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; - if (target == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); - } - - runCommand(clang, [ - target, - ...args, - ]); - } - - /// Full path to Android SDK. - final String sdkPath; - - /// Full version of Android NDK. - final String ndkVersion; - - /// Minimum supported SDK version. - final int minSdkVersion; - - /// Target directory for build artifacts. - final String targetTempDir; - - /// Target being built. - final Target target; - - bool ndkIsInstalled() { - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); - return ndkPackageXml.existsSync(); - } - - void installNdk({ - required String javaHome, - }) { - final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; - final sdkManager = path.join( - sdkPath, - 'cmdline-tools', - 'latest', - 'bin', - 'sdkmanager$sdkManagerExtension', - ); - - log.info('Installing NDK $ndkVersion'); - runCommand(sdkManager, [ - '--install', - 'ndk;$ndkVersion', - ], environment: { - 'JAVA_HOME': javaHome, - }); - } - - Future> buildEnvironment() async { - final hostArch = Platform.isMacOS - ? "darwin-x86_64" - : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); - - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final toolchainPath = path.join( - ndkPath, - 'toolchains', - 'llvm', - 'prebuilt', - hostArch, - 'bin', - ); - - final minSdkVersion = - math.max(target.androidMinSdkVersion!, this.minSdkVersion); - - final exe = Platform.isWindows ? '.exe' : ''; - - final arKey = 'AR_${target.rust}'; - final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] - .map((e) => path.join(toolchainPath, e)) - .firstWhereOrNull((element) => File(element).existsSync()); - if (arValue == null) { - throw Exception('Failed to find ar for $target in $toolchainPath'); - } - - final targetArg = '--target=${target.rust}$minSdkVersion'; - - final ccKey = 'CC_${target.rust}'; - final ccValue = path.join(toolchainPath, 'clang$exe'); - final cfFlagsKey = 'CFLAGS_${target.rust}'; - final cFlagsValue = targetArg; - - final cxxKey = 'CXX_${target.rust}'; - final cxxValue = path.join(toolchainPath, 'clang++$exe'); - final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; - final cxxFlagsValue = targetArg; - - final linkerKey = - 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); - - final ranlibKey = 'RANLIB_${target.rust}'; - final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); - - final ndkVersionParsed = Version.parse(ndkVersion); - final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; - final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); - - final runRustTool = - Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; - - final packagePath = (await Isolate.resolvePackageUri( - Uri.parse('package:build_tool/buildtool.dart')))! - .toFilePath(); - final selfPath = path.canonicalize(path.join( - packagePath, - '..', - '..', - '..', - runRustTool, - )); - - // Make sure that run_build_tool is working properly even initially launched directly - // through dart run. - final toolTempDir = - Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; - - return { - arKey: arValue, - ccKey: ccValue, - cfFlagsKey: cFlagsValue, - cxxKey: cxxValue, - cxxFlagsKey: cxxFlagsValue, - ranlibKey: ranlibValue, - rustFlagsKey: rustFlagsValue, - linkerKey: selfPath, - // Recognized by main() so we know when we're acting as a wrapper - '_CARGOKIT_NDK_LINK_TARGET': targetArg, - '_CARGOKIT_NDK_LINK_CLANG': ccValue, - 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, - }; - } - - // Workaround for libgcc missing in NDK23, inspired by cargo-ndk - String _libGccWorkaround(String buildDir, Version ndkVersion) { - final workaroundDir = path.join( - buildDir, - 'cargokit', - 'libgcc_workaround', - '${ndkVersion.major}', - ); - Directory(workaroundDir).createSync(recursive: true); - if (ndkVersion.major >= 23) { - File(path.join(workaroundDir, 'libgcc.a')) - .writeAsStringSync('INPUT(-lunwind)'); - } else { - // Other way around, untested, forward libgcc.a from libunwind once Rust - // gets updated for NDK23+. - File(path.join(workaroundDir, 'libunwind.a')) - .writeAsStringSync('INPUT(-lgcc)'); - } - - var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; - if (rustFlags.isNotEmpty) { - rustFlags = '$rustFlags\x1f'; - } - rustFlags = '$rustFlags-L\x1f$workaroundDir'; - return rustFlags; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart deleted file mode 100644 index e608cece..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart +++ /dev/null @@ -1,266 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'builder.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'rustup.dart'; -import 'target.dart'; - -class Artifact { - /// File system location of the artifact. - final String path; - - /// Actual file name that the artifact should have in destination folder. - final String finalFileName; - - AritifactType get type { - if (finalFileName.endsWith('.dll') || - finalFileName.endsWith('.dll.lib') || - finalFileName.endsWith('.pdb') || - finalFileName.endsWith('.so') || - finalFileName.endsWith('.dylib')) { - return AritifactType.dylib; - } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { - return AritifactType.staticlib; - } else { - throw Exception('Unknown artifact type for $finalFileName'); - } - } - - Artifact({ - required this.path, - required this.finalFileName, - }); -} - -final _log = Logger('artifacts_provider'); - -class ArtifactProvider { - ArtifactProvider({ - required this.environment, - required this.userOptions, - }); - - final BuildEnvironment environment; - final CargokitUserOptions userOptions; - - Future>> getArtifacts(List targets) async { - final result = await _getPrecompiledArtifacts(targets); - - final pendingTargets = List.of(targets); - pendingTargets.removeWhere((element) => result.containsKey(element)); - - if (pendingTargets.isEmpty) { - return result; - } - - final rustup = Rustup(); - for (final target in targets) { - final builder = RustBuilder(target: target, environment: environment); - builder.prepare(rustup); - _log.info('Building ${environment.crateInfo.packageName} for $target'); - final targetDir = await builder.build(); - // For local build accept both static and dynamic libraries. - final artifactNames = { - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.dylib, - remote: false, - ), - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.staticlib, - remote: false, - ) - }; - final artifacts = artifactNames - .map((artifactName) => Artifact( - path: path.join(targetDir, artifactName), - finalFileName: artifactName, - )) - .where((element) => File(element.path).existsSync()) - .toList(); - result[target] = artifacts; - } - return result; - } - - Future>> _getPrecompiledArtifacts( - List targets) async { - if (userOptions.usePrecompiledBinaries == false) { - _log.info('Precompiled binaries are disabled'); - return {}; - } - if (environment.crateOptions.precompiledBinaries == null) { - _log.fine('Precompiled binaries not enabled for this crate'); - return {}; - } - - final start = Stopwatch()..start(); - final crateHash = CrateHash.compute(environment.manifestDir, - tempStorage: environment.targetTempDir); - _log.fine( - 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); - - final downloadedArtifactsDir = - path.join(environment.targetTempDir, 'precompiled', crateHash); - Directory(downloadedArtifactsDir).createSync(recursive: true); - - final res = >{}; - - for (final target in targets) { - final requiredArtifacts = getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - remote: true, - ); - final artifactsForTarget = []; - - for (final artifact in requiredArtifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final downloadedPath = path.join(downloadedArtifactsDir, fileName); - if (!File(downloadedPath).existsSync()) { - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - await _tryDownloadArtifacts( - crateHash: crateHash, - fileName: fileName, - signatureFileName: signatureFileName, - finalPath: downloadedPath, - ); - } - if (File(downloadedPath).existsSync()) { - artifactsForTarget.add(Artifact( - path: downloadedPath, - finalFileName: artifact, - )); - } else { - break; - } - } - - // Only provide complete set of artifacts. - if (artifactsForTarget.length == requiredArtifacts.length) { - _log.fine('Found precompiled artifacts for $target'); - res[target] = artifactsForTarget; - } - } - - return res; - } - - static Future _get(Uri url, {Map? headers}) async { - int attempt = 0; - const maxAttempts = 10; - while (true) { - try { - return await get(url, headers: headers); - } on SocketException catch (e) { - // Try to detect reset by peer error and retry. - if (attempt++ < maxAttempts && - (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { - _log.severe( - 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); - await Future.delayed(Duration(seconds: 1)); - continue; - } else { - rethrow; - } - } - } - } - - Future _tryDownloadArtifacts({ - required String crateHash, - required String fileName, - required String signatureFileName, - required String finalPath, - }) async { - final precompiledBinaries = environment.crateOptions.precompiledBinaries!; - final prefix = precompiledBinaries.uriPrefix; - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); - _log.fine('Downloading signature from $signatureUrl'); - final signature = await _get(signatureUrl); - if (signature.statusCode == 404) { - _log.warning( - 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); - return; - } - if (signature.statusCode != 200) { - _log.severe( - 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); - return; - } - _log.fine('Downloading binary from $url'); - final res = await _get(url); - if (res.statusCode != 200) { - _log.severe('Failed to download binary $url: status ${res.statusCode}'); - return; - } - if (verify( - precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { - File(finalPath).writeAsBytesSync(res.bodyBytes); - } else { - _log.shout('Signature verification failed! Ignoring binary.'); - } - } -} - -enum AritifactType { - staticlib, - dylib, -} - -AritifactType artifactTypeForTarget(Target target) { - if (target.darwinPlatform != null) { - return AritifactType.staticlib; - } else { - return AritifactType.dylib; - } -} - -List getArtifactNames({ - required Target target, - required String libraryName, - required bool remote, - AritifactType? aritifactType, -}) { - aritifactType ??= artifactTypeForTarget(target); - if (target.darwinArch != null) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.dylib']; - } - } else if (target.rust.contains('-windows-')) { - if (aritifactType == AritifactType.staticlib) { - return ['$libraryName.lib']; - } else { - return [ - '$libraryName.dll', - '$libraryName.dll.lib', - if (!remote) '$libraryName.pdb' - ]; - } - } else if (target.rust.contains('-linux-')) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.so']; - } - } else { - throw Exception("Unsupported target: ${target.rust}"); - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart deleted file mode 100644 index 6f3b2a4e..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart +++ /dev/null @@ -1,40 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -class BuildCMake { - final CargokitUserOptions userOptions; - - BuildCMake({required this.userOptions}); - - Future build() async { - final targetPlatform = Environment.targetPlatform; - final target = Target.forFlutterName(Environment.targetPlatform); - if (target == null) { - throw Exception("Unknown target platform: $targetPlatform"); - } - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts([target]); - - final libs = artifacts[target]!; - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path) - .copySync(path.join(Environment.outputDir, lib.finalFileName)); - } - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart deleted file mode 100644 index 7e61fcbb..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart +++ /dev/null @@ -1,49 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -final log = Logger('build_gradle'); - -class BuildGradle { - BuildGradle({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.targetPlatforms.map((arch) { - final target = Target.forFlutterName(arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: true); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - for (final target in targets) { - final libs = artifacts[target]!; - final outputDir = path.join(Environment.outputDir, target.android!); - Directory(outputDir).createSync(recursive: true); - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); - } - } - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_pod.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_pod.dart deleted file mode 100644 index 8a9c0db5..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_pod.dart +++ /dev/null @@ -1,89 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; -import 'util.dart'; - -class BuildPod { - BuildPod({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.darwinArchs.map((arch) { - final target = Target.forDarwin( - platformName: Environment.darwinPlatformName, darwinAarch: arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - void performLipo(String targetFile, Iterable sourceFiles) { - runCommand("lipo", [ - '-create', - ...sourceFiles, - '-output', - targetFile, - ]); - } - - final outputDir = Environment.outputDir; - - Directory(outputDir).createSync(recursive: true); - - final staticLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.staticlib) - .toList(); - final dynamicLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.dylib) - .toList(); - - final libName = environment.crateInfo.packageName; - - // If there is static lib, use it and link it with pod - if (staticLibs.isNotEmpty) { - final finalTargetFile = path.join(outputDir, "lib$libName.a"); - performLipo(finalTargetFile, staticLibs.map((e) => e.path)); - } else { - // Otherwise try to replace bundle dylib with our dylib - final bundlePaths = [ - '$libName.framework/Versions/A/$libName', - '$libName.framework/$libName', - ]; - - for (final bundlePath in bundlePaths) { - final targetFile = path.join(outputDir, bundlePath); - if (File(targetFile).existsSync()) { - performLipo(targetFile, dynamicLibs.map((e) => e.path)); - - // Replace absolute id with @rpath one so that it works properly - // when moved to Frameworks. - runCommand("install_name_tool", [ - '-id', - '@rpath/$bundlePath', - targetFile, - ]); - return; - } - } - throw Exception('Unable to find bundle for dynamic library'); - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/build_tool.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/build_tool.dart deleted file mode 100644 index c8f36981..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/build_tool.dart +++ /dev/null @@ -1,271 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; - -import 'android_environment.dart'; -import 'build_cmake.dart'; -import 'build_gradle.dart'; -import 'build_pod.dart'; -import 'logging.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; -import 'util.dart'; -import 'verify_binaries.dart'; - -final log = Logger('build_tool'); - -abstract class BuildCommand extends Command { - Future runBuildCommand(CargokitUserOptions options); - - @override - Future run() async { - final options = CargokitUserOptions.load(); - - if (options.verboseLogging || - Platform.environment['CARGOKIT_VERBOSE'] == '1') { - enableVerboseLogging(); - } - - await runBuildCommand(options); - } -} - -class BuildPodCommand extends BuildCommand { - @override - final name = 'build-pod'; - - @override - final description = 'Build cocoa pod library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildPod(userOptions: options); - await build.build(); - } -} - -class BuildGradleCommand extends BuildCommand { - @override - final name = 'build-gradle'; - - @override - final description = 'Build android library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildGradle(userOptions: options); - await build.build(); - } -} - -class BuildCMakeCommand extends BuildCommand { - @override - final name = 'build-cmake'; - - @override - final description = 'Build CMake library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildCMake(userOptions: options); - await build.build(); - } -} - -class GenKeyCommand extends Command { - @override - final name = 'gen-key'; - - @override - final description = 'Generate key pair for signing precompiled binaries'; - - @override - void run() { - final kp = generateKey(); - final private = HEX.encode(kp.privateKey.bytes); - final public = HEX.encode(kp.publicKey.bytes); - print("Private Key: $private"); - print("Public Key: $public"); - } -} - -class PrecompileBinariesCommand extends Command { - PrecompileBinariesCommand() { - argParser - ..addOption( - 'repository', - mandatory: true, - help: 'Github repository slug in format owner/name', - ) - ..addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ) - ..addMultiOption('target', - help: 'Rust target triple of artifact to build.\n' - 'Can be specified multiple times or omitted in which case\n' - 'all targets for current platform will be built.') - ..addOption( - 'android-sdk-location', - help: 'Location of Android SDK (if available)', - ) - ..addOption( - 'android-ndk-version', - help: 'Android NDK version (if available)', - ) - ..addOption( - 'android-min-sdk-version', - help: 'Android minimum rquired version (if available)', - ) - ..addOption( - 'temp-dir', - help: 'Directory to store temporary build artifacts', - ) - ..addFlag( - "verbose", - abbr: "v", - defaultsTo: false, - help: "Enable verbose logging", - ); - } - - @override - final name = 'precompile-binaries'; - - @override - final description = 'Prebuild and upload binaries\n' - 'Private key must be passed through PRIVATE_KEY environment variable. ' - 'Use gen_key through generate priave key.\n' - 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; - - @override - Future run() async { - final verbose = argResults!['verbose'] as bool; - if (verbose) { - enableVerboseLogging(); - } - - final privateKeyString = Platform.environment['PRIVATE_KEY']; - if (privateKeyString == null) { - throw ArgumentError('Missing PRIVATE_KEY environment variable'); - } - final githubToken = Platform.environment['GITHUB_TOKEN']; - if (githubToken == null) { - throw ArgumentError('Missing GITHUB_TOKEN environment variable'); - } - final privateKey = HEX.decode(privateKeyString); - if (privateKey.length != 64) { - throw ArgumentError('Private key must be 64 bytes long'); - } - final manifestDir = argResults!['manifest-dir'] as String; - if (!Directory(manifestDir).existsSync()) { - throw ArgumentError('Manifest directory does not exist: $manifestDir'); - } - String? androidMinSdkVersionString = - argResults!['android-min-sdk-version'] as String?; - int? androidMinSdkVersion; - if (androidMinSdkVersionString != null) { - androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); - if (androidMinSdkVersion == null) { - throw ArgumentError( - 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); - } - } - final targetStrigns = argResults!['target'] as List; - final targets = targetStrigns.map((target) { - final res = Target.forRustTriple(target); - if (res == null) { - throw ArgumentError('Invalid target: $target'); - } - return res; - }).toList(growable: false); - final precompileBinaries = PrecompileBinaries( - privateKey: PrivateKey(privateKey), - githubToken: githubToken, - manifestDir: manifestDir, - repositorySlug: RepositorySlug.full(argResults!['repository'] as String), - targets: targets, - androidSdkLocation: argResults!['android-sdk-location'] as String?, - androidNdkVersion: argResults!['android-ndk-version'] as String?, - androidMinSdkVersion: androidMinSdkVersion, - tempDir: argResults!['temp-dir'] as String?, - ); - - await precompileBinaries.run(); - } -} - -class VerifyBinariesCommand extends Command { - VerifyBinariesCommand() { - argParser.addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ); - } - - @override - final name = "verify-binaries"; - - @override - final description = 'Verifies published binaries\n' - 'Checks whether there is a binary published for each targets\n' - 'and checks the signature.'; - - @override - Future run() async { - final manifestDir = argResults!['manifest-dir'] as String; - final verifyBinaries = VerifyBinaries( - manifestDir: manifestDir, - ); - await verifyBinaries.run(); - } -} - -Future runMain(List args) async { - try { - // Init logging before options are loaded - initLogging(); - - if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { - return AndroidEnvironment.clangLinkerWrapper(args); - } - - final runner = CommandRunner('build_tool', 'Cargokit built_tool') - ..addCommand(BuildPodCommand()) - ..addCommand(BuildGradleCommand()) - ..addCommand(BuildCMakeCommand()) - ..addCommand(GenKeyCommand()) - ..addCommand(PrecompileBinariesCommand()) - ..addCommand(VerifyBinariesCommand()); - - await runner.run(args); - } on ArgumentError catch (e) { - stderr.writeln(e.toString()); - exit(1); - } catch (e, s) { - log.severe(kDoubleSeparator); - log.severe('Cargokit BuildTool failed with error:'); - log.severe(kSeparator); - log.severe(e); - // This tells user to install Rust, there's no need to pollute the log with - // stack trace. - if (e is! RustupNotFoundException) { - log.severe(kSeparator); - log.severe(s); - log.severe(kSeparator); - log.severe('BuildTool arguments: $args'); - } - log.severe(kDoubleSeparator); - exit(1); - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/builder.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/builder.dart deleted file mode 100644 index 84c46e4f..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/builder.dart +++ /dev/null @@ -1,198 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'android_environment.dart'; -import 'cargo.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; -import 'util.dart'; - -final _log = Logger('builder'); - -enum BuildConfiguration { - debug, - release, - profile, -} - -extension on BuildConfiguration { - bool get isDebug => this == BuildConfiguration.debug; - String get rustName => switch (this) { - BuildConfiguration.debug => 'debug', - BuildConfiguration.release => 'release', - BuildConfiguration.profile => 'release', - }; -} - -class BuildException implements Exception { - final String message; - - BuildException(this.message); - - @override - String toString() { - return 'BuildException: $message'; - } -} - -class BuildEnvironment { - final BuildConfiguration configuration; - final CargokitCrateOptions crateOptions; - final String targetTempDir; - final String manifestDir; - final CrateInfo crateInfo; - - final bool isAndroid; - final String? androidSdkPath; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? javaHome; - - BuildEnvironment({ - required this.configuration, - required this.crateOptions, - required this.targetTempDir, - required this.manifestDir, - required this.crateInfo, - required this.isAndroid, - this.androidSdkPath, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.javaHome, - }); - - static BuildConfiguration parseBuildConfiguration(String value) { - // XCode configuration adds the flavor to configuration name. - final firstSegment = value.split('-').first; - final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( - (e) => e.name == firstSegment, - ); - if (buildConfiguration == null) { - _log.warning('Unknown build configuraiton $value, will assume release'); - return BuildConfiguration.release; - } - return buildConfiguration; - } - - static BuildEnvironment fromEnvironment({ - required bool isAndroid, - }) { - final buildConfiguration = - parseBuildConfiguration(Environment.configuration); - final manifestDir = Environment.manifestDir; - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - final crateInfo = CrateInfo.load(manifestDir); - return BuildEnvironment( - configuration: buildConfiguration, - crateOptions: crateOptions, - targetTempDir: Environment.targetTempDir, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: isAndroid, - androidSdkPath: isAndroid ? Environment.sdkPath : null, - androidNdkVersion: isAndroid ? Environment.ndkVersion : null, - androidMinSdkVersion: - isAndroid ? int.parse(Environment.minSdkVersion) : null, - javaHome: isAndroid ? Environment.javaHome : null, - ); - } -} - -class RustBuilder { - final Target target; - final BuildEnvironment environment; - - RustBuilder({ - required this.target, - required this.environment, - }); - - void prepare( - Rustup rustup, - ) { - final toolchain = _toolchain; - if (rustup.installedTargets(toolchain) == null) { - rustup.installToolchain(toolchain); - } - if (toolchain == 'nightly') { - rustup.installRustSrcForNightly(); - } - if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { - rustup.installTarget(target.rust, toolchain: toolchain); - } - } - - CargoBuildOptions? get _buildOptions => - environment.crateOptions.cargo[environment.configuration]; - - String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; - - /// Returns the path of directory containing build artifacts. - Future build() async { - final extraArgs = _buildOptions?.flags ?? []; - final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); - runCommand( - 'rustup', - [ - 'run', - _toolchain, - 'cargo', - 'build', - ...extraArgs, - '--manifest-path', - manifestPath, - '-p', - environment.crateInfo.packageName, - if (!environment.configuration.isDebug) '--release', - '--target', - target.rust, - '--target-dir', - environment.targetTempDir, - ], - environment: await _buildEnvironment(), - ); - return path.join( - environment.targetTempDir, - target.rust, - environment.configuration.rustName, - ); - } - - Future> _buildEnvironment() async { - if (target.android == null) { - return {}; - } else { - final sdkPath = environment.androidSdkPath; - final ndkVersion = environment.androidNdkVersion; - final minSdkVersion = environment.androidMinSdkVersion; - if (sdkPath == null) { - throw BuildException('androidSdkPath is not set'); - } - if (ndkVersion == null) { - throw BuildException('androidNdkVersion is not set'); - } - if (minSdkVersion == null) { - throw BuildException('androidMinSdkVersion is not set'); - } - final env = AndroidEnvironment( - sdkPath: sdkPath, - ndkVersion: ndkVersion, - minSdkVersion: minSdkVersion, - targetTempDir: environment.targetTempDir, - target: target, - ); - if (!env.ndkIsInstalled() && environment.javaHome != null) { - env.installNdk(javaHome: environment.javaHome!); - } - return env.buildEnvironment(); - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/cargo.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/cargo.dart deleted file mode 100644 index 0d8958ff..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/cargo.dart +++ /dev/null @@ -1,48 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:path/path.dart' as path; -import 'package:toml/toml.dart'; - -class ManifestException { - ManifestException(this.message, {required this.fileName}); - - final String? fileName; - final String message; - - @override - String toString() { - if (fileName != null) { - return 'Failed to parse package manifest at $fileName: $message'; - } else { - return 'Failed to parse package manifest: $message'; - } - } -} - -class CrateInfo { - CrateInfo({required this.packageName}); - - final String packageName; - - static CrateInfo parseManifest(String manifest, {final String? fileName}) { - final toml = TomlDocument.parse(manifest); - final package = toml.toMap()['package']; - if (package == null) { - throw ManifestException('Missing package section', fileName: fileName); - } - final name = package['name']; - if (name == null) { - throw ManifestException('Missing package name', fileName: fileName); - } - return CrateInfo(packageName: name); - } - - static CrateInfo load(String manifestDir) { - final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); - final manifest = manifestFile.readAsStringSync(); - return parseManifest(manifest, fileName: manifestFile.path); - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart deleted file mode 100644 index 0c4d88d1..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart +++ /dev/null @@ -1,124 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; -import 'package:path/path.dart' as path; - -class CrateHash { - /// Computes a hash uniquely identifying crate content. This takes into account - /// content all all .rs files inside the src directory, as well as Cargo.toml, - /// Cargo.lock, build.rs and cargokit.yaml. - /// - /// If [tempStorage] is provided, computed hash is stored in a file in that directory - /// and reused on subsequent calls if the crate content hasn't changed. - static String compute(String manifestDir, {String? tempStorage}) { - return CrateHash._( - manifestDir: manifestDir, - tempStorage: tempStorage, - )._compute(); - } - - CrateHash._({ - required this.manifestDir, - required this.tempStorage, - }); - - String _compute() { - final files = getFiles(); - final tempStorage = this.tempStorage; - if (tempStorage != null) { - final quickHash = _computeQuickHash(files); - final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); - quickHashFolder.createSync(recursive: true); - final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); - if (quickHashFile.existsSync()) { - return quickHashFile.readAsStringSync(); - } - final hash = _computeHash(files); - quickHashFile.writeAsStringSync(hash); - return hash; - } else { - return _computeHash(files); - } - } - - /// Computes a quick hash based on files stat (without reading contents). This - /// is used to cache the real hash, which is slower to compute since it involves - /// reading every single file. - String _computeQuickHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - final data = ByteData(8); - for (final file in files) { - input.add(utf8.encode(file.path)); - final stat = file.statSync(); - data.setUint64(0, stat.size); - input.add(data.buffer.asUint8List()); - data.setUint64(0, stat.modified.millisecondsSinceEpoch); - input.add(data.buffer.asUint8List()); - } - - input.close(); - return base64Url.encode(output.events.single.bytes); - } - - String _computeHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - void addTextFile(File file) { - // text Files are hashed by lines in case we're dealing with github checkout - // that auto-converts line endings. - final splitter = LineSplitter(); - if (file.existsSync()) { - final data = file.readAsStringSync(); - final lines = splitter.convert(data); - for (final line in lines) { - input.add(utf8.encode(line)); - } - } - } - - for (final file in files) { - addTextFile(file); - } - - input.close(); - final res = output.events.single; - - // Truncate to 128bits. - final hash = res.bytes.sublist(0, 16); - return hex.encode(hash); - } - - List getFiles() { - final src = Directory(path.join(manifestDir, 'src')); - final files = src - .listSync(recursive: true, followLinks: false) - .whereType() - .toList(); - files.sortBy((element) => element.path); - void addFile(String relative) { - final file = File(path.join(manifestDir, relative)); - if (file.existsSync()) { - files.add(file); - } - } - - addFile('Cargo.toml'); - addFile('Cargo.lock'); - addFile('build.rs'); - addFile('cargokit.yaml'); - return files; - } - - final String manifestDir; - final String? tempStorage; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/environment.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/environment.dart deleted file mode 100644 index 996483a1..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/environment.dart +++ /dev/null @@ -1,68 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -extension on String { - String resolveSymlink() => File(this).resolveSymbolicLinksSync(); -} - -class Environment { - /// Current build configuration (debug or release). - static String get configuration => - _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); - - static bool get isDebug => configuration == 'debug'; - static bool get isRelease => configuration == 'release'; - - /// Temporary directory where Rust build artifacts are placed. - static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); - - /// Final output directory where the build artifacts are placed. - static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); - - /// Path to the crate manifest (containing Cargo.toml). - static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); - - /// Directory inside root project. Not necessarily root folder. Symlinks are - /// not resolved on purpose. - static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); - - // Pod - - /// Platform name (macosx, iphoneos, iphonesimulator). - static String get darwinPlatformName => - _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); - - /// List of architectures to build for (arm64, armv7, x86_64). - static List get darwinArchs => - _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); - - // Gradle - static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); - static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); - static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); - static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); - static List get targetPlatforms => - _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); - - // CMAKE - static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); - - static String _getEnv(String key) { - final res = Platform.environment[key]; - if (res == null) { - throw Exception("Missing environment variable $key"); - } - return res; - } - - static String _getEnvPath(String key) { - final res = _getEnv(key); - if (Directory(res).existsSync()) { - return res.resolveSymlink(); - } else { - return res; - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/logging.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/logging.dart deleted file mode 100644 index 5edd4fd1..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/logging.dart +++ /dev/null @@ -1,52 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:logging/logging.dart'; - -const String kSeparator = "--"; -const String kDoubleSeparator = "=="; - -bool _lastMessageWasSeparator = false; - -void _log(LogRecord rec) { - final prefix = '${rec.level.name}: '; - final out = rec.level == Level.SEVERE ? stderr : stdout; - if (rec.message == kSeparator) { - if (!_lastMessageWasSeparator) { - out.write(prefix); - out.writeln('-' * 80); - _lastMessageWasSeparator = true; - } - return; - } else if (rec.message == kDoubleSeparator) { - out.write(prefix); - out.writeln('=' * 80); - _lastMessageWasSeparator = true; - return; - } - out.write(prefix); - out.writeln(rec.message); - _lastMessageWasSeparator = false; -} - -void initLogging() { - Logger.root.level = Level.INFO; - Logger.root.onRecord.listen((LogRecord rec) { - final lines = rec.message.split('\n'); - for (final line in lines) { - if (line.isNotEmpty || lines.length == 1 || line != lines.last) { - _log(LogRecord( - rec.level, - line, - rec.loggerName, - )); - } - } - }); -} - -void enableVerboseLogging() { - Logger.root.level = Level.ALL; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/options.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/options.dart deleted file mode 100644 index 22aef1d3..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/options.dart +++ /dev/null @@ -1,309 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:source_span/source_span.dart'; -import 'package:yaml/yaml.dart'; - -import 'builder.dart'; -import 'environment.dart'; -import 'rustup.dart'; - -final _log = Logger('options'); - -/// A class for exceptions that have source span information attached. -class SourceSpanException implements Exception { - // This is a getter so that subclasses can override it. - /// A message describing the exception. - String get message => _message; - final String _message; - - // This is a getter so that subclasses can override it. - /// The span associated with this exception. - /// - /// This may be `null` if the source location can't be determined. - SourceSpan? get span => _span; - final SourceSpan? _span; - - SourceSpanException(this._message, this._span); - - /// Returns a string representation of `this`. - /// - /// [color] may either be a [String], a [bool], or `null`. If it's a string, - /// it indicates an ANSI terminal color escape that should be used to - /// highlight the span's text. If it's `true`, it indicates that the text - /// should be highlighted using the default color. If it's `false` or `null`, - /// it indicates that the text shouldn't be highlighted. - @override - String toString({Object? color}) { - if (span == null) return message; - return 'Error on ${span!.message(message, color: color)}'; - } -} - -enum Toolchain { - stable, - beta, - nightly, -} - -class CargoBuildOptions { - final Toolchain toolchain; - final List flags; - - CargoBuildOptions({ - required this.toolchain, - required this.flags, - }); - - static Toolchain _toolchainFromNode(YamlNode node) { - if (node case YamlScalar(value: String name)) { - final toolchain = - Toolchain.values.firstWhereOrNull((element) => element.name == name); - if (toolchain != null) { - return toolchain; - } - } - throw SourceSpanException( - 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', - node.span); - } - - static CargoBuildOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - Toolchain toolchain = Toolchain.stable; - List flags = []; - for (final MapEntry(:key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: 'toolchain')) { - toolchain = _toolchainFromNode(value); - } else if (key case YamlScalar(value: 'extra_flags')) { - if (value case YamlList(nodes: List list)) { - if (list.every((element) { - if (element case YamlScalar(value: String _)) { - return true; - } - return false; - })) { - flags = list.map((e) => e.value as String).toList(); - continue; - } - } - throw SourceSpanException( - 'Extra flags must be a list of strings', value.span); - } else { - throw SourceSpanException( - 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', - key.span); - } - } - return CargoBuildOptions(toolchain: toolchain, flags: flags); - } -} - -extension on YamlMap { - /// Map that extracts keys so that we can do map case check on them. - Map get valueMap => - nodes.map((key, value) => MapEntry(key.value, value)); -} - -class PrecompiledBinaries { - final String uriPrefix; - final PublicKey publicKey; - - PrecompiledBinaries({ - required this.uriPrefix, - required this.publicKey, - }); - - static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { - final bytes = HEX.decode(key); - if (bytes.length != 32) { - throw SourceSpanException( - 'Invalid public key. Must be 32 bytes long.', span); - } - return PublicKey(bytes); - } - - static PrecompiledBinaries parse(YamlNode node) { - if (node case YamlMap(valueMap: Map map)) { - if (map - case { - 'url_prefix': YamlNode urlPrefixNode, - 'public_key': YamlNode publicKeyNode, - }) { - final urlPrefix = switch (urlPrefixNode) { - YamlScalar(value: String urlPrefix) => urlPrefix, - _ => throw SourceSpanException( - 'Invalid URL prefix value.', urlPrefixNode.span), - }; - final publicKey = switch (publicKeyNode) { - YamlScalar(value: String publicKey) => - _publicKeyFromHex(publicKey, publicKeyNode.span), - _ => throw SourceSpanException( - 'Invalid public key value.', publicKeyNode.span), - }; - return PrecompiledBinaries( - uriPrefix: urlPrefix, - publicKey: publicKey, - ); - } - } - throw SourceSpanException( - 'Invalid precompiled binaries value. ' - 'Expected Map with "url_prefix" and "public_key".', - node.span); - } -} - -/// Cargokit options specified for Rust crate. -class CargokitCrateOptions { - CargokitCrateOptions({ - this.cargo = const {}, - this.precompiledBinaries, - }); - - final Map cargo; - final PrecompiledBinaries? precompiledBinaries; - - static CargokitCrateOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - final options = {}; - PrecompiledBinaries? precompiledBinaries; - - for (final entry in node.nodes.entries) { - if (entry - case MapEntry( - key: YamlScalar(value: 'cargo'), - value: YamlNode node, - )) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: String name)) { - final configuration = BuildConfiguration.values - .firstWhereOrNull((element) => element.name == name); - if (configuration != null) { - options[configuration] = CargoBuildOptions.parse(value); - continue; - } - } - throw SourceSpanException( - 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', - key.span); - } - } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { - precompiledBinaries = PrecompiledBinaries.parse(entry.value); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', - entry.key.span); - } - } - return CargokitCrateOptions( - cargo: options, - precompiledBinaries: precompiledBinaries, - ); - } - - static CargokitCrateOptions load({ - required String manifestDir, - }) { - final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); - final file = File.fromUri(uri); - if (file.existsSync()) { - final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); - return parse(contents); - } else { - return CargokitCrateOptions(); - } - } -} - -class CargokitUserOptions { - // When Rustup is installed always build locally unless user opts into - // using precompiled binaries. - static bool defaultUsePrecompiledBinaries() { - return Rustup.executablePath() == null; - } - - CargokitUserOptions({ - required this.usePrecompiledBinaries, - required this.verboseLogging, - }); - - CargokitUserOptions._() - : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), - verboseLogging = false; - - static CargokitUserOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); - bool verboseLogging = false; - - for (final entry in node.nodes.entries) { - if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { - if (entry.value case YamlScalar(value: bool value)) { - usePrecompiledBinaries = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "use_precompiled_binaries". Must be a boolean.', - entry.value.span); - } else if (entry.key case YamlScalar(value: 'verbose_logging')) { - if (entry.value case YamlScalar(value: bool value)) { - verboseLogging = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "verbose_logging". Must be a boolean.', - entry.value.span); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', - entry.key.span); - } - } - return CargokitUserOptions( - usePrecompiledBinaries: usePrecompiledBinaries, - verboseLogging: verboseLogging, - ); - } - - static CargokitUserOptions load() { - String fileName = "cargokit_options.yaml"; - var userProjectDir = Directory(Environment.rootProjectDir); - - while (userProjectDir.parent.path != userProjectDir.path) { - final configFile = File(path.join(userProjectDir.path, fileName)); - if (configFile.existsSync()) { - final contents = loadYamlNode( - configFile.readAsStringSync(), - sourceUrl: configFile.uri, - ); - final res = parse(contents); - if (res.verboseLogging) { - _log.info('Found user options file at ${configFile.path}'); - } - return res; - } - userProjectDir = userProjectDir.parent; - } - return CargokitUserOptions._(); - } - - final bool usePrecompiledBinaries; - final bool verboseLogging; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart deleted file mode 100644 index c27f4195..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart +++ /dev/null @@ -1,202 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; - -final _log = Logger('precompile_binaries'); - -class PrecompileBinaries { - PrecompileBinaries({ - required this.privateKey, - required this.githubToken, - required this.repositorySlug, - required this.manifestDir, - required this.targets, - this.androidSdkLocation, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.tempDir, - }); - - final PrivateKey privateKey; - final String githubToken; - final RepositorySlug repositorySlug; - final String manifestDir; - final List targets; - final String? androidSdkLocation; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? tempDir; - - static String fileName(Target target, String name) { - return '${target.rust}_$name'; - } - - static String signatureFileName(Target target, String name) { - return '${target.rust}_$name.sig'; - } - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final targets = List.of(this.targets); - if (targets.isEmpty) { - targets.addAll([ - ...Target.buildableTargets(), - if (androidSdkLocation != null) ...Target.androidTargets(), - ]); - } - - _log.info('Precompiling binaries for $targets'); - - final hash = CrateHash.compute(manifestDir); - _log.info('Computed crate hash: $hash'); - - final String tagName = 'precompiled_$hash'; - - final github = GitHub(auth: Authentication.withToken(githubToken)); - final repo = github.repositories; - final release = await _getOrCreateRelease( - repo: repo, - tagName: tagName, - packageName: crateInfo.packageName, - hash: hash, - ); - - final tempDir = this.tempDir != null - ? Directory(this.tempDir!) - : Directory.systemTemp.createTempSync('precompiled_'); - - tempDir.createSync(recursive: true); - - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - - final buildEnvironment = BuildEnvironment( - configuration: BuildConfiguration.release, - crateOptions: crateOptions, - targetTempDir: tempDir.path, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: androidSdkLocation != null, - androidSdkPath: androidSdkLocation, - androidNdkVersion: androidNdkVersion, - androidMinSdkVersion: androidMinSdkVersion, - ); - - final rustup = Rustup(); - - for (final target in targets) { - final artifactNames = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - if (artifactNames.every((name) { - final fileName = PrecompileBinaries.fileName(target, name); - return (release.assets ?? []).any((e) => e.name == fileName); - })) { - _log.info("All artifacts for $target already exist - skipping"); - continue; - } - - _log.info('Building for $target'); - - final builder = - RustBuilder(target: target, environment: buildEnvironment); - builder.prepare(rustup); - final res = await builder.build(); - - final assets = []; - for (final name in artifactNames) { - final file = File(path.join(res, name)); - if (!file.existsSync()) { - throw Exception('Missing artifact: ${file.path}'); - } - - final data = file.readAsBytesSync(); - final create = CreateReleaseAsset( - name: PrecompileBinaries.fileName(target, name), - contentType: "application/octet-stream", - assetData: data, - ); - final signature = sign(privateKey, data); - final signatureCreate = CreateReleaseAsset( - name: signatureFileName(target, name), - contentType: "application/octet-stream", - assetData: signature, - ); - bool verified = verify(public(privateKey), data, signature); - if (!verified) { - throw Exception('Signature verification failed'); - } - assets.add(create); - assets.add(signatureCreate); - } - _log.info('Uploading assets: ${assets.map((e) => e.name)}'); - for (final asset in assets) { - // This seems to be failing on CI so do it one by one - int retryCount = 0; - while (true) { - try { - await repo.uploadReleaseAssets(release, [asset]); - break; - } on Exception catch (e) { - if (retryCount == 10) { - rethrow; - } - ++retryCount; - _log.shout( - 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); - await Future.delayed(Duration(seconds: 2)); - } - } - } - } - - _log.info('Cleaning up'); - tempDir.deleteSync(recursive: true); - } - - Future _getOrCreateRelease({ - required RepositoriesService repo, - required String tagName, - required String packageName, - required String hash, - }) async { - Release release; - try { - _log.info('Fetching release $tagName'); - release = await repo.getReleaseByTagName(repositorySlug, tagName); - } on ReleaseNotFound { - _log.info('Release not found - creating release $tagName'); - release = await repo.createRelease( - repositorySlug, - CreateRelease.from( - tagName: tagName, - name: 'Precompiled binaries ${hash.substring(0, 8)}', - targetCommitish: null, - isDraft: false, - isPrerelease: false, - body: 'Precompiled binaries for crate $packageName, ' - 'crate hash $hash.', - )); - } - return release; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/rustup.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/rustup.dart deleted file mode 100644 index 0ac8d086..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/rustup.dart +++ /dev/null @@ -1,136 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; - -import 'util.dart'; - -class _Toolchain { - _Toolchain( - this.name, - this.targets, - ); - - final String name; - final List targets; -} - -class Rustup { - List? installedTargets(String toolchain) { - final targets = _installedTargets(toolchain); - return targets != null ? List.unmodifiable(targets) : null; - } - - void installToolchain(String toolchain) { - log.info("Installing Rust toolchain: $toolchain"); - runCommand("rustup", ['toolchain', 'install', toolchain]); - _installedToolchains - .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); - } - - void installTarget( - String target, { - required String toolchain, - }) { - log.info("Installing Rust target: $target"); - runCommand("rustup", [ - 'target', - 'add', - '--toolchain', - toolchain, - target, - ]); - _installedTargets(toolchain)?.add(target); - } - - final List<_Toolchain> _installedToolchains; - - Rustup() : _installedToolchains = _getInstalledToolchains(); - - List? _installedTargets(String toolchain) => _installedToolchains - .firstWhereOrNull( - (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) - ?.targets; - - static List<_Toolchain> _getInstalledToolchains() { - String extractToolchainName(String line) { - // ignore (default) after toolchain name - final parts = line.split(' '); - return parts[0]; - } - - final res = runCommand("rustup", ['toolchain', 'list']); - - // To list all non-custom toolchains, we need to filter out lines that - // don't start with "stable", "beta", or "nightly". - Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) - .map(extractToolchainName) - .toList(growable: true); - - return lines - .map( - (name) => _Toolchain( - name, - _getInstalledTargets(name), - ), - ) - .toList(growable: true); - } - - static List _getInstalledTargets(String toolchain) { - final res = runCommand("rustup", [ - 'target', - 'list', - '--toolchain', - toolchain, - '--installed', - ]); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty) - .toList(growable: true); - return lines; - } - - bool _didInstallRustSrcForNightly = false; - - void installRustSrcForNightly() { - if (_didInstallRustSrcForNightly) { - return; - } - // Useful for -Z build-std - runCommand( - "rustup", - ['component', 'add', 'rust-src', '--toolchain', 'nightly'], - ); - _didInstallRustSrcForNightly = true; - } - - static String? executablePath() { - final envPath = Platform.environment['PATH']; - final envPathSeparator = Platform.isWindows ? ';' : ':'; - final home = Platform.isWindows - ? Platform.environment['USERPROFILE'] - : Platform.environment['HOME']; - final paths = [ - if (home != null) path.join(home, '.cargo', 'bin'), - if (envPath != null) ...envPath.split(envPathSeparator), - ]; - for (final p in paths) { - final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; - final rustupPath = path.join(p, rustup); - if (File(rustupPath).existsSync()) { - return rustupPath; - } - } - return null; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/target.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/target.dart deleted file mode 100644 index 6fbc58b6..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/target.dart +++ /dev/null @@ -1,140 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:collection/collection.dart'; - -import 'util.dart'; - -class Target { - Target({ - required this.rust, - this.flutter, - this.android, - this.androidMinSdkVersion, - this.darwinPlatform, - this.darwinArch, - }); - - static final all = [ - Target( - rust: 'armv7-linux-androideabi', - flutter: 'android-arm', - android: 'armeabi-v7a', - androidMinSdkVersion: 16, - ), - Target( - rust: 'aarch64-linux-android', - flutter: 'android-arm64', - android: 'arm64-v8a', - androidMinSdkVersion: 21, - ), - Target( - rust: 'i686-linux-android', - flutter: 'android-x86', - android: 'x86', - androidMinSdkVersion: 16, - ), - Target( - rust: 'x86_64-linux-android', - flutter: 'android-x64', - android: 'x86_64', - androidMinSdkVersion: 21, - ), - Target( - rust: 'x86_64-pc-windows-msvc', - flutter: 'windows-x64', - ), - Target( - rust: 'x86_64-unknown-linux-gnu', - flutter: 'linux-x64', - ), - Target( - rust: 'aarch64-unknown-linux-gnu', - flutter: 'linux-arm64', - ), - Target( - rust: 'x86_64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'x86_64', - ), - Target( - rust: 'aarch64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios', - darwinPlatform: 'iphoneos', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios-sim', - darwinPlatform: 'iphonesimulator', - darwinArch: 'arm64', - ), - Target( - rust: 'x86_64-apple-ios', - darwinPlatform: 'iphonesimulator', - darwinArch: 'x86_64', - ), - ]; - - static Target? forFlutterName(String flutterName) { - return all.firstWhereOrNull((element) => element.flutter == flutterName); - } - - static Target? forDarwin({ - required String platformName, - required String darwinAarch, - }) { - return all.firstWhereOrNull((element) => // - element.darwinPlatform == platformName && - element.darwinArch == darwinAarch); - } - - static Target? forRustTriple(String triple) { - return all.firstWhereOrNull((element) => element.rust == triple); - } - - static List androidTargets() { - return all - .where((element) => element.android != null) - .toList(growable: false); - } - - /// Returns buildable targets on current host platform ignoring Android targets. - static List buildableTargets() { - if (Platform.isLinux) { - // Right now we don't support cross-compiling on Linux. So we just return - // the host target. - final arch = runCommand('arch', []).stdout as String; - if (arch.trim() == 'aarch64') { - return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; - } else { - return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; - } - } - return all.where((target) { - if (Platform.isWindows) { - return target.rust.contains('-windows-'); - } else if (Platform.isMacOS) { - return target.darwinPlatform != null; - } - return false; - }).toList(growable: false); - } - - @override - String toString() { - return rust; - } - - final String? flutter; - final String rust; - final String? android; - final int? androidMinSdkVersion; - final String? darwinPlatform; - final String? darwinArch; -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/util.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/util.dart deleted file mode 100644 index 8bb6a872..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/util.dart +++ /dev/null @@ -1,172 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'logging.dart'; -import 'rustup.dart'; - -final log = Logger("process"); - -class CommandFailedException implements Exception { - final String executable; - final List arguments; - final ProcessResult result; - - CommandFailedException({ - required this.executable, - required this.arguments, - required this.result, - }); - - @override - String toString() { - final stdout = result.stdout.toString().trim(); - final stderr = result.stderr.toString().trim(); - return [ - "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", - "Returned Exit Code: ${result.exitCode}", - kSeparator, - "STDOUT:", - if (stdout.isNotEmpty) stdout, - kSeparator, - "STDERR:", - if (stderr.isNotEmpty) stderr, - ].join('\n'); - } -} - -class TestRunCommandArgs { - final String executable; - final List arguments; - final String? workingDirectory; - final Map? environment; - final bool includeParentEnvironment; - final bool runInShell; - final Encoding? stdoutEncoding; - final Encoding? stderrEncoding; - - TestRunCommandArgs({ - required this.executable, - required this.arguments, - this.workingDirectory, - this.environment, - this.includeParentEnvironment = true, - this.runInShell = false, - this.stdoutEncoding, - this.stderrEncoding, - }); -} - -class TestRunCommandResult { - TestRunCommandResult({ - this.pid = 1, - this.exitCode = 0, - this.stdout = '', - this.stderr = '', - }); - - final int pid; - final int exitCode; - final String stdout; - final String stderr; -} - -TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; - -ProcessResult runCommand( - String executable, - List arguments, { - String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - Encoding? stdoutEncoding = systemEncoding, - Encoding? stderrEncoding = systemEncoding, -}) { - if (testRunCommandOverride != null) { - final result = testRunCommandOverride!(TestRunCommandArgs( - executable: executable, - arguments: arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding, - )); - return ProcessResult( - result.pid, - result.exitCode, - result.stdout, - result.stderr, - ); - } - log.finer('Running command $executable ${arguments.join(' ')}'); - final res = Process.runSync( - _resolveExecutable(executable), - arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stderrEncoding: stderrEncoding, - stdoutEncoding: stdoutEncoding, - ); - if (res.exitCode != 0) { - throw CommandFailedException( - executable: executable, - arguments: arguments, - result: res, - ); - } else { - return res; - } -} - -class RustupNotFoundException implements Exception { - @override - String toString() { - return [ - ' ', - 'rustup not found in PATH.', - ' ', - 'Maybe you need to install Rust? It only takes a minute:', - ' ', - if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', - if (hasHomebrewRustInPath()) ...[ - '\$ brew unlink rust # Unlink homebrew Rust from PATH', - ], - if (!Platform.isWindows) - "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", - ' ', - ].join('\n'); - } - - static bool hasHomebrewRustInPath() { - if (!Platform.isMacOS) { - return false; - } - final envPath = Platform.environment['PATH'] ?? ''; - final paths = envPath.split(':'); - return paths.any((p) { - return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); - }); - } -} - -String _resolveExecutable(String executable) { - if (executable == 'rustup') { - final resolved = Rustup.executablePath(); - if (resolved != null) { - return resolved; - } - throw RustupNotFoundException(); - } else { - return executable; - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart b/mobile/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart deleted file mode 100644 index 2366b57b..00000000 --- a/mobile/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart +++ /dev/null @@ -1,84 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; - -import 'artifacts_provider.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; - -class VerifyBinaries { - VerifyBinaries({ - required this.manifestDir, - }); - - final String manifestDir; - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final config = CargokitCrateOptions.load(manifestDir: manifestDir); - final precompiledBinaries = config.precompiledBinaries; - if (precompiledBinaries == null) { - stdout.writeln('Crate does not support precompiled binaries.'); - } else { - final crateHash = CrateHash.compute(manifestDir); - stdout.writeln('Crate hash: $crateHash'); - - for (final target in Target.all) { - final message = 'Checking ${target.rust}...'; - stdout.write(message.padRight(40)); - stdout.flush(); - - final artifacts = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - final prefix = precompiledBinaries.uriPrefix; - - bool ok = true; - - for (final artifact in artifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = - Uri.parse('$prefix$crateHash/$signatureFileName'); - - final signature = await get(signatureUrl); - if (signature.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - final asset = await get(url); - if (asset.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - - if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, - signature.bodyBytes)) { - stdout.writeln('INVALID SIGNATURE'); - ok = false; - } - } - - if (ok) { - stdout.writeln('OK'); - } - } - } - } -} diff --git a/mobile/rust_builder/cargokit/build_tool/pubspec.lock b/mobile/rust_builder/cargokit/build_tool/pubspec.lock deleted file mode 100644 index 343bdd36..00000000 --- a/mobile/rust_builder/cargokit/build_tool/pubspec.lock +++ /dev/null @@ -1,453 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 - url: "https://pub.dev" - source: hosted - version: "64.0.0" - adaptive_number: - dependency: transitive - description: - name: adaptive_number - sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - args: - dependency: "direct main" - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - collection: - dependency: "direct main" - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - convert: - dependency: "direct main" - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" - url: "https://pub.dev" - source: hosted - version: "1.6.3" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - ed25519_edwards: - dependency: "direct main" - description: - name: ed25519_edwards - sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - file: - dependency: transitive - description: - name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - github: - dependency: "direct main" - description: - name: github - sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" - url: "https://pub.dev" - source: hosted - version: "9.17.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - hex: - dependency: "direct main" - description: - name: hex - sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - http: - dependency: "direct main" - description: - name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - lints: - dependency: "direct dev" - description: - name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - logging: - dependency: "direct main" - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - meta: - dependency: transitive - description: - name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: "direct main" - description: - name: path - sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" - url: "https://pub.dev" - source: hosted - version: "1.8.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 - url: "https://pub.dev" - source: hosted - version: "5.4.0" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" - source_span: - dependency: "direct main" - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test: - dependency: "direct dev" - description: - name: test - sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" - url: "https://pub.dev" - source: hosted - version: "1.24.6" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - test_core: - dependency: transitive - description: - name: test_core - sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" - url: "https://pub.dev" - source: hosted - version: "0.5.6" - toml: - dependency: "direct main" - description: - name: toml - sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" - url: "https://pub.dev" - source: hosted - version: "0.14.0" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - version: - dependency: "direct main" - description: - name: version - sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" - url: "https://pub.dev" - source: hosted - version: "11.9.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.0.0 <4.0.0" diff --git a/mobile/rust_builder/cargokit/build_tool/pubspec.yaml b/mobile/rust_builder/cargokit/build_tool/pubspec.yaml deleted file mode 100644 index 18c61e33..00000000 --- a/mobile/rust_builder/cargokit/build_tool/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# This is copied from Cargokit (which is the official way to use it currently) -# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -name: build_tool -description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. -publish_to: none -version: 1.0.0 - -environment: - sdk: ">=3.0.0 <4.0.0" - -# Add regular dependencies here. -dependencies: - # these are pinned on purpose because the bundle_tool_runner doesn't have - # pubspec.lock. See run_build_tool.sh - logging: 1.2.0 - path: 1.8.0 - version: 3.0.0 - collection: 1.18.0 - ed25519_edwards: 0.3.1 - hex: 0.2.0 - yaml: 3.1.2 - source_span: 1.10.0 - github: 9.17.0 - args: 2.4.2 - crypto: 3.0.3 - convert: 3.1.1 - http: 1.1.0 - toml: 0.14.0 - -dev_dependencies: - lints: ^2.1.0 - test: ^1.24.0 diff --git a/mobile/rust_builder/cargokit/cmake/cargokit.cmake b/mobile/rust_builder/cargokit/cmake/cargokit.cmake deleted file mode 100644 index ddd05df9..00000000 --- a/mobile/rust_builder/cargokit/cmake/cargokit.cmake +++ /dev/null @@ -1,99 +0,0 @@ -SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) - -if(WIN32) - # REALPATH does not properly resolve symlinks on windows :-/ - execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) -endif() - -# Arguments -# - target: CMAKE target to which rust library is linked -# - manifest_dir: relative path from current folder to directory containing cargo manifest -# - lib_name: cargo package name -# - any_symbol_name: name of any exported symbol from the library. -# used on windows to force linking with library. -function(apply_cargokit target manifest_dir lib_name any_symbol_name) - - set(CARGOKIT_LIB_NAME "${lib_name}") - set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") - if (CMAKE_CONFIGURATION_TYPES) - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") - else() - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") - endif() - set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") - - if (FLUTTER_TARGET_PLATFORM) - set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") - else() - set(CARGOKIT_TARGET_PLATFORM "windows-x64") - endif() - - set(CARGOKIT_ENV - "CARGOKIT_CMAKE=${CMAKE_COMMAND}" - "CARGOKIT_CONFIGURATION=$" - "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" - "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" - "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" - "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" - "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" - "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" - ) - - if (WIN32) - set(SCRIPT_EXTENSION ".cmd") - set(IMPORT_LIB_EXTENSION ".lib") - else() - set(SCRIPT_EXTENSION ".sh") - set(IMPORT_LIB_EXTENSION "") - execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") - endif() - - # Using generators in custom command is only supported in CMake 3.20+ - if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") - foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) - add_custom_command( - OUTPUT - "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endforeach() - else() - add_custom_command( - OUTPUT - ${OUTPUT_LIB} - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endif() - - - set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) - - if (TARGET ${target}) - # If we have actual cmake target provided create target and make existing - # target depend on it - add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) - add_dependencies("${target}" "${target}_cargokit") - target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") - if(WIN32) - target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") - endif() - else() - # Otherwise (FFI) just use ALL to force building always - add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) - endif() - - # Allow adding the output library to plugin bundled libraries - set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) - -endfunction() diff --git a/mobile/rust_builder/cargokit/cmake/resolve_symlinks.ps1 b/mobile/rust_builder/cargokit/cmake/resolve_symlinks.ps1 deleted file mode 100644 index 2ac593a1..00000000 --- a/mobile/rust_builder/cargokit/cmake/resolve_symlinks.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -function Resolve-Symlinks { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [string] $Path - ) - - [string] $separator = '/' - [string[]] $parts = $Path.Split($separator) - - [string] $realPath = '' - foreach ($part in $parts) { - if ($realPath -and !$realPath.EndsWith($separator)) { - $realPath += $separator - } - - $realPath += $part.Replace('\', '/') - - # The slash is important when using Get-Item on Drive letters in pwsh. - if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) { - $realPath += '/' - } - - $item = Get-Item $realPath - if ($item.LinkTarget) { - $realPath = $item.LinkTarget.Replace('\', '/') - } - } - $realPath -} - -$path = Resolve-Symlinks -Path $args[0] -Write-Host $path diff --git a/mobile/rust_builder/cargokit/gradle/plugin.gradle b/mobile/rust_builder/cargokit/gradle/plugin.gradle deleted file mode 100644 index 4af35ee0..00000000 --- a/mobile/rust_builder/cargokit/gradle/plugin.gradle +++ /dev/null @@ -1,179 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import java.nio.file.Paths -import org.apache.tools.ant.taskdefs.condition.Os - -CargoKitPlugin.file = buildscript.sourceFile - -apply plugin: CargoKitPlugin - -class CargoKitExtension { - String manifestDir; // Relative path to folder containing Cargo.toml - String libname; // Library name within Cargo.toml. Must be a cdylib -} - -abstract class CargoKitBuildTask extends DefaultTask { - - @Input - String buildMode - - @Input - String buildDir - - @Input - String outputDir - - @Input - String ndkVersion - - @Input - String sdkDirectory - - @Input - int compileSdkVersion; - - @Input - int minSdkVersion; - - @Input - String pluginFile - - @Input - List targetPlatforms - - @TaskAction - def build() { - if (project.cargokit.manifestDir == null) { - throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); - } - - if (project.cargokit.libname == null) { - throw new GradleException("Property 'libname' must be set on cargokit extension"); - } - - def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" - def path = Paths.get(new File(pluginFile).parent, "..", executableName); - - def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) - - def rootProjectDir = project.rootProject.projectDir - - if (!Os.isFamily(Os.FAMILY_WINDOWS)) { - project.exec { - commandLine 'chmod', '+x', path - } - } - - project.exec { - executable path - args "build-gradle" - environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir - environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" - environment "CARGOKIT_MANIFEST_DIR", manifestDir - environment "CARGOKIT_CONFIGURATION", buildMode - environment "CARGOKIT_TARGET_TEMP_DIR", buildDir - environment "CARGOKIT_OUTPUT_DIR", outputDir - environment "CARGOKIT_NDK_VERSION", ndkVersion - environment "CARGOKIT_SDK_DIR", sdkDirectory - environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion - environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion - environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") - environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] - } - } -} - -class CargoKitPlugin implements Plugin { - - static String file; - - private Plugin findFlutterPlugin(Project rootProject) { - _findFlutterPlugin(rootProject.childProjects) - } - - private Plugin _findFlutterPlugin(Map projects) { - for (project in projects) { - for (plugin in project.value.getPlugins()) { - if (plugin.class.name == "com.flutter.gradle.FlutterPlugin") { - return plugin; - } - } - def plugin = _findFlutterPlugin(project.value.childProjects); - if (plugin != null) { - return plugin; - } - } - return null; - } - - @Override - void apply(Project project) { - def plugin = findFlutterPlugin(project.rootProject); - - project.extensions.create("cargokit", CargoKitExtension) - - if (plugin == null) { - print("Flutter plugin not found, CargoKit plugin will not be applied.") - return; - } - - def cargoBuildDir = "${project.buildDir}/build" - - // Determine if the project is an application or library - def isApplication = plugin.project.plugins.hasPlugin('com.android.application') - def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants - - variants.all { variant -> - - final buildType = variant.buildType.name - - def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; - def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; - jniLibs.srcDir(new File(cargoOutputDir)) - - def platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect() - - // Same thing addFlutterDependencies does in flutter.gradle - if (buildType == "debug") { - platforms.add("android-x86") - platforms.add("android-x64") - } - - // The task name depends on plugin properties, which are not available - // at this point - project.getGradle().afterProject { - def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; - - if (project.tasks.findByName(taskName)) { - return - } - - if (plugin.project.android.ndkVersion == null) { - throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") - } - - def task = project.tasks.create(taskName, CargoKitBuildTask.class) { - buildMode = variant.buildType.name - buildDir = cargoBuildDir - outputDir = cargoOutputDir - ndkVersion = plugin.project.android.ndkVersion - sdkDirectory = plugin.project.android.sdkDirectory - minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int - compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int - targetPlatforms = platforms - pluginFile = CargoKitPlugin.file - } - def onTask = { newTask -> - if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { - newTask.dependsOn task - // Fix gradle 7.4.2 not picking up JNI library changes - newTask.outputs.upToDateWhen { false } - } - } - project.tasks.each onTask - project.tasks.whenTaskAdded onTask - } - } - } -} diff --git a/mobile/rust_builder/cargokit/run_build_tool.cmd b/mobile/rust_builder/cargokit/run_build_tool.cmd deleted file mode 100755 index c45d0aa8..00000000 --- a/mobile/rust_builder/cargokit/run_build_tool.cmd +++ /dev/null @@ -1,91 +0,0 @@ -@echo off -setlocal - -setlocal ENABLEDELAYEDEXPANSION - -SET BASEDIR=%~dp0 - -if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( - mkdir "%CARGOKIT_TOOL_TEMP_DIR%" -) -cd /D "%CARGOKIT_TOOL_TEMP_DIR%" - -SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool -SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart - -set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% - -( - echo name: build_tool_runner - echo version: 1.0.0 - echo publish_to: none - echo. - echo environment: - echo sdk: '^>=3.0.0 ^<4.0.0' - echo. - echo dependencies: - echo build_tool: - echo path: %BUILD_TOOL_PKG_DIR_POSIX% -) >pubspec.yaml - -if not exist bin ( - mkdir bin -) - -( - echo import 'package:build_tool/build_tool.dart' as build_tool; - echo void main^(List^ args^) ^{ - echo build_tool.runMain^(args^); - echo ^} -) >bin\build_tool_runner.dart - -SET PRECOMPILED=bin\build_tool_runner.dill - -REM To detect changes in package we compare output of DIR /s (recursive) -set PREV_PACKAGE_INFO=.dart_tool\package_info.prev -set CUR_PACKAGE_INFO=.dart_tool\package_info.cur - -DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" - -REM Last line in dir output is free space on harddrive. That is bound to -REM change between invocation so we need to remove it -( - Set "Line=" - For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( - SetLocal EnableDelayedExpansion - If Defined Line Echo !Line! - EndLocal - Set "Line=%%A") -) >"%CUR_PACKAGE_INFO%" -DEL "%CUR_PACKAGE_INFO%_orig" - -REM Compare current directory listing with previous -FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 - -If %ERRORLEVEL% neq 0 ( - REM Changed - copy current to previous and remove precompiled kernel - if exist "%PREV_PACKAGE_INFO%" ( - DEL "%PREV_PACKAGE_INFO%" - ) - MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" - if exist "%PRECOMPILED%" ( - DEL "%PRECOMPILED%" - ) -) - -REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% -REM which means we need to do pub get and precompile -if not exist "%PRECOMPILED%" ( - echo Running pub get in "%cd%" - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart -) - -"%DART%" "%PRECOMPILED%" %* - -REM 253 means invalid snapshot version. -If %ERRORLEVEL% equ 253 ( - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart - "%DART%" "%PRECOMPILED%" %* -) diff --git a/mobile/rust_builder/cargokit/run_build_tool.sh b/mobile/rust_builder/cargokit/run_build_tool.sh deleted file mode 100755 index 069c1585..00000000 --- a/mobile/rust_builder/cargokit/run_build_tool.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Ensure Cargo/Rust toolchain is in PATH -if [ -d "$HOME/.cargo/bin" ]; then - export PATH="$HOME/.cargo/bin:$PATH" -fi - -BASEDIR=$(dirname "$0") - -mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" - -cd "$CARGOKIT_TOOL_TEMP_DIR" - -# Write a very simple bin package in temp folder that depends on build_tool package -# from Cargokit. This is done to ensure that we don't pollute Cargokit folder -# with .dart_tool contents. - -BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" - -if [[ -z $FLUTTER_ROOT ]]; then # not defined - DART=dart -else - DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" -fi - -cat << EOF > "pubspec.yaml" -name: build_tool_runner -version: 1.0.0 -publish_to: none - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - build_tool: - path: "$BUILD_TOOL_PKG_DIR" -EOF - -mkdir -p "bin" - -cat << EOF > "bin/build_tool_runner.dart" -import 'package:build_tool/build_tool.dart' as build_tool; -void main(List args) { - build_tool.runMain(args); -} -EOF - -# Create alias for `shasum` if it does not exist and `sha1sum` exists -if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then - shopt -s expand_aliases - alias shasum="sha1sum" -fi - -# Dart run will not cache any package that has a path dependency, which -# is the case for our build_tool_runner. So instead we precompile the package -# ourselves. -# To invalidate the cached kernel we use the hash of ls -LR of the build_tool -# package directory. This should be good enough, as the build_tool package -# itself is not meant to have any path dependencies. - -if [[ "$OSTYPE" == "darwin"* ]]; then - PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) -else - PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) -fi - -PACKAGE_HASH_FILE=".package_hash" - -if [ -f "$PACKAGE_HASH_FILE" ]; then - EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") - if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then - rm "$PACKAGE_HASH_FILE" - fi -fi - -# Run pub get if needed. -if [ ! -f "$PACKAGE_HASH_FILE" ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" -fi - -# Rebuild the tool if it was deleted by Android Studio -if [ ! -f "bin/build_tool_runner.dill" ]; then - "$DART" compile kernel bin/build_tool_runner.dart -fi - -set +e - -"$DART" bin/build_tool_runner.dill "$@" - -exit_code=$? - -# 253 means invalid snapshot version. -if [ $exit_code == 253 ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - "$DART" bin/build_tool_runner.dill "$@" - exit_code=$? -fi - -exit $exit_code diff --git a/mobile/rust_builder/ios/Classes/dummy_file.c b/mobile/rust_builder/ios/Classes/dummy_file.c deleted file mode 100644 index e06dab99..00000000 --- a/mobile/rust_builder/ios/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/mobile/rust_builder/ios/rust_lib_mobile.podspec b/mobile/rust_builder/ios/rust_lib_mobile.podspec deleted file mode 100644 index e241d00c..00000000 --- a/mobile/rust_builder/ios/rust_lib_mobile.podspec +++ /dev/null @@ -1,45 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint rust_lib_mobile.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'rust_lib_mobile' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '11.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' - - s.script_phase = { - :name => 'Build Rust library', - # First argument is relative path to the `rust` folder, second is name of rust library - :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../native okena_mobile_native', - :execution_position => :before_compile, - :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], - # Let XCode know that the static library referenced in -force_load below is - # created by this build step. - :output_files => ["${BUILT_PRODUCTS_DIR}/libokena_mobile_native.a"], - } - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - # Flutter.framework does not contain a i386 slice. - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libokena_mobile_native.a', - } -end \ No newline at end of file diff --git a/mobile/rust_builder/linux/CMakeLists.txt b/mobile/rust_builder/linux/CMakeLists.txt deleted file mode 100644 index 7ca21fd5..00000000 --- a/mobile/rust_builder/linux/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# The Flutter tooling requires that developers have CMake 3.10 or later -# installed. You should not increase this version, as doing so will cause -# the plugin to fail to compile for some customers of the plugin. -cmake_minimum_required(VERSION 3.10) - -# Project-level configuration. -set(PROJECT_NAME "rust_lib_mobile") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} ../../native okena_mobile_native "") - -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(rust_lib_mobile_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/mobile/rust_builder/macos/Classes/dummy_file.c b/mobile/rust_builder/macos/Classes/dummy_file.c deleted file mode 100644 index e06dab99..00000000 --- a/mobile/rust_builder/macos/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/mobile/rust_builder/macos/rust_lib_mobile.podspec b/mobile/rust_builder/macos/rust_lib_mobile.podspec deleted file mode 100644 index 24df209a..00000000 --- a/mobile/rust_builder/macos/rust_lib_mobile.podspec +++ /dev/null @@ -1,44 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint rust_lib_mobile.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'rust_lib_mobile' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.swift_version = '5.0' - - s.script_phase = { - :name => 'Build Rust library', - # First argument is relative path to the `rust` folder, second is name of rust library - :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../native okena_mobile_native', - :execution_position => :before_compile, - :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], - # Let XCode know that the static library referenced in -force_load below is - # created by this build step. - :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_mobile.a"], - } - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - # Flutter.framework does not contain a i386 slice. - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_mobile.a', - } -end \ No newline at end of file diff --git a/mobile/rust_builder/pubspec.yaml b/mobile/rust_builder/pubspec.yaml deleted file mode 100644 index f8efc186..00000000 --- a/mobile/rust_builder/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: rust_lib_mobile -description: "Utility to build Rust code" -version: 0.0.1 -publish_to: none - -environment: - sdk: '>=3.3.0 <4.0.0' - flutter: '>=3.3.0' - -dependencies: - flutter: - sdk: flutter - plugin_platform_interface: ^2.0.2 - -dev_dependencies: - ffi: ^2.0.2 - ffigen: ^11.0.0 - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - ffiPlugin: true - ios: - ffiPlugin: true - linux: - ffiPlugin: true - macos: - ffiPlugin: true - windows: - ffiPlugin: true diff --git a/mobile/rust_builder/windows/.gitignore b/mobile/rust_builder/windows/.gitignore deleted file mode 100644 index b3eb2be1..00000000 --- a/mobile/rust_builder/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/mobile/rust_builder/windows/CMakeLists.txt b/mobile/rust_builder/windows/CMakeLists.txt deleted file mode 100644 index a3e26953..00000000 --- a/mobile/rust_builder/windows/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# The Flutter tooling requires that developers have a version of Visual Studio -# installed that includes CMake 3.14 or later. You should not increase this -# version, as doing so will cause the plugin to fail to compile for some -# customers of the plugin. -cmake_minimum_required(VERSION 3.14) - -# Project-level configuration. -set(PROJECT_NAME "rust_lib_mobile") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} ../../../../../../native okena_mobile_native "") - -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(rust_lib_mobile_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/mobile/test/models/layout_node_test.dart b/mobile/test/models/layout_node_test.dart deleted file mode 100644 index 71953ea6..00000000 --- a/mobile/test/models/layout_node_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/src/models/layout_node.dart'; - -void main() { - group('LayoutNode.fromJson', () { - test('parses terminal node', () { - final node = LayoutNode.fromJson(jsonEncode({ - 'type': 'terminal', - 'terminal_id': 't1', - 'minimized': false, - 'detached': false, - })); - - expect(node, isA()); - final t = node as TerminalNode; - expect(t.terminalId, 't1'); - expect(t.minimized, isFalse); - expect(t.detached, isFalse); - }); - - test('parses terminal node with null id', () { - final node = LayoutNode.fromJson(jsonEncode({ - 'type': 'terminal', - 'terminal_id': null, - 'minimized': true, - 'detached': true, - })); - - expect(node, isA()); - expect((node as TerminalNode).terminalId, isNull); - }); - - test('parses split node', () { - final node = LayoutNode.fromJson(jsonEncode({ - 'type': 'split', - 'direction': 'horizontal', - 'sizes': [50.0, 50.0], - 'children': [ - {'type': 'terminal', 'terminal_id': 't1', 'minimized': false, 'detached': false}, - {'type': 'terminal', 'terminal_id': 't2', 'minimized': false, 'detached': false}, - ], - })); - - expect(node, isA()); - final s = node as SplitNode; - expect(s.direction, SplitDirection.horizontal); - expect(s.sizes, [50.0, 50.0]); - expect(s.children.length, 2); - expect((s.children[0] as TerminalNode).terminalId, 't1'); - expect((s.children[1] as TerminalNode).terminalId, 't2'); - }); - - test('parses tabs node', () { - final node = LayoutNode.fromJson(jsonEncode({ - 'type': 'tabs', - 'active_tab': 1, - 'children': [ - {'type': 'terminal', 'terminal_id': 'a', 'minimized': false, 'detached': false}, - {'type': 'terminal', 'terminal_id': 'b', 'minimized': false, 'detached': false}, - ], - })); - - expect(node, isA()); - final t = node as TabsNode; - expect(t.activeTab, 1); - expect(t.children.length, 2); - }); - - test('parses nested split with tabs', () { - final node = LayoutNode.fromJson(jsonEncode({ - 'type': 'split', - 'direction': 'vertical', - 'sizes': [30.0, 70.0], - 'children': [ - {'type': 'terminal', 'terminal_id': 't1', 'minimized': false, 'detached': false}, - { - 'type': 'tabs', - 'active_tab': 0, - 'children': [ - {'type': 'terminal', 'terminal_id': 't2', 'minimized': false, 'detached': false}, - {'type': 'terminal', 'terminal_id': 't3', 'minimized': false, 'detached': false}, - ], - }, - ], - })); - - expect(node, isA()); - final s = node as SplitNode; - expect(s.children[0], isA()); - expect(s.children[1], isA()); - final tabs = s.children[1] as TabsNode; - expect(tabs.children.length, 2); - }); - - test('unknown type returns null', () { - final node = LayoutNode.fromJson(jsonEncode({ - 'type': 'unknown_future_type', - })); - - expect(node, isNull); - }); - - test('handles missing optional fields with defaults', () { - final node = LayoutNode.fromJson(jsonEncode({ - 'type': 'split', - })); - - expect(node, isA()); - final s = node as SplitNode; - expect(s.direction, SplitDirection.horizontal); - expect(s.sizes, isEmpty); - expect(s.children, isEmpty); - }); - }); -} diff --git a/mobile/test/models/saved_server_test.dart b/mobile/test/models/saved_server_test.dart deleted file mode 100644 index 0f33c4b4..00000000 --- a/mobile/test/models/saved_server_test.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/src/models/saved_server.dart'; - -void main() { - group('SavedServer', () { - test('toJson/fromJson round-trip without label', () { - const server = SavedServer(host: '192.168.1.100', port: 19100); - final json = server.toJson(); - final restored = SavedServer.fromJson(json); - - expect(restored.host, '192.168.1.100'); - expect(restored.port, 19100); - expect(restored.label, isNull); - }); - - test('toJson/fromJson round-trip with label', () { - const server = - SavedServer(host: '10.0.0.1', port: 19200, label: 'Home PC'); - final json = server.toJson(); - final restored = SavedServer.fromJson(json); - - expect(restored.host, '10.0.0.1'); - expect(restored.port, 19200); - expect(restored.label, 'Home PC'); - }); - - test('label omitted from JSON when null', () { - const server = SavedServer(host: 'host', port: 1234); - final json = server.toJson(); - expect(json.containsKey('label'), isFalse); - }); - - test('listToJson/listFromJson round-trip', () { - const servers = [ - SavedServer(host: 'a.com', port: 100), - SavedServer(host: 'b.com', port: 200, label: 'B'), - ]; - final jsonStr = SavedServer.listToJson(servers); - final restored = SavedServer.listFromJson(jsonStr); - - expect(restored.length, 2); - expect(restored[0].host, 'a.com'); - expect(restored[1].label, 'B'); - }); - - test('displayName uses label when present', () { - const server = SavedServer(host: 'h', port: 1, label: 'My Server'); - expect(server.displayName, 'My Server'); - }); - - test('displayName falls back to host:port', () { - const server = SavedServer(host: '10.0.0.1', port: 19100); - expect(server.displayName, '10.0.0.1:19100'); - }); - - test('equality by host and port', () { - const a = SavedServer(host: 'x', port: 1); - const b = SavedServer(host: 'x', port: 1, label: 'different'); - const c = SavedServer(host: 'y', port: 1); - - expect(a, equals(b)); - expect(a, isNot(equals(c))); - }); - }); -} diff --git a/mobile/test/models/terminal_flags_test.dart b/mobile/test/models/terminal_flags_test.dart deleted file mode 100644 index c84feaa4..00000000 --- a/mobile/test/models/terminal_flags_test.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/painting.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mobile/src/widgets/terminal_painter.dart'; - -void main() { - group('flagsToDecoration', () { - test('no flags returns none', () { - expect(flagsToDecoration(0), TextDecoration.none); - }); - - test('underline flag', () { - expect(flagsToDecoration(4), TextDecoration.underline); - }); - - test('strikethrough flag', () { - expect(flagsToDecoration(8), TextDecoration.lineThrough); - }); - - test('underline + strikethrough combined', () { - final deco = flagsToDecoration(4 | 8); - // TextDecoration.combine returns a combined decoration - expect(deco.contains(TextDecoration.underline), isTrue); - expect(deco.contains(TextDecoration.lineThrough), isTrue); - }); - - test('bold/italic flags do not add decoration', () { - expect(flagsToDecoration(1), TextDecoration.none); // bold - expect(flagsToDecoration(2), TextDecoration.none); // italic - expect(flagsToDecoration(3), TextDecoration.none); // bold+italic - }); - }); - - group('argbToColor', () { - test('opaque white', () { - final c = argbToColor(0xFFFFFFFF); - expect(c, const Color(0xFFFFFFFF)); - }); - - test('opaque red', () { - final c = argbToColor(0xFFFF0000); - expect(c.r, closeTo(1.0, 0.01)); - expect(c.g, closeTo(0.0, 0.01)); - expect(c.b, closeTo(0.0, 0.01)); - }); - - test('semi-transparent', () { - final c = argbToColor(0x80000000); - expect(c.a, closeTo(0.5, 0.01)); - }); - }); -} diff --git a/mobile/test/widget_test.dart b/mobile/test/widget_test.dart deleted file mode 100644 index a6b7d51e..00000000 --- a/mobile/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:mobile/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/mobile/test_driver/integration_test.dart b/mobile/test_driver/integration_test.dart deleted file mode 100644 index b38629cc..00000000 --- a/mobile/test_driver/integration_test.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); From ab081d94646e574024e9f68017d4696ce9effd09 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 10 Jun 2026 13:51:06 +0200 Subject: [PATCH 34/37] fix(ci): make clippy --all-targets green on the branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing breakage surfaced by `cargo clippy --workspace --all-targets` (CI's "Check compilation" runs `cargo check --release`, which skips tests, so these test-only failures slipped through): - remote_apply.rs / client/state.rs test helpers: add the `cols`/`rows` fields added in ac0a91bd, plus the missing ApiProject fields and the `is_visible` → `show_in_overview` rename. - text_utils.rs: silence newer clippy lints (is_some_and, drop unwrap()). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/okena-core/src/client/state.rs | 6 +++++- crates/okena-ui/src/text_utils.rs | 14 +++++++------- crates/okena-workspace/src/remote_apply.rs | 2 ++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/okena-core/src/client/state.rs b/crates/okena-core/src/client/state.rs index a84b67c0..fa184239 100644 --- a/crates/okena-core/src/client/state.rs +++ b/crates/okena-core/src/client/state.rs @@ -251,7 +251,7 @@ mod tests { id: "p1".into(), name: "p1".into(), path: "/tmp".into(), - is_visible: true, + show_in_overview: true, layout: Some(ApiLayoutNode::Split { direction: SplitDirection::Horizontal, sizes: vec![50.0, 50.0], @@ -273,7 +273,11 @@ mod tests { ], }), terminal_names: Default::default(), + git_status: None, folder_color: FolderColor::default(), + services: Vec::new(), + worktree_info: None, + worktree_ids: Vec::new(), }]); let sizes = collect_terminal_sizes(&state); assert_eq!(sizes.get("t1"), Some(&(120, 40))); diff --git a/crates/okena-ui/src/text_utils.rs b/crates/okena-ui/src/text_utils.rs index aaa10b39..db1ec8a5 100644 --- a/crates/okena-ui/src/text_utils.rs +++ b/crates/okena-ui/src/text_utils.rs @@ -29,7 +29,7 @@ pub fn find_word_boundaries(text: &str, byte_col: usize) -> (usize, usize) { // Get the char at `col` (if col == text.len(), there is no char) let cur_char = text[col..].chars().next(); - let on_word = cur_char.map_or(false, |c| is_word_char(c)); + let on_word = cur_char.is_some_and(is_word_char); // Scan backwards for start (byte offset) let mut start = col; @@ -40,11 +40,9 @@ pub fn find_word_boundaries(text: &str, byte_col: usize) -> (usize, usize) { while prev > 0 && !text.is_char_boundary(prev) { prev -= 1; } - let prev_char = text[prev..].chars().next().unwrap(); - if is_word_char(prev_char) { - start = prev; - } else { - break; + match text[prev..].chars().next() { + Some(prev_char) if is_word_char(prev_char) => start = prev, + _ => break, } } } @@ -52,7 +50,9 @@ pub fn find_word_boundaries(text: &str, byte_col: usize) -> (usize, usize) { // Scan forwards for end (byte offset) let mut end = col; while end < text.len() { - let next_char = text[end..].chars().next().unwrap(); + let Some(next_char) = text[end..].chars().next() else { + break; + }; if is_word_char(next_char) { end += next_char.len_utf8(); } else { diff --git a/crates/okena-workspace/src/remote_apply.rs b/crates/okena-workspace/src/remote_apply.rs index aa2b6c37..5216204d 100644 --- a/crates/okena-workspace/src/remote_apply.rs +++ b/crates/okena-workspace/src/remote_apply.rs @@ -398,6 +398,8 @@ mod tests { terminal_id: Some(id.to_string()), minimized: false, detached: false, + cols: None, + rows: None, } } From 7946657225adc3313363d6b1e8f2d79ac2ea804c Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 10 Jun 2026 19:56:41 +0200 Subject: [PATCH 35/37] fix(remote-client): use rustls `ring` provider instead of `aws-lc-rs` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `aws-lc-rs`'s jitter-entropy init (`jent_entropy_init`) segfaults on Android when rustls builds a TLS client connection, crashing the mobile app on connect (SIGSEGV in libokena-mobile-ffi during the TLS handshake setup). Switch the rustls crypto provider to `ring`, which is portable on every target (incl. the Android NDK) and is already what reqwest 0.12's `rustls-tls` feature pulls in for hyper/tokio-rustls — so a single provider is shared across reqwest + tungstenite. Applied to both the shared client (okena-core) and the desktop remote server (src/remote/tls.rs) so the whole workspace uses one provider; this also drops aws-lc-rs/aws-lc-sys from the build entirely. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 39 ----------------------------- Cargo.toml | 4 +-- crates/okena-core/Cargo.toml | 9 ++++--- crates/okena-core/src/client/tls.rs | 6 ++--- src/remote/tls.rs | 6 ++--- 5 files changed, 14 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c756c74..d64b6714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -615,28 +615,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.8.8" @@ -1107,15 +1085,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "cocoa" version = "0.25.0" @@ -2266,12 +2235,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -7327,7 +7290,6 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring", @@ -7365,7 +7327,6 @@ version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", diff --git a/Cargo.toml b/Cargo.toml index 9d37baf9..e830ed73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,9 +98,9 @@ semver = "1" # Remote server TLS (self-signed cert + dual-stack http/https on one port) rcgen = "0.13" -rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "logging", "tls12"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "tls12"] } rustls-pki-types = "1" -tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] } hyper = { version = "1", features = ["server", "http1", "http2"] } hyper-util = { version = "0.1", features = ["server-auto", "tokio"] } tower-service = "0.3" diff --git a/crates/okena-core/Cargo.toml b/crates/okena-core/Cargo.toml index b67f7859..ee2fcc8e 100644 --- a/crates/okena-core/Cargo.toml +++ b/crates/okena-core/Cargo.toml @@ -25,9 +25,12 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls" tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"], optional = true } async-channel = { version = "2.3", optional = true } futures = { version = "0.3", optional = true } -# Client TLS with certificate pinning (TOFU). aws_lc_rs matches reqwest's default -# rustls backend so a single CryptoProvider is shared across reqwest + tungstenite. -rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "tls12", "logging", "std"], optional = true } +# Client TLS with certificate pinning (TOFU). Uses the `ring` crypto backend: +# it's the portable provider that works on every target (notably Android/NDK, +# where `aws_lc_rs`'s jitter-entropy init segfaults), and reqwest 0.12's +# `rustls-tls` feature already pulls `ring` for hyper/tokio-rustls, so a single +# CryptoProvider is shared across reqwest + tungstenite. +rustls = { version = "0.23", default-features = false, features = ["ring", "tls12", "logging", "std"], optional = true } rustls-pki-types = { version = "1", optional = true } sha2 = { version = "0.10", optional = true } diff --git a/crates/okena-core/src/client/tls.rs b/crates/okena-core/src/client/tls.rs index fc49997c..909353aa 100644 --- a/crates/okena-core/src/client/tls.rs +++ b/crates/okena-core/src/client/tls.rs @@ -153,7 +153,7 @@ impl ServerCertVerifier for PinnedCertVerifier { } fn provider() -> Arc { - Arc::new(rustls::crypto::aws_lc_rs::default_provider()) + Arc::new(rustls::crypto::ring::default_provider()) } /// Build a rustls `ClientConfig` that pins the server cert via [`PinnedCertVerifier`]. @@ -162,11 +162,11 @@ fn pinned_client_config(pinned: Option, observed: ObservedFingerprint) - let verifier = Arc::new(PinnedCertVerifier::new(pinned, observed, provider.clone())); #[allow( clippy::expect_used, - reason = "aws_lc_rs default provider always supports the default protocol versions" + reason = "ring default provider always supports the default protocol versions" )] let builder = rustls::ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .expect("aws_lc_rs default provider supports default protocol versions"); + .expect("ring default provider supports default protocol versions"); builder .dangerous() .with_custom_certificate_verifier(verifier) diff --git a/src/remote/tls.rs b/src/remote/tls.rs index fa840a0b..59110064 100644 --- a/src/remote/tls.rs +++ b/src/remote/tls.rs @@ -103,7 +103,7 @@ fn atomic_write(path: &Path, bytes: &[u8], _mode: u32) -> std::io::Result<()> { } /// Build a rustls `ServerConfig` from the persisted self-signed cert + key, -/// using the aws_lc_rs provider (matches the client side). +/// using the ring provider (matches the client side). pub fn server_config(material: &TlsMaterial) -> Result> { let certs: Vec> = CertificateDer::pem_file_iter(&material.cert_path) @@ -113,7 +113,7 @@ pub fn server_config(material: &TlsMaterial) -> Result let key = PrivateKeyDer::from_pem_file(&material.key_path) .with_context(|| format!("reading private key {:?}", material.key_path))?; - let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider()); + let provider = Arc::new(rustls::crypto::ring::default_provider()); let config = rustls::ServerConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() .context("rustls default protocol versions")? @@ -212,7 +212,7 @@ mod handshake_tests { #[tokio::test] async fn dual_stack_serves_http_and_pinned_https_on_one_port() { // Server providers may consult the process-default CryptoProvider. - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let _ = rustls::crypto::ring::default_provider().install_default(); let dir = tempfile::tempdir().unwrap(); let material = load_or_generate(dir.path()).unwrap(); From 8289713ee9decb7497f7a62497a5cf72ecebab57 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 10 Jun 2026 19:56:53 +0200 Subject: [PATCH 36/37] fix(mobile-rn): ubrn enum/record adapters, skia setEmbolden, @ubjs/core alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconcile the hand-written `OkenaNative` contract with the shapes ubrn 0.31 actually emits, plus two build/render fixes surfaced by running the app: - okena.ts: translate at the `getOkenaNative` boundary — ConnectionStatus (PascalCase `.tag` → lowercase `.kind`, Error payload from `.inner.message`), CursorShape (numeric enum → string union), and ProjectInfo.terminalNames (JS `Map` → plain object). Without these the status pill and terminal cursor crash on render (`Cannot read property 'color' of undefined`). - TerminalView.tsx: drop the synthetic `setEmbolden` calls. react-native-skia 1.12.4's native binding rejects the (typed `boolean`) arg with "Value is false, expected a number"; all four JetBrainsMono variants are bundled so the fallback is unused, and italic is still synthesized via `setSkewX`. - metro.config.js: alias `@ubjs/core` (ubrn's renamed TS runtime, imported by the generated bindings) to the runtime already shipped inside uniffi-bindgen-react-native, avoiding a second, version-skewed copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/rn/metro.config.js | 23 +++++++- mobile/rn/src/components/TerminalView.tsx | 8 ++- mobile/rn/src/native/okena.ts | 70 ++++++++++++++++++++++- 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/mobile/rn/metro.config.js b/mobile/rn/metro.config.js index ba95bf4a..386d3753 100644 --- a/mobile/rn/metro.config.js +++ b/mobile/rn/metro.config.js @@ -1,4 +1,5 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const path = require('path'); /** * Metro configuration @@ -6,6 +7,26 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); * * @type {import('@react-native/metro-config').MetroConfig} */ -const config = {}; + +// ubrn (uniffi-bindgen-react-native) 0.31 generates bindings that import the +// TypeScript runtime as `@ubjs/core` (its new published identity). That runtime +// is the *same bytes* already shipped inside `uniffi-bindgen-react-native` +// (typescript/dist), so we alias `@ubjs/core` to it rather than installing a +// second, potentially version-skewed copy. See node_modules/uniffi-bindgen-react-native/README.md. +const ubrnRuntime = path.resolve( + __dirname, + 'node_modules/uniffi-bindgen-react-native/typescript/dist/cjs/index.js', +); + +const config = { + resolver: { + resolveRequest: (context, moduleName, platform) => { + if (moduleName === '@ubjs/core') { + return {type: 'sourceFile', filePath: ubrnRuntime}; + } + return context.resolveRequest(context, moduleName, platform); + }, + }, +}; module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/mobile/rn/src/components/TerminalView.tsx b/mobile/rn/src/components/TerminalView.tsx index 79a34c1c..37e2b725 100644 --- a/mobile/rn/src/components/TerminalView.tsx +++ b/mobile/rn/src/components/TerminalView.tsx @@ -192,13 +192,17 @@ function fontFor(fonts: TerminalFonts, bold: boolean, italic: boolean): SkFont { // Variant missing → synthesize on regular. (Mutates a shared font; acceptable // here because painting is single-threaded and we reset below.) const f = fonts.regular; - f.setEmbolden(bold); + // NOTE: synthetic *bold* via `setEmbolden` is intentionally omitted. All four + // JetBrainsMono variants are bundled, so this fallback only runs if a variant + // fails to load — and `setEmbolden` is broken in react-native-skia 1.12.4: its + // native binding rejects the (typed `boolean`) arg with "Value is false, + // expected a number". Italic is still synthesized via `setSkewX` (typed + // `number`, works fine). f.setSkewX(italic ? -0.25 : 0); return f; } function resetSynthetic(font: SkFont) { - font.setEmbolden(false); font.setSkewX(0); } diff --git a/mobile/rn/src/native/okena.ts b/mobile/rn/src/native/okena.ts index 5f33736c..72c19768 100644 --- a/mobile/rn/src/native/okena.ts +++ b/mobile/rn/src/native/okena.ts @@ -437,10 +437,76 @@ export interface OkenaNative { * Until the module is generated, the `require` throws and we rethrow with a * pointer to the generation step (see mobile/rn/README.md). */ +/** + * ubrn → app-contract adapters. + * + * ubrn 0.31 represents uniffi types in a few shapes that differ from the + * hand-written contract above, so the boundary (`getOkenaNative`) translates: + * - Data-carrying enums → tagged class instances with a PascalCase `.tag` + * (`{ tag: 'Connected' }`; payload under `.inner`, e.g. Error → + * `.inner.message`). App wants `{ kind: 'lowercase' }`. → `ConnectionStatus`. + * - Fieldless enums → a numeric TS enum (`CursorShape.Block === 0`). App wants + * the lowercase string. → `getCursor().shape`. + * - Map-typed record fields → a JS `Map`. App indexes them as plain objects. + * → `ProjectInfo.terminalNames`. + * Plain-string enums (split direction, diff mode) and all other record fields + * already match (camelCase), so they pass through untouched. + */ +function toConnectionStatus(s: {tag?: string; inner?: {message?: string}}): ConnectionStatus { + switch (s?.tag) { + case 'Disconnected': + return {kind: 'disconnected'}; + case 'Connecting': + return {kind: 'connecting'}; + case 'Connected': + return {kind: 'connected'}; + case 'Pairing': + return {kind: 'pairing'}; + case 'Error': + return {kind: 'error', message: s.inner?.message ?? 'Unknown error'}; + default: + return {kind: 'disconnected'}; + } +} + +// Generated `CursorShape` is a numeric enum (Block=0, Underline=1, Beam=2); +// index by it to recover the app's string union. +const CURSOR_SHAPES: CursorShape[] = ['block', 'underline', 'beam']; + +function toCursorState(c: {col: number; row: number; shape: number; visible: boolean}): CursorState { + return {col: c.col, row: c.row, visible: c.visible, shape: CURSOR_SHAPES[c.shape] ?? 'block'}; +} + +// Generated records carry `terminalNames` as a JS `Map`; the app reads it as a +// plain `Record`. Convert and pass everything else through. +function toProjectInfo(p: ProjectInfo & {terminalNames: unknown}): ProjectInfo { + const names = p.terminalNames; + return { + ...p, + terminalNames: + names instanceof Map ? Object.fromEntries(names) : (names as Record), + }; +} + export function getOkenaNative(): OkenaNative { try { - const gen = require('../generated'); - return (gen.default ?? gen) as OkenaNative; + // The local `okena-mobile-ffi` package entry (src/index.tsx) installs the + // JSI bindings (`installRustCrate()` + `initialize()`) on first import and + // re-exports every generated function at the top level. Most functions + // satisfy `OkenaNative` structurally as-is; the wrapper below only overrides + // the ones whose enum shapes need translating (see adapters above). + const gen = require('okena-mobile-ffi'); + return { + ...gen, + connectionStatus: (connId: ConnId) => + toConnectionStatus(gen.connectionStatus(connId)), + getCursor: (connId: ConnId, terminalId: TerminalId) => + toCursorState(gen.getCursor(connId, terminalId)), + getProjects: (connId: ConnId) => + (gen.getProjects(connId) as Array).map( + toProjectInfo, + ), + } as OkenaNative; } catch (e) { throw new Error( 'okena native module not generated yet — run `npm run ubrn:android` or ' + From 4431efd3a86dcc5d648a807c9993475ac4624c25 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Thu, 11 Jun 2026 11:07:17 +0200 Subject: [PATCH 37/37] chore(mobile-rn): commit okena-mobile-ffi package manifest + document Android integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ubrn turbo module is kept as a local package (`modules/okena-mobile-ffi/`, a `file:` dependency of the app) rather than letting ubrn clobber the app's root `android/build.gradle`. Track only its hand-authored `package.json` (so the `file:` dep resolves on a fresh checkout / in CI — `npm ci` no longer fails); everything else under it is ubrn-generated and stays gitignored (incl. the multi-hundred-MB Rust static lib — must never be committed). README: add a "Verified Android run" section documenting what the end-to-end run actually required — the local-package layout, the committed fixes (rustls `ring`, ubrn enum/record adapters, dropped Skia `setEmbolden`, `@ubjs/core` Metro alias), the post-generation fixups (Node≥20 CMakeLists `require.resolve`, missing `AndroidManifestNew.xml`), `okena pair` for the pairing code, and the emulator GL-present crash that makes a physical device necessary for rendering. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/rn/.gitignore | 6 ++ mobile/rn/README.md | 59 ++++++++++++++++++- .../rn/modules/okena-mobile-ffi/package.json | 26 ++++++++ mobile/rn/package-lock.json | 12 ++++ mobile/rn/package.json | 1 + 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 mobile/rn/modules/okena-mobile-ffi/package.json diff --git a/mobile/rn/.gitignore b/mobile/rn/.gitignore index 541dc6e6..6f00f421 100644 --- a/mobile/rn/.gitignore +++ b/mobile/rn/.gitignore @@ -9,3 +9,9 @@ ios/ src/generated/ cpp/generated/ rust_modules/ + +# Local ubrn turbo-module package. Only the hand-authored package.json is +# tracked; everything else (TS/cpp/Kotlin bindings, gradle lib, the Rust +# static lib in jniLibs) is generated by `npm run ubrn:android|ios` — see README. +modules/okena-mobile-ffi/* +!modules/okena-mobile-ffi/package.json diff --git a/mobile/rn/README.md b/mobile/rn/README.md index edcb5ad9..c5d3184f 100644 --- a/mobile/rn/README.md +++ b/mobile/rn/README.md @@ -35,14 +35,69 @@ npm run lint # eslint npm test # jest (packed-cell decoder smoke test) ``` -**Not** verified here (needs the mobile toolchain + a device/emulator): the ubrn cross-compile, -the Skia native binaries, and an on-device run. Those are the steps below. +The ubrn cross-compile, the Skia native binaries, and an on-device run need the mobile +toolchain — see the device steps below, and the **verified Android run** notes next for the +gotchas that the generic steps don't mention. > Package manager: **npm** (the lockfile is `package-lock.json`). RN 0.76 native autolinking, > CocoaPods, and ubrn are validated against npm/yarn — don't swap in a different manager here. --- +## Verified Android run — what it actually took (2026-06) + +The full chain has been run end-to-end on an Android **emulator**: build → TLS connect → pair → +workspace → live terminal (Skia paint of colored shell output). The generic steps below are +correct in spirit but several things needed fixing/wiring that aren't obvious — captured here. + +**The turbo module is a local package.** ubrn treats a project as a *library* and would clobber +the app's root `android/build.gradle`, so the generated module is kept as its own package at +`modules/okena-mobile-ffi/` and the app depends on it (`"okena-mobile-ffi": "file:./modules/okena-mobile-ffi"`, +autolinked + a Metro symlink). Only that package's hand-authored `package.json` (with +`codegenConfig`) is committed; **everything else under it is generated by ubrn and gitignored** +(incl. the multi-hundred-MB Rust static lib in `jniLibs` — never commit it). `getOkenaNative()` +in `src/native/okena.ts` `require`s `'okena-mobile-ffi'` (its entry installs the JSI bindings). + +**Fixes that are committed** (needed to build + run at all): + +- **TLS provider → `ring`.** `okena-core` (and the desktop server) now build rustls with the + `ring` crypto provider, not `aws-lc-rs` — `aws-lc-rs`'s jitter-entropy init **segfaults on + Android** during the TLS handshake. (Supersedes the "rustls-tls, no OpenSSL" note below.) +- **ubrn enum/record adapters** (`src/native/okena.ts`). ubrn 0.31 emits enums in shapes the + hand-written contract didn't match: `ConnectionStatus` as a PascalCase-`.tag` class (payload + under `.inner`), `CursorShape` as a numeric enum, `ProjectInfo.terminalNames` as a JS `Map`. + `getOkenaNative()` translates these at the boundary to the app's `{ kind }` / string / object + contract. Plain-string enums (split direction, diff mode) and other records pass through. +- **Skia `setEmbolden` dropped** (`src/components/TerminalView.tsx`). react-native-skia 1.12.4's + native binding rejects the (typed `boolean`) arg with *"Value is false, expected a number"*; + all four JetBrainsMono variants are bundled so synthetic bold is unused anyway. +- **`@ubjs/core` Metro alias** (`metro.config.js`). ubrn 0.31's generated bindings import the + TS runtime as `@ubjs/core`; we alias it to the runtime shipped inside + `uniffi-bindgen-react-native` (same bytes, guaranteed version match) instead of a 2nd install. + +**Post-generation fixups** — apply after `npm run ubrn:android` (ubrn regenerates these, so they +are *not* committed; a small post-gen step should automate them): + +- `modules/okena-mobile-ffi/android/CMakeLists.txt` resolves the runtime via + `node -p "require.resolve('uniffi-bindgen-react-native/package.json')"`, which **throws on + Node ≥ 20** (`ERR_PACKAGE_PATH_NOT_EXPORTED` — the package's `exports` doesn't expose + `./package.json`). Replace it with `node -e "…require.resolve('uniffi-bindgen-react-native')…"` + sliced back to the package root. +- The generated lib `build.gradle` (AGP 8 path) references `src/main/AndroidManifestNew.xml`, + but ubrn doesn't create it — add a minimal namespace-less ``. + +**Pairing:** get the 6-char code from the desktop host with `okena pair` (CLI, prints +`XXXX-XXXX`, 60s TTL) and enter it as-is (with the dash). From the emulator the host is `10.0.2.2`. + +**Emulator caveat (rendering):** `react-native-skia`'s GL present crashes **intermittently** +inside the Android emulator's EGL driver (`libEGL_emulation.so` → `createNativeSync` / +`eglSwapBuffers`, from `RNSkia::OpenGLWindowContext::present`). It is **not an app bug** and does +not happen on physical devices; the `-gpu host|swiftshader_indirect|angle_indirect` flags don't +help (the guest EGL is fixed by the system image). **Use a real Android device** for stable +terminal rendering. + +--- + ## Device-side setup (run on a machine with the RN toolchain) Prereqs: Node ≥ 18, Watchman, JDK 17, Android SDK + NDK + `cargo-ndk` (Android), Xcode + diff --git a/mobile/rn/modules/okena-mobile-ffi/package.json b/mobile/rn/modules/okena-mobile-ffi/package.json new file mode 100644 index 00000000..2bed4871 --- /dev/null +++ b/mobile/rn/modules/okena-mobile-ffi/package.json @@ -0,0 +1,26 @@ +{ + "name": "okena-mobile-ffi", + "version": "0.0.0", + "private": true, + "description": "ubrn-generated React Native turbo module for crates/okena-mobile-ffi (local, gitignored — regenerated by `npm run ubrn:android|ios`).", + "main": "src/index.tsx", + "react-native": "src/index.tsx", + "source": "src/index.tsx", + "files": [ + "src", + "android", + "cpp" + ], + "codegenConfig": { + "name": "OkenaMobileFfi", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "com.okenamobilern" + } + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } +} diff --git a/mobile/rn/package-lock.json b/mobile/rn/package-lock.json index 50aca277..1c293c05 100644 --- a/mobile/rn/package-lock.json +++ b/mobile/rn/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@react-native-async-storage/async-storage": "^2.1.0", "@shopify/react-native-skia": "^1.5.0", + "okena-mobile-ffi": "file:./modules/okena-mobile-ffi", "react": "18.3.1", "react-native": "0.76.5", "zustand": "^4.5.5" @@ -39,6 +40,13 @@ "node": ">=18" } }, + "modules/okena-mobile-ffi": { + "version": "0.0.0", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -9998,6 +10006,10 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/okena-mobile-ffi": { + "resolved": "modules/okena-mobile-ffi", + "link": true + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/mobile/rn/package.json b/mobile/rn/package.json index 3de0e3f4..9e355a34 100644 --- a/mobile/rn/package.json +++ b/mobile/rn/package.json @@ -23,6 +23,7 @@ "dependencies": { "@react-native-async-storage/async-storage": "^2.1.0", "@shopify/react-native-skia": "^1.5.0", + "okena-mobile-ffi": "file:./modules/okena-mobile-ffi", "react": "18.3.1", "react-native": "0.76.5", "zustand": "^4.5.5"