Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a20d6f7
fix(terminal): clamp resize to minimum 1 col/row to prevent panic
jonasnobile Feb 16, 2026
9f382c6
feat(core): add Backspace and Delete special keys
jonasnobile Feb 16, 2026
cdd7198
feat(core): add folders, project ordering, and folder colors to API
jonasnobile Feb 16, 2026
36119ee
feat(core): add terminal size propagation in layout tree
jonasnobile Feb 16, 2026
266a358
feat(desktop): include folders, project order, and terminal sizes in …
jonasnobile Feb 16, 2026
0d95072
chore(mobile/ios): configure iOS project with CocoaPods and signing
jonasnobile Feb 16, 2026
174452e
feat(mobile): add design system with OkenaColors and OkenaTypography
jonasnobile Feb 16, 2026
828e657
feat(mobile): add terminal scroll, resize, and display offset FFI
jonasnobile Feb 16, 2026
edd337c
feat(mobile): add folders, project ordering, and terminal management FFI
jonasnobile Feb 16, 2026
d49353a
feat(mobile): redesign app with iOS-native dark theme and terminal im…
jonasnobile Feb 16, 2026
721a90a
chore(mobile): regenerate flutter_rust_bridge bindings
jonasnobile Feb 16, 2026
fd45575
chore(mobile): remove google_fonts dependency and add devtools config
jonasnobile Feb 16, 2026
87b44e8
feat(mobile): add LayoutNode sealed class model and update tests
jonasnobile Mar 29, 2026
236cf54
refactor(mobile): extract send_action_with_response in ConnectionManager
jonasnobile Mar 29, 2026
1b533db
feat(mobile): add fullscreen, git, services, and terminal management FFI
jonasnobile Mar 29, 2026
d199e41
chore(mobile): regenerate flutter_rust_bridge bindings
jonasnobile Mar 29, 2026
a14a506
feat(mobile): add fullscreen, services, git status, and layout manage…
jonasnobile Mar 29, 2026
ddf6138
feat(mobile): add terminal selection and scroll info APIs
jonasnobile Mar 29, 2026
2886637
feat(mobile): add layout management, project reorder, and git file co…
jonasnobile Mar 29, 2026
c762eee
chore(mobile): regenerate flutter_rust_bridge bindings
jonasnobile Mar 29, 2026
f7abe23
feat(mobile): add resizable splits, tab management, and minimized ter…
jonasnobile Mar 29, 2026
297cbb1
feat(mobile): add project drawer enhancements (add project, reorder, …
jonasnobile Mar 29, 2026
d42a96a
feat(mobile): add git diff viewer and file contents viewer
jonasnobile Mar 29, 2026
72e2f3a
fix(ui): make find_word_boundaries work with byte offsets for UTF-8 c…
jonasnobile Apr 16, 2026
0bebe54
fix(terminal): deregister inactive tab panes and add tab-aware naviga…
jonasnobile Apr 16, 2026
7885f54
fix(desktop): use ensure-visible scroll instead of center scroll on u…
jonasnobile Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/okena-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ pub enum ApiLayoutNode {
terminal_id: Option<String>,
minimized: bool,
detached: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
cols: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
rows: Option<u16>,
},
Split {
direction: SplitDirection,
Expand Down Expand Up @@ -443,13 +447,17 @@ mod tests {
terminal_id: Some("t1".into()),
minimized: false,
detached: false,
cols: None,
rows: None,
},
ApiLayoutNode::Tabs {
active_tab: 0,
children: vec![ApiLayoutNode::Terminal {
terminal_id: Some("t2".into()),
minimized: true,
detached: true,
cols: None,
rows: None,
}],
},
],
Expand Down Expand Up @@ -718,6 +726,8 @@ mod tests {
terminal_id: Some("t1".into()),
minimized: false,
detached: false,
cols: None,
rows: None,
},
ApiLayoutNode::Tabs {
active_tab: 0,
Expand All @@ -726,16 +736,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,
},
],
},
Expand Down
17 changes: 15 additions & 2 deletions crates/okena-core/src/client/connection.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -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<WsClientMessage>,
cols: u16,
rows: u16,
);
/// Binary PTY output arrived — route to the terminal's emulator.
fn on_terminal_output(&self, prefixed_id: &str, data: &[u8]);
Expand Down Expand Up @@ -606,9 +609,11 @@ impl<H: ConnectionHandler> RemoteClient<H> {
handler.remove_terminals_except(&config.id, &current_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
Expand Down Expand Up @@ -824,16 +829,24 @@ impl<H: ConnectionHandler> RemoteClient<H> {
{
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,
);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/okena-core/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
77 changes: 77 additions & 0 deletions crates/okena-core/src/client/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,48 @@ fn collect_layout_ids_vec(node: &ApiLayoutNode, ids: &mut Vec<String>) {
}
}

/// 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<String, (u16, u16)> {
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<String, (u16, u16)>,
) {
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<ApiProject>) -> StateResponse {
Expand All @@ -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 {
Expand All @@ -148,6 +188,8 @@ mod tests {
terminal_id: Some(tid.to_string()),
minimized: false,
detached: false,
cols: None,
rows: None,
})
.collect(),
})
Expand Down Expand Up @@ -202,4 +244,39 @@ mod tests {
assert!(diff.removed_terminals.is_empty());
assert!(diff.changed_projects.is_empty());
}

#[test]
fn collect_terminal_sizes_extracts_from_layout() {
let state = make_state(vec![ApiProject {
id: "p1".into(),
name: "p1".into(),
path: "/tmp".into(),
is_visible: true,
layout: Some(ApiLayoutNode::Split {
direction: SplitDirection::Horizontal,
sizes: vec![50.0, 50.0],
children: vec![
ApiLayoutNode::Terminal {
terminal_id: Some("t1".into()),
minimized: false,
detached: false,
cols: Some(120),
rows: Some(40),
},
ApiLayoutNode::Terminal {
terminal_id: Some("t2".into()),
minimized: false,
detached: false,
cols: None,
rows: None,
},
],
}),
terminal_names: Default::default(),
folder_color: FolderColor::default(),
}]);
let sizes = collect_terminal_sizes(&state);
assert_eq!(sizes.get("t1"), Some(&(120, 40)));
assert_eq!(sizes.get("t2"), None);
}
}
16 changes: 8 additions & 8 deletions crates/okena-core/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
pub enum SpecialKey {
Enter,
Escape,
Backspace,
Delete,
CtrlC,
CtrlD,
CtrlZ,
Expand All @@ -17,8 +19,6 @@ pub enum SpecialKey {
End,
PageUp,
PageDown,
Backspace,
Delete,
}

impl SpecialKey {
Expand All @@ -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",
Expand All @@ -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~",
}
}
}
Expand All @@ -54,6 +54,8 @@ mod tests {
let keys = vec![
SpecialKey::Enter,
SpecialKey::Escape,
SpecialKey::Backspace,
SpecialKey::Delete,
SpecialKey::CtrlC,
SpecialKey::CtrlD,
SpecialKey::CtrlZ,
Expand All @@ -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();
Expand All @@ -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");
Expand All @@ -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~");
}
}
37 changes: 28 additions & 9 deletions crates/okena-layout/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ impl LayoutNode {
terminal_id,
minimized,
detached,
..
} => LayoutNode::Terminal {
terminal_id: terminal_id.clone(),
minimized: *minimized,
Expand Down Expand Up @@ -697,6 +698,7 @@ impl LayoutNode {
terminal_id,
minimized,
detached,
..
} => LayoutNode::Terminal {
terminal_id: terminal_id.as_ref().map(|id| format!("{}:{}", prefix, id)),
minimized: *minimized,
Expand Down Expand Up @@ -731,31 +733,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<String, (u16, u16)>,
) -> 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,
},
}
Expand Down
Loading
Loading