diff --git a/crates/okena-core/src/api.rs b/crates/okena-core/src/api.rs index 7d92f350..7c0c7aae 100644 --- a/crates/okena-core/src/api.rs +++ b/crates/okena-core/src/api.rs @@ -88,6 +88,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, @@ -334,6 +338,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -341,6 +347,8 @@ mod tests { terminal_id: Some("t2".into()), minimized: true, detached: true, + cols: None, + rows: None, }], }, ], @@ -552,6 +560,8 @@ mod tests { terminal_id: Some("t1".into()), minimized: false, detached: false, + cols: None, + rows: None, }, ApiLayoutNode::Tabs { active_tab: 0, @@ -560,16 +570,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 5d651433..162dcdf7 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]); @@ -606,9 +609,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 @@ -824,16 +829,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 349a2cbc..f37880eb 100644 --- a/crates/okena-core/src/client/mod.rs +++ b/crates/okena-core/src/client/mod.rs @@ -7,5 +7,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 67a59453..a4086a17 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(), }) @@ -200,4 +242,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-core/src/keys.rs b/crates/okena-core/src/keys.rs index a9023841..19f3d4fd 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, @@ -17,8 +19,6 @@ pub enum SpecialKey { End, PageUp, PageDown, - Backspace, - Delete, } impl SpecialKey { @@ -27,6 +27,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", @@ -39,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~", } } } @@ -54,6 +54,8 @@ mod tests { let keys = vec![ SpecialKey::Enter, SpecialKey::Escape, + SpecialKey::Backspace, + SpecialKey::Delete, SpecialKey::CtrlC, SpecialKey::CtrlD, SpecialKey::CtrlZ, @@ -66,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(); @@ -80,6 +80,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"); @@ -92,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~"); } } diff --git a/crates/okena-remote-client/src/connection.rs b/crates/okena-remote-client/src/connection.rs index d9899663..a7dfd2bd 100644 --- a/crates/okena-remote-client/src/connection.rs +++ b/crates/okena-remote-client/src/connection.rs @@ -30,6 +30,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 @@ -43,9 +45,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-terminal/src/terminal.rs b/crates/okena-terminal/src/terminal.rs index 2c54dac9..33811f39 100644 --- a/crates/okena-terminal/src/terminal.rs +++ b/crates/okena-terminal/src/terminal.rs @@ -428,6 +428,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(); @@ -437,7 +442,7 @@ impl Terminal { // Resize terminal grid immediately (independent mutex) let mut term = self.term.lock(); - let term_size = TermSize::new(new_size.cols as usize, new_size.rows as usize); + let term_size = TermSize::new(cols as usize, rows as usize); term.resize(term_size); drop(term); diff --git a/crates/okena-workspace/src/state.rs b/crates/okena-workspace/src/state.rs index 7dd3470f..839cfb45 100644 --- a/crates/okena-workspace/src/state.rs +++ b/crates/okena-workspace/src/state.rs @@ -1599,6 +1599,7 @@ impl LayoutNode { terminal_id, minimized, detached, + .. } => LayoutNode::Terminal { terminal_id: terminal_id.clone(), minimized: *minimized, @@ -1633,6 +1634,7 @@ impl LayoutNode { terminal_id, minimized, detached, + .. } => LayoutNode::Terminal { terminal_id: terminal_id.as_ref().map(|id| format!("{}:{}", prefix, id)), minimized: *minimized, @@ -1667,31 +1669,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, - }, + } => { + 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/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/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 - 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/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/lib/src/providers/workspace_provider.dart b/mobile/lib/src/providers/workspace_provider.dart index aa51d962..99b9f235 100644 --- a/mobile/lib/src/providers/workspace_provider.dart +++ b/mobile/lib/src/providers/workspace_provider.dart @@ -9,6 +9,9 @@ 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; @@ -16,6 +19,9 @@ class WorkspaceProvider extends ChangeNotifier { 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; @@ -46,12 +52,23 @@ 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(); } else { _stopPolling(); _projects = []; + _folders = []; + _projectOrder = []; + _fullscreenTerminal = null; _selectedProjectId = null; _selectedTerminalId = null; notifyListeners(); @@ -79,6 +96,9 @@ class WorkspaceProvider extends ChangeNotifier { 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; @@ -87,6 +107,22 @@ class WorkspaceProvider extends ChangeNotifier { 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; @@ -138,6 +174,19 @@ class WorkspaceProvider extends ChangeNotifier { 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; } diff --git a/mobile/lib/src/rust/api/state.dart b/mobile/lib/src/rust/api/state.dart index ac4924d9..2c55dec2 100644 --- a/mobile/lib/src/rust/api/state.dart +++ b/mobile/lib/src/rust/api/state.dart @@ -7,7 +7,7 @@ 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 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,6 +17,27 @@ List getProjects({required String connId}) => 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 @@ -40,7 +61,7 @@ Future sendSpecialKey({ List getAllTerminalIds({required String connId}) => RustLib.instance.api.crateApiStateGetAllTerminalIds(connId: connId); -/// Create a new terminal in the given project via POST /v1/actions. +/// Create a new terminal in the given project. Future createTerminal({ required String connId, required String projectId, @@ -49,7 +70,7 @@ Future createTerminal({ projectId: projectId, ); -/// Close a terminal in the given project via POST /v1/actions. +/// Close a terminal in the given project. Future closeTerminal({ required String connId, required String projectId, @@ -60,6 +81,387 @@ Future closeTerminal({ 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; @@ -68,6 +470,11 @@ class ProjectInfo { 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, @@ -76,6 +483,11 @@ class ProjectInfo { required this.showInOverview, required this.terminalIds, required this.terminalNames, + this.gitBranch, + required this.gitLinesAdded, + required this.gitLinesRemoved, + required this.services, + required this.folderColor, }); @override @@ -85,7 +497,12 @@ class ProjectInfo { path.hashCode ^ showInOverview.hashCode ^ terminalIds.hashCode ^ - terminalNames.hashCode; + terminalNames.hashCode ^ + gitBranch.hashCode ^ + gitLinesAdded.hashCode ^ + gitLinesRemoved.hashCode ^ + services.hashCode ^ + folderColor.hashCode; @override bool operator ==(Object other) => @@ -97,5 +514,54 @@ class ProjectInfo { path == other.path && showInOverview == other.showInOverview && terminalIds == other.terminalIds && - terminalNames == other.terminalNames; + 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 index 38e37931..54fb41e2 100644 --- a/mobile/lib/src/rust/api/terminal.dart +++ b/mobile/lib/src/rust/api/terminal.dart @@ -130,6 +130,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. diff --git a/mobile/lib/src/rust/frb_generated.dart b/mobile/lib/src/rust/frb_generated.dart index bf15aa8d..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 => -1973712882; + int get rustContentHash => 632182563; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -79,6 +79,19 @@ class RustLib extends BaseEntrypoint { } 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, @@ -90,6 +103,12 @@ abstract class RustLibApi extends BaseApi { required String terminalId, }); + Future crateApiStateCloseTerminals({ + required String connId, + required String projectId, + required List terminalIds, + }); + String crateApiConnectionConnect({ required String host, required int port, @@ -105,6 +124,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({ @@ -114,6 +139,17 @@ abstract class RustLibApi extends BaseApi { 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({ @@ -138,6 +174,34 @@ abstract class RustLibApi extends BaseApi { 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({ @@ -145,11 +209,68 @@ 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, }); + 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, @@ -157,6 +278,18 @@ abstract class RustLibApi extends BaseApi { 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, @@ -177,6 +310,43 @@ 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, + 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, @@ -184,6 +354,12 @@ abstract class RustLibApi extends BaseApi { required int row, }); + Future crateApiStateStartService({ + required String connId, + required String projectId, + required String serviceName, + }); + void crateApiTerminalStartWordSelection({ required String connId, required String terminalId, @@ -191,12 +367,36 @@ abstract class RustLibApi extends BaseApi { 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 { @@ -207,6 +407,80 @@ 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 + 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, @@ -218,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: 1)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -253,7 +527,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 2, + funcId: 4, port: port_, ); }, @@ -273,6 +547,43 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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, @@ -286,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: 3)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -313,7 +624,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: 7)!; }, codec: SseCodec( decodeSuccessData: sse_decode_connection_status, @@ -343,7 +654,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 5, + funcId: 8, port: port_, ); }, @@ -371,7 +682,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: 9)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -387,6 +698,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: 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( @@ -394,7 +741,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: 11)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_String, @@ -424,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: 8)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; }, codec: SseCodec( decodeSuccessData: sse_decode_cursor_state, @@ -449,7 +796,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(connId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -468,6 +815,108 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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( @@ -475,7 +924,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: 18)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_project_info, @@ -502,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: 11)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; }, codec: SseCodec( decodeSuccessData: sse_decode_scroll_info, @@ -532,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: 12)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -562,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: 13)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_box_autoadd_selection_bounds, @@ -588,7 +1037,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: 22)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -615,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: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_cell_data, @@ -634,6 +1083,184 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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( @@ -643,7 +1270,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 29, port: port_, ); }, @@ -651,60 +1278,849 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: null, ), - constMeta: kCrateApiConnectionInitAppConstMeta, - argValues: [], + 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 kCrateApiConnectionInitAppConstMeta => - const TaskConstMeta(debugName: "init_app", argNames: []); + TaskConstMeta get kCrateApiStateSetProjectColorConstMeta => + const TaskConstMeta( + debugName: "set_project_color", + argNames: ["connId", "projectId", "color"], + ); @override - bool crateApiStateIsDirty({ + Future crateApiStateSplitTerminal({ required String connId, - required String terminalId, + required String projectId, + required Uint64List path, + required String direction, }) { - 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: 17)!; + 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_bool, - decodeErrorData: null, + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiStateIsDirtyConstMeta, - argValues: [connId, terminalId], + constMeta: kCrateApiStateSplitTerminalConstMeta, + argValues: [connId, projectId, path, direction], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiStateIsDirtyConstMeta => const TaskConstMeta( - debugName: "is_dirty", - argNames: ["connId", "terminalId"], + 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: 18, + funcId: 52, port: port_, ); }, @@ -712,22 +2128,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 crateApiTerminalResizeTerminal({ + void crateApiTerminalStartSelection({ required String connId, required String terminalId, - required int cols, - required int rows, + required int col, + required int row, }) { return handler.executeSync( SyncTask( @@ -735,101 +2154,112 @@ 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: 19)!; + 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: kCrateApiTerminalResizeTerminalConstMeta, - argValues: [connId, terminalId, cols, rows], + constMeta: kCrateApiTerminalStartSelectionConstMeta, + argValues: [connId, terminalId, col, row], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalResizeTerminalConstMeta => + TaskConstMeta get kCrateApiTerminalStartSelectionConstMeta => const TaskConstMeta( - debugName: "resize_terminal", - argNames: ["connId", "terminalId", "cols", "rows"], + debugName: "start_selection", + argNames: ["connId", "terminalId", "col", "row"], ); @override - void crateApiTerminalScroll({ + Future crateApiStateStartService({ required String connId, - required String terminalId, - required int delta, + required String projectId, + required String serviceName, }) { - 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_String(projectId, serializer); + sse_encode_String(serviceName, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 54, + port: port_, + ); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, - decodeErrorData: null, + decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiTerminalScrollConstMeta, - argValues: [connId, terminalId, delta], + constMeta: kCrateApiStateStartServiceConstMeta, + argValues: [connId, projectId, serviceName], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalScrollConstMeta => const TaskConstMeta( - debugName: "scroll", - argNames: ["connId", "terminalId", "delta"], + TaskConstMeta get kCrateApiStateStartServiceConstMeta => const TaskConstMeta( + debugName: "start_service", + argNames: ["connId", "projectId", "serviceName"], ); @override - double crateApiConnectionSecondsSinceActivity({required String connId}) { + 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); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + 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_f_64, + decodeSuccessData: sse_decode_unit, decodeErrorData: null, ), - constMeta: kCrateApiConnectionSecondsSinceActivityConstMeta, - argValues: [connId], + constMeta: kCrateApiTerminalStartWordSelectionConstMeta, + argValues: [connId, terminalId, col, row], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiConnectionSecondsSinceActivityConstMeta => + TaskConstMeta get kCrateApiTerminalStartWordSelectionConstMeta => const TaskConstMeta( - debugName: "seconds_since_activity", - argNames: ["connId"], + 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: 22, + funcId: 56, port: port_, ); }, @@ -837,36 +2267,36 @@ 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 terminalId, - required String text, + 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_String(text, serializer); + sse_encode_String(projectId, serializer); + sse_encode_String(serviceName, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 57, port: port_, ); }, @@ -874,54 +2304,57 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiTerminalSendTextConstMeta, - argValues: [connId, terminalId, text], + constMeta: kCrateApiStateStopServiceConstMeta, + argValues: [connId, projectId, serviceName], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalSendTextConstMeta => const TaskConstMeta( - debugName: "send_text", - argNames: ["connId", "terminalId", "text"], + TaskConstMeta get kCrateApiStateStopServiceConstMeta => const TaskConstMeta( + debugName: "stop_service", + argNames: ["connId", "projectId", "serviceName"], ); @override - void crateApiTerminalStartSelection({ + Future crateApiStateToggleMinimized({ required String connId, + required String projectId, required String terminalId, - required int col, - required int row, }) { - return handler.executeSync( - SyncTask( - callFfi: () { + 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_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 58, + port: port_, + ); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, - decodeErrorData: null, + decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiTerminalStartSelectionConstMeta, - argValues: [connId, terminalId, col, row], + constMeta: kCrateApiStateToggleMinimizedConstMeta, + argValues: [connId, projectId, terminalId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalStartSelectionConstMeta => + TaskConstMeta get kCrateApiStateToggleMinimizedConstMeta => const TaskConstMeta( - debugName: "start_selection", - argNames: ["connId", "terminalId", "col", "row"], + debugName: "toggle_minimized", + argNames: ["connId", "projectId", "terminalId"], ); @override - void crateApiTerminalStartWordSelection({ + void crateApiTerminalUpdateSelection({ required String connId, required String terminalId, required int col, @@ -935,57 +2368,62 @@ 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: 25)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 59)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, decodeErrorData: null, ), - constMeta: kCrateApiTerminalStartWordSelectionConstMeta, + constMeta: kCrateApiTerminalUpdateSelectionConstMeta, argValues: [connId, terminalId, col, row], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalStartWordSelectionConstMeta => + TaskConstMeta get kCrateApiTerminalUpdateSelectionConstMeta => const TaskConstMeta( - debugName: "start_word_selection", + debugName: "update_selection", argNames: ["connId", "terminalId", "col", "row"], ); @override - void crateApiTerminalUpdateSelection({ + Future crateApiStateUpdateSplitSizes({ required String connId, - required String terminalId, - required int col, - required int row, + required String projectId, + required Uint64List path, + required List sizes, }) { - 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_u_16(col, serializer); - sse_encode_u_16(row, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; + 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: null, + decodeErrorData: sse_decode_AnyhowException, ), - constMeta: kCrateApiTerminalUpdateSelectionConstMeta, - argValues: [connId, terminalId, col, row], + constMeta: kCrateApiStateUpdateSplitSizesConstMeta, + argValues: [connId, projectId, path, sizes], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiTerminalUpdateSelectionConstMeta => + TaskConstMeta get kCrateApiStateUpdateSplitSizesConstMeta => const TaskConstMeta( - debugName: "update_selection", - argNames: ["connId", "terminalId", "col", "row"], + debugName: "update_split_sizes", + argNames: ["connId", "projectId", "path", "sizes"], ); @protected @@ -1016,12 +2454,30 @@ 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 + 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 @@ -1075,12 +2531,44 @@ 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 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 @@ -1090,13 +2578,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @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(); + 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 - List dco_decode_list_cell_data(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 List).map(dco_decode_cell_data).toList(); + return raw as Uint16List; } @protected @@ -1105,6 +2617,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 @@ -1117,24 +2635,48 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 != 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]), @@ -1142,6 +2684,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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]), ); } @@ -1182,6 +2729,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @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 @@ -1206,6 +2770,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 @@ -1235,6 +2805,14 @@ 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, @@ -1243,6 +2821,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 @@ -1302,12 +2892,41 @@ 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 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 @@ -1338,6 +2957,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 @@ -1345,6 +2997,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 @@ -1371,6 +3030,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 @@ -1382,6 +3053,19 @@ 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, @@ -1395,6 +3079,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @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 @@ -1404,6 +3110,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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, @@ -1411,6 +3122,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { showInOverview: var_showInOverview, terminalIds: var_terminalIds, terminalNames: var_terminalNames, + gitBranch: var_gitBranch, + gitLinesAdded: var_gitLinesAdded, + gitLinesRemoved: var_gitLinesRemoved, + services: var_services, + folderColor: var_folderColor, ); } @@ -1452,6 +3168,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @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 @@ -1475,6 +3212,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, @@ -1508,6 +3251,15 @@ 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, @@ -1517,6 +3269,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 @@ -1562,12 +3326,37 @@ 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 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 @@ -1595,6 +3384,50 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @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, @@ -1605,6 +3438,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, @@ -1629,6 +3472,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @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 @@ -1639,6 +3494,19 @@ 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, @@ -1652,6 +3520,29 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @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 @@ -1661,6 +3552,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 @@ -1693,6 +3589,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 @@ -1715,4 +3623,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 f6b9363a..ac9322e6 100644 --- a/mobile/lib/src/rust/frb_generated.io.dart +++ b/mobile/lib/src/rust/frb_generated.io.dart @@ -32,9 +32,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -47,9 +56,18 @@ 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); + @protected + FolderInfo dco_decode_folder_info(dynamic raw); + + @protected + FullscreenInfo dco_decode_fullscreen_info(dynamic raw); + @protected int dco_decode_i_32(dynamic raw); @@ -59,21 +77,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -86,6 +131,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SelectionBounds dco_decode_selection_bounds(dynamic raw); + @protected + ServiceInfo dco_decode_service_info(dynamic raw); + @protected int dco_decode_u_16(dynamic raw); @@ -98,6 +146,9 @@ 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); @@ -112,11 +163,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -129,9 +191,18 @@ 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); + @protected + FolderInfo sse_decode_folder_info(SseDeserializer deserializer); + + @protected + FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer); + @protected int sse_decode_i_32(SseDeserializer deserializer); @@ -141,9 +212,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -152,14 +238,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { 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); @@ -174,6 +274,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); + @protected + ServiceInfo sse_decode_service_info(SseDeserializer deserializer); + @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -186,6 +289,9 @@ 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, @@ -204,12 +310,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -225,9 +343,21 @@ 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); + @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); @@ -237,12 +367,42 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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, @@ -255,15 +415,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { 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); @@ -282,6 +460,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_service_info(ServiceInfo self, SseSerializer serializer); + @protected void sse_encode_u_16(int self, SseSerializer serializer); @@ -293,6 +474,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 b0f66d8d..8fd5550d 100644 --- a/mobile/lib/src/rust/frb_generated.web.dart +++ b/mobile/lib/src/rust/frb_generated.web.dart @@ -34,9 +34,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -49,9 +58,18 @@ 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); + @protected + FolderInfo dco_decode_folder_info(dynamic raw); + + @protected + FullscreenInfo dco_decode_fullscreen_info(dynamic raw); + @protected int dco_decode_i_32(dynamic raw); @@ -61,21 +79,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -88,6 +133,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SelectionBounds dco_decode_selection_bounds(dynamic raw); + @protected + ServiceInfo dco_decode_service_info(dynamic raw); + @protected int dco_decode_u_16(dynamic raw); @@ -100,6 +148,9 @@ 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); @@ -114,11 +165,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -131,9 +193,18 @@ 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); + @protected + FolderInfo sse_decode_folder_info(SseDeserializer deserializer); + + @protected + FullscreenInfo sse_decode_fullscreen_info(SseDeserializer deserializer); + @protected int sse_decode_i_32(SseDeserializer deserializer); @@ -143,9 +214,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -154,14 +240,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { 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); @@ -176,6 +276,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SelectionBounds sse_decode_selection_bounds(SseDeserializer deserializer); + @protected + ServiceInfo sse_decode_service_info(SseDeserializer deserializer); + @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -188,6 +291,9 @@ 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, @@ -206,12 +312,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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); @@ -227,9 +345,21 @@ 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); + @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); @@ -239,12 +369,42 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @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, @@ -257,15 +417,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { 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); @@ -284,6 +462,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_service_info(ServiceInfo self, SseSerializer serializer); + @protected void sse_encode_u_16(int self, SseSerializer serializer); @@ -295,6 +476,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/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..73c7285e 100644 --- a/mobile/lib/src/screens/workspace_screen.dart +++ b/mobile/lib/src/screens/workspace_screen.dart @@ -1,4 +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'; @@ -8,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(); @@ -41,15 +57,121 @@ class WorkspaceScreen extends StatelessWidget { secondsSinceActivity: workspace.secondsSinceActivity, ), ), - if (connId != null && project != null) + // 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: const Icon(Icons.add), - tooltip: 'New Terminal', + icon: Icon( + workspace.fullscreenTerminal != null + ? Icons.fullscreen_exit + : Icons.fullscreen, + size: 20, + ), + tooltip: workspace.fullscreenTerminal != null + ? 'Exit Fullscreen' + : 'Fullscreen', onPressed: () { - state_ffi.createTerminal( - connId: connId, - projectId: project.id, - ); + 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; + } }, ), ], @@ -95,21 +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, ), ], ), @@ -199,6 +313,15 @@ class _ProjectSwitcher extends StatelessWidget { ), ), ), + 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]), + ), + ], ], ), ); @@ -245,7 +368,7 @@ class _TerminalTabBar extends StatelessWidget { return GestureDetector( onTap: () => onSelect(tid), - onLongPress: () => _showCloseDialog(context, tid, name), + onLongPress: () => _showTabMenu(context, tid, name), child: Container( margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12), @@ -273,13 +396,104 @@ class _TerminalTabBar extends StatelessWidget { ); } - void _showCloseDialog( - BuildContext context, String terminalId, String name) { + 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('Close terminal'), - content: Text('Close "$name"?'), + 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(), @@ -288,14 +502,14 @@ class _TerminalTabBar extends StatelessWidget { TextButton( onPressed: () { Navigator.of(ctx).pop(); - state_ffi.closeTerminal( + state_ffi.renameTerminal( connId: connId, projectId: projectId, terminalId: terminalId, + name: controller.text, ); }, - child: const Text('Close', - style: TextStyle(color: Colors.redAccent)), + child: const Text('Rename'), ), ], ), @@ -330,3 +544,826 @@ class _ConnectionDot extends StatelessWidget { ); } } + +/// 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 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); } 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..a9dd58ed --- /dev/null +++ b/mobile/lib/src/widgets/layout_renderer.dart @@ -0,0 +1,400 @@ +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 index 5383fc2c..21ad0185 100644 --- a/mobile/lib/src/widgets/project_drawer.dart +++ b/mobile/lib/src/widgets/project_drawer.dart @@ -40,112 +40,19 @@ class ProjectDrawer extends StatelessWidget { ), ), 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, - ), - 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(); - }, - ), - ], - ], - ); - }, + 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'), @@ -159,18 +66,705 @@ class ProjectDrawer extends StatelessWidget { ); } - void _showCloseDialog( + 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('Close terminal'), - content: Text('Close "$name"?'), + 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(), @@ -178,19 +772,220 @@ class ProjectDrawer extends StatelessWidget { ), TextButton( onPressed: () { - Navigator.of(ctx).pop(); // dialog - Navigator.of(context).pop(); // drawer - state_ffi.closeTerminal( + Navigator.of(ctx).pop(); + state_ffi.renameTerminal( connId: connId, projectId: projectId, terminalId: terminalId, + name: controller.text, ); }, - child: const Text('Close', - style: TextStyle(color: Colors.redAccent)), + 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 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, + ), ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/mobile/native/src/api/state.rs b/mobile/native/src/api/state.rs index 7ae29f3b..6bfe6e8c 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,6 +15,39 @@ 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 { + 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. @@ -36,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(), @@ -43,6 +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(), + git_branch, + git_lines_added, + git_lines_removed, + services, + folder_color: format!("{:?}", p.folder_color).to_lowercase(), } }) .collect() @@ -56,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 { @@ -86,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); } @@ -112,30 +222,512 @@ 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. +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 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..ff1de1b4 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"); @@ -161,12 +164,36 @@ 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(); 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(); diff --git a/mobile/native/src/client/manager.rs b/mobile/native/src/client/manager.rs index 4d3e35a2..71811876 100644 --- a/mobile/native/src/client/manager.rs +++ b/mobile/native/src/client/manager.rs @@ -187,6 +187,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(); @@ -214,6 +263,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 @@ -242,7 +301,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. diff --git a/mobile/native/src/frb_generated.rs b/mobile/native/src/frb_generated.rs index b9b4ea32..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 = -1973712882; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 632182563; // Section: executor @@ -45,6 +45,90 @@ 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, @@ -121,6 +205,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 +385,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 +524,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 +548,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 +576,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 +607,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 +641,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 +674,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 +705,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 +714,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 +735,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 +773,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,17 +804,700 @@ 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__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) + })()) }, ) } @@ -661,7 +1508,133 @@ fn wire__crate__api__terminal__resize_terminal_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "resize_terminal", + 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, }, @@ -677,33 +1650,231 @@ 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); + let api_delta = ::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, - ); + crate::api::terminal::scroll(api_conn_id, api_terminal_id, api_delta); })?; Ok(output_ok) })()) }, ) } -fn wire__crate__api__terminal__scroll_impl( +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: "scroll", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + 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 { @@ -716,28 +1887,37 @@ 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); + let api_terminal_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::set_fullscreen( + api_conn_id, + api_project_id, + api_terminal_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } }, ) } -fn wire__crate__api__connection__seconds_since_activity_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, 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_project_color", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, move || { let message = unsafe { @@ -750,17 +1930,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_color = ::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_project_color( + api_conn_id, + api_project_id, + api_color, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } }, ) } -fn wire__crate__api__state__send_special_key_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, @@ -768,7 +1958,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: "split_terminal", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -783,16 +1973,18 @@ 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_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::send_special_key( + let output_ok = crate::api::state::split_terminal( api_conn_id, - api_terminal_id, - api_key, + api_project_id, + api_path, + api_direction, ) .await?; Ok(output_ok) @@ -803,7 +1995,7 @@ fn wire__crate__api__state__send_special_key_impl( }, ) } -fn wire__crate__api__terminal__send_text_impl( +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, @@ -811,7 +2003,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: "start_all_services", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -826,14 +2018,13 @@ 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); 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,6 +2074,49 @@ fn wire__crate__api__terminal__start_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, @@ -923,6 +2157,131 @@ fn wire__crate__api__terminal__start_word_selection_impl( }, ) } +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, @@ -963,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 @@ -1072,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 { @@ -1079,6 +2490,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 +2549,42 @@ 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 { + 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 +2597,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 +2633,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 +2656,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,23 +2682,55 @@ 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 { + 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, }; } } @@ -1230,6 +2774,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 { @@ -1256,6 +2822,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, @@ -1265,12 +2838,51 @@ 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), + 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!(), } } @@ -1283,28 +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 { - 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 => { + 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) } - 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), + 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!(), } } @@ -1408,6 +3025,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 { [ @@ -1417,6 +3075,11 @@ 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() } @@ -1477,6 +3140,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 @@ -1570,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) { @@ -1577,6 +3273,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) { @@ -1604,6 +3318,36 @@ 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) { + ::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) { @@ -1614,6 +3358,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) { @@ -1634,6 +3388,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) { @@ -1644,6 +3408,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) { @@ -1654,6 +3428,26 @@ 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) { + ::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) { @@ -1663,6 +3457,11 @@ 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); } } @@ -1693,6 +3492,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) { @@ -1719,6 +3531,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. 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: 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); }); diff --git a/src/app/remote_commands.rs b/src/app/remote_commands.rs index 39e75360..4ad18582 100644 --- a/src/app/remote_commands.rs +++ b/src/app/remote_commands.rs @@ -118,6 +118,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(); @@ -158,7 +167,7 @@ pub(crate) async fn remote_command_loop( name: p.name.clone(), path: p.path.clone(), show_in_overview: p.show_in_overview, - 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, 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 {