diff --git a/warzone/crates/warzone-client/src/tui/commands.rs b/warzone/crates/warzone-client/src/tui/commands.rs index 9236200..1bcc431 100644 --- a/warzone/crates/warzone-client/src/tui/commands.rs +++ b/warzone/crates/warzone-client/src/tui/commands.rs @@ -379,6 +379,12 @@ impl App { Some(resolved) => resolved, None => return, } + } else if raw.starts_with("0x") || raw.starts_with("0X") { + // Resolve ETH address via server + match self.resolve_address(&raw, client).await { + Some(resolved) => resolved, + None => return, + } } else { raw }; @@ -928,6 +934,29 @@ impl App { } } + /// Resolve an ETH address (0x...) or any address format via the server. + pub(crate) async fn resolve_address(&self, addr: &str, client: &ServerClient) -> Option { + let url = format!("{}/v1/resolve/{}", client.base_url, addr); + match client.client.get(&url).send().await { + Ok(resp) => { + if let Ok(data) = resp.json::().await { + if let Some(fp) = data.get("fingerprint").and_then(|v| v.as_str()) { + self.add_message(ChatLine { sender: "system".into(), text: format!("{} → {}", addr, &fp[..fp.len().min(16)]), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }); + return Some(fp.to_string()); + } + if let Some(err) = data.get("error") { + self.add_message(ChatLine { sender: "system".into(), text: format!("Cannot resolve {}: {}", addr, err), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }); + } + } + None + } + Err(e) => { + self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }); + None + } + } + } + async fn list_aliases(&self, client: &ServerClient) { let url = format!("{}/v1/alias/list", client.base_url); match client.client.get(&url).send().await { diff --git a/warzone/crates/warzone-client/src/tui/draw.rs b/warzone/crates/warzone-client/src/tui/draw.rs index 685659c..fb75fe3 100644 --- a/warzone/crates/warzone-client/src/tui/draw.rs +++ b/warzone/crates/warzone-client/src/tui/draw.rs @@ -58,9 +58,14 @@ impl App { } else { (" \u{25CF}", Color::Red) // ● }; + let identity_display = if self.our_eth.is_empty() { + self.our_fp.clone() + } else { + format!("{}", &self.our_eth[..self.our_eth.len().min(12)]) + }; let header = Paragraph::new(Line::from(vec![ Span::styled("WZ ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), - Span::styled(&self.our_fp, Style::default().fg(Color::Green)), + Span::styled(identity_display, Style::default().fg(Color::Green)), Span::raw(" \u{2192} "), Span::styled(peer_str, Style::default().fg(Color::Yellow)), Span::styled( diff --git a/warzone/crates/warzone-client/src/tui/input.rs b/warzone/crates/warzone-client/src/tui/input.rs index 60bde52..fbb9224 100644 --- a/warzone/crates/warzone-client/src/tui/input.rs +++ b/warzone/crates/warzone-client/src/tui/input.rs @@ -51,9 +51,9 @@ impl App { self.cursor_pos += 1; } } - // Home / Ctrl+A + // Home / Ctrl+A / Cmd+A (macOS) KeyCode::Home => { self.cursor_pos = 0; } - KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => { + KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SUPER) => { self.cursor_pos = 0; } // End: cursor to end of input when typing, snap to bottom when input is empty. @@ -70,20 +70,20 @@ impl App { self.cursor_pos = self.input.len(); } } - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { + KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SUPER) => { self.cursor_pos = self.input.len(); } - // Ctrl+U: clear line - KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+U / Cmd+U: clear line + KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SUPER) => { self.input.clear(); self.cursor_pos = 0; } - // Ctrl+K: kill to end of line - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+K / Cmd+K: kill to end of line + KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SUPER) => { self.input.truncate(self.cursor_pos); } - // Ctrl+W: delete word back - KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+W / Cmd+W: delete word back + KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SUPER) => { let before = &self.input[..self.cursor_pos]; let new_pos = before.trim_end().rfind(' ').map(|i| i + 1).unwrap_or(0); self.input.drain(new_pos..self.cursor_pos); diff --git a/warzone/crates/warzone-client/src/tui/types.rs b/warzone/crates/warzone-client/src/tui/types.rs index 17bb889..518d94a 100644 --- a/warzone/crates/warzone-client/src/tui/types.rs +++ b/warzone/crates/warzone-client/src/tui/types.rs @@ -41,6 +41,8 @@ pub struct App { pub receipts: Arc>>, /// Pending incoming file transfers, keyed by file ID. pub pending_files: Arc>>, + /// Our ETH address (derived from seed). + pub our_eth: String, /// Scroll offset from bottom (0 = pinned to newest). pub scroll_offset: usize, /// Whether the WebSocket connection is active. @@ -99,6 +101,14 @@ impl App { timestamp: Local::now(), }); + // Derive ETH address from seed if available + let our_eth = crate::keystore::load_seed_raw() + .map(|seed| { + let eth = warzone_protocol::ethereum::derive_eth_identity(&seed); + eth.address.to_checksum() + }) + .unwrap_or_default(); + App { input: String::new(), messages, @@ -110,6 +120,7 @@ impl App { cursor_pos: 0, receipts: Arc::new(Mutex::new(HashMap::new())), pending_files: Arc::new(Mutex::new(HashMap::new())), + our_eth, scroll_offset: 0, connected: Arc::new(AtomicBool::new(false)), }