1use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[repr(C)]
10pub enum StatusState {
11 Idle,
12 Attached,
13 Titled,
14 Reconnecting,
15 Failed,
16}
17
18impl StatusState {
19 pub fn as_str(&self) -> &'static str {
20 match self {
21 StatusState::Idle => "idle",
22 StatusState::Attached => "attached",
23 StatusState::Titled => "titled",
24 StatusState::Reconnecting => "reconnecting",
25 StatusState::Failed => "failed",
26 }
27 }
28}
29
30pub const TOOL_DISPLAY_EXPIRY_MS: u64 = 30_000;
32
33pub const SHIMMER_INTERVAL_MS: u64 = 150;
35
36pub fn timestamp() -> String {
38 let now = SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .unwrap()
41 .as_secs();
42 let h = (now / 3600) % 24;
43 let m = (now / 60) % 60;
44 let s = now % 60;
45 format!("{:02}:{:02}:{:02}", h, m, s)
46}
47
48pub fn format_duration(ms: u64) -> String {
50 let seconds = ms / 1000;
51 let minutes = seconds / 60;
52 let hours = minutes / 60;
53
54 if hours > 0 {
55 format!("{}h {}m", hours, minutes % 60)
56 } else if minutes > 0 {
57 format!("{}m {}s", minutes, seconds % 60)
58 } else {
59 format!("{}s", seconds)
60 }
61}
62
63pub fn truncate_to_width(text: &str, max_width: usize) -> String {
65 if text.len() <= max_width {
67 text.to_string()
68 } else {
69 format!("{}...", &text[..max_width.saturating_sub(3)])
70 }
71}
72
73pub fn abbreviate_activity(summary: &str) -> String {
75 truncate_to_width(summary, 30)
76}
77
78pub fn build_bridge_connect_url(environment_id: &str, ingress_url: Option<&str>) -> String {
80 let base_url = get_claude_ai_base_url(None, ingress_url);
81 format!("{}/code?bridge={}", base_url, environment_id)
82}
83
84fn get_claude_ai_base_url(_env: Option<&str>, ingress_url: Option<&str>) -> String {
86 ingress_url
87 .map(|s| s.to_string())
88 .unwrap_or_else(|| "https://claude.ai".to_string())
89}
90
91pub fn build_bridge_session_url(
94 session_id: &str,
95 environment_id: &str,
96 ingress_url: Option<&str>,
97) -> String {
98 let base = get_remote_session_url(session_id, ingress_url);
99 format!("{}?bridge={}", base, environment_id)
100}
101
102fn get_remote_session_url(session_id: &str, ingress_url: Option<&str>) -> String {
104 let base_url = get_claude_ai_base_url(None, ingress_url);
105 let compat_id = session_id.replace("cse_", "session_");
107 format!("{}/code/{}", base_url, compat_id)
108}
109
110pub fn compute_glimmer_index(tick: u64, message_width: u64) -> u64 {
112 let cycle_length = message_width + 20;
113 message_width + 10 - (tick % cycle_length)
114}
115
116pub fn compute_shimmer_segments(text: &str, glimmer_index: u64) -> (String, String, String) {
125 let message_width = string_width(text) as u64;
126 let shimmer_start = glimmer_index as i64 - 1;
127 let shimmer_end = glimmer_index as i64 + 1;
128
129 if shimmer_start >= message_width as i64 || shimmer_end < 0 {
131 return (text.to_string(), String::new(), String::new());
132 }
133
134 let clamped_start = shimmer_start.max(0) as usize;
136 let mut col_pos = 0usize;
137 let mut before = String::new();
138 let mut shimmer = String::new();
139 let mut after = String::new();
140
141 for c in text.chars() {
143 let seg_width = string_width(&c.to_string()) as usize;
144 if col_pos + seg_width <= clamped_start {
145 before.push(c);
146 } else if col_pos > shimmer_end as usize {
147 after.push(c);
148 } else {
149 shimmer.push(c);
150 }
151 col_pos += seg_width;
152 }
153
154 (before, shimmer, after)
155}
156
157fn string_width(s: &str) -> usize {
159 s.chars().count()
162}
163
164#[derive(Debug, Clone)]
166pub struct BridgeStatusInfo {
167 pub label: BridgeStatusLabel,
168 pub color: BridgeStatusColor,
169}
170
171#[derive(Debug, Clone)]
173pub enum BridgeStatusLabel {
174 RemoteControlFailed,
175 RemoteControlReconnecting,
176 RemoteControlActive,
177 RemoteControlConnecting,
178}
179
180impl BridgeStatusLabel {
181 pub fn as_str(&self) -> &'static str {
182 match self {
183 BridgeStatusLabel::RemoteControlFailed => "Remote Control failed",
184 BridgeStatusLabel::RemoteControlReconnecting => "Remote Control reconnecting",
185 BridgeStatusLabel::RemoteControlActive => "Remote Control active",
186 BridgeStatusLabel::RemoteControlConnecting => "Remote Control connecting...",
187 }
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum BridgeStatusColor {
194 Error,
195 Warning,
196 Success,
197}
198
199pub fn get_bridge_status(params: &GetBridgeStatusParams) -> BridgeStatusInfo {
201 if params.error.is_some() {
202 return BridgeStatusInfo {
203 label: BridgeStatusLabel::RemoteControlFailed,
204 color: BridgeStatusColor::Error,
205 };
206 }
207 if params.reconnecting {
208 return BridgeStatusInfo {
209 label: BridgeStatusLabel::RemoteControlReconnecting,
210 color: BridgeStatusColor::Warning,
211 };
212 }
213 if params.session_active || params.connected {
214 return BridgeStatusInfo {
215 label: BridgeStatusLabel::RemoteControlActive,
216 color: BridgeStatusColor::Success,
217 };
218 }
219 BridgeStatusInfo {
220 label: BridgeStatusLabel::RemoteControlConnecting,
221 color: BridgeStatusColor::Warning,
222 }
223}
224
225pub struct GetBridgeStatusParams<'a> {
227 pub error: Option<&'a str>,
228 pub connected: bool,
229 pub session_active: bool,
230 pub reconnecting: bool,
231}
232
233pub fn build_idle_footer_text(url: &str) -> String {
235 format!("Code everywhere with the Claude app or {}", url)
236}
237
238pub fn build_active_footer_text(url: &str) -> String {
240 format!("Continue coding in the Claude app or {}", url)
241}
242
243pub const FAILED_FOOTER_TEXT: &str = "Something went wrong, please try again";
245
246pub fn wrap_with_osc8_link(text: &str, url: &str) -> String {
249 format!("\x1b]8;;{}\x07{}\x1b]8;;\x07", url, text)
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_timestamp() {
258 let ts = timestamp();
259 assert_eq!(ts.len(), 8);
260 assert!(ts.contains(':'));
261 }
262
263 #[test]
264 fn test_truncate_to_width() {
265 assert_eq!(truncate_to_width("hello", 10), "hello");
266 assert_eq!(truncate_to_width("hello world", 8), "hello...");
267 }
268
269 #[test]
270 fn test_compute_glimmer_index() {
271 assert_eq!(compute_glimmer_index(0, 50), 60);
272 assert_eq!(compute_glimmer_index(10, 50), 50);
273 }
274}