diff --git a/src/secure_chat/mod.rs b/src/secure_chat/mod.rs index 13ea57a..7c4bff9 100644 --- a/src/secure_chat/mod.rs +++ b/src/secure_chat/mod.rs @@ -6,7 +6,7 @@ mod webhub; use std::ffi::{c_void, CString}; use std::mem::zeroed; -use std::os::raw::c_int; +use std::os::raw::{c_char, c_int, c_uint, c_ulong}; use std::ptr::null_mut; use std::sync::mpsc::{Receiver, Sender}; @@ -49,6 +49,11 @@ struct SecureChatCtx { const CONTACT_REFRESH_TICKS: u32 = 20; const CHAT_POLL_TICKS: u32 = 4; const UI_TIMER_MS: c_int = 500; +const GDK_KEY_RETURN: c_uint = 0xff0d; +const GDK_KEY_KP_ENTER: c_uint = 0xff8d; +const GDK_KEY_TAB: c_uint = 0xff09; +const GDK_KEY_ISO_LEFT_TAB: c_uint = 0xfe20; +const GDK_SHIFT_MASK: c_uint = 1 << 0; #[allow(non_upper_case_globals)] static mut CTX: *mut SecureChatCtx = null_mut(); @@ -84,7 +89,7 @@ static mut thread_header_label: *mut libui::uiLabel = null_mut(); static mut thread_view: *mut libui::uiMultilineEntry = null_mut(); #[allow(non_upper_case_globals)] -static mut compose_entry: *mut libui::uiEntry = null_mut(); +static mut compose_entry: *mut libui::uiMultilineEntry = null_mut(); #[allow(non_upper_case_globals)] static mut send_button: *mut libui::uiButton = null_mut(); @@ -134,6 +139,38 @@ static mut my_qr_name_entry: *mut libui::uiEntry = null_mut(); #[allow(non_upper_case_globals)] static mut my_qr_path_label: *mut libui::uiLabel = null_mut(); +#[cfg(target_os = "linux")] +#[repr(C)] +struct GdkEventKey { + type_: c_int, + window: *mut c_void, + send_event: i8, + time: u32, + state: c_uint, + keyval: c_uint, + length: c_int, + string: *mut c_char, + hardware_keycode: u16, + group: u8, + is_modifier: u32, +} + +#[cfg(target_os = "linux")] +type GCallback = Option; + +#[cfg(target_os = "linux")] +extern "C" { + fn gtk_widget_grab_focus(widget: *mut c_void); + fn g_signal_connect_data( + instance: *mut c_void, + detailed_signal: *const c_char, + c_handler: GCallback, + data: *mut c_void, + destroy_data: Option, + connect_flags: c_uint, + ) -> c_ulong; +} + unsafe fn ctx() -> &'static mut SecureChatCtx { &mut *CTX } @@ -164,6 +201,207 @@ unsafe fn multiline_text(entry: *mut libui::uiMultilineEntry) -> String { .into_owned() } +unsafe fn selected_contact_is_online() -> bool { + selected_contact().map(|cv| cv.online).unwrap_or(false) +} + +unsafe fn send_current_message() -> bool { + if compose_entry.is_null() || CTX.is_null() { + return false; + } + let body = multiline_text(compose_entry).trim().to_string(); + if body.is_empty() { + return false; + } + + let network_id = match selected_network_id() { + Some(id) => id, + None => return false, + }; + let cv = match selected_contact() { + Some(c) => c, + None => return false, + }; + + let c = ctx(); + let sender_name = self_display_name(&c.client); + let sender_id = c.client.self_address(); + let target_host = preferred_chat_host(&c.client, &cv.contact.peer_id, &cv.contact.ip); + let now = crate::serviceclient::ms_since_epoch(); + let mut ports = vec![cv.contact.port]; + if !ports.contains(&DEFAULT_WEBHUB_PORT) { + ports.push(DEFAULT_WEBHUB_PORT); + } + if !ports.contains(&DEFAULT_BACKONE_PORT) { + ports.push(DEFAULT_BACKONE_PORT); + } + if c.realtime_chat_available { + let _ = c.hub_cmd.send(HubCommand::SendChatRemote { + host: target_host.clone(), + ports, + network_id: network_id.to_lowercase(), + sender_id: sender_id.to_lowercase(), + target_id: cv.contact.peer_id.to_lowercase(), + sender_name: sender_name.clone(), + body: body.clone(), + }); + } + c.session.thread_for(&cv.contact.peer_id).push(ChatLine { + sender: String::from("me"), + sender_name: sender_name.clone(), + body: body.clone(), + at: now, + }); + let empty = CString::new("").unwrap(); + libui::uiMultilineEntrySetText(compose_entry, empty.as_ptr()); + if !c.realtime_chat_available { + let relayed = relay::relay_chat_to_peer_paths( + &c.client, + &network_id, + &sender_id, + &sender_name, + &cv.contact.peer_id, + &target_host, + &body, + ); + if !relayed { + c.session + .thread_for(&cv.contact.peer_id) + .push(ChatLine { + sender: String::from("system"), + sender_name: String::from("system"), + body: String::from( + "Message not delivered — check contact IP:port, webhub bind, and install-secure-chat-deps.sh on both machines", + ), + at: crate::serviceclient::ms_since_epoch(), + }); + } + } + render_thread(); + true +} + +#[cfg(target_os = "linux")] +unsafe fn control_widget(control: *mut libui::uiControl) -> *mut c_void { + if control.is_null() { + return null_mut(); + } + libui::uiControlHandle(control) as *mut c_void +} + +#[cfg(not(target_os = "linux"))] +unsafe fn control_widget(_: *mut libui::uiControl) -> *mut c_void { + null_mut() +} + +unsafe fn focus_compose_entry() { + let widget = control_widget(compose_entry.cast()); + if !widget.is_null() { + #[cfg(target_os = "linux")] + gtk_widget_grab_focus(widget); + } +} + +unsafe fn focus_contact_combo() { + let widget = control_widget(contact_combo.cast()); + if !widget.is_null() { + #[cfg(target_os = "linux")] + gtk_widget_grab_focus(widget); + } +} + +unsafe fn focus_message_input_if_ready() { + if compose_entry.is_null() + || libui::uiControlEnabled(compose_entry.cast()) == 0 + || !selected_contact_is_online() + { + return; + } + focus_compose_entry(); +} + +#[cfg(target_os = "linux")] +unsafe extern "C" fn on_compose_key_press( + _: *mut c_void, + event: *mut GdkEventKey, + _: *mut c_void, +) -> c_int { + if event.is_null() { + return 0; + } + let key = (*event).keyval; + let state = (*event).state; + if key == GDK_KEY_TAB && state & GDK_SHIFT_MASK == 0 { + focus_contact_combo(); + return 1; + } + if key == GDK_KEY_ISO_LEFT_TAB || (key == GDK_KEY_TAB && state & GDK_SHIFT_MASK != 0) { + focus_contact_combo(); + return 1; + } + if (key == GDK_KEY_RETURN || key == GDK_KEY_KP_ENTER) && state & GDK_SHIFT_MASK == 0 { + return if send_current_message() { 1 } else { 0 }; + } + 0 +} + +#[cfg(target_os = "linux")] +unsafe extern "C" fn on_contact_combo_key_press( + _: *mut c_void, + event: *mut GdkEventKey, + _: *mut c_void, +) -> c_int { + if event.is_null() { + return 0; + } + let key = (*event).keyval; + let state = (*event).state; + if key == GDK_KEY_TAB && state & GDK_SHIFT_MASK == 0 { + focus_compose_entry(); + return 1; + } + if key == GDK_KEY_ISO_LEFT_TAB || (key == GDK_KEY_TAB && state & GDK_SHIFT_MASK != 0) { + focus_compose_entry(); + return 1; + } + 0 +} + +unsafe fn install_keyboard_shortcuts() { + #[cfg(target_os = "linux")] + { + let signal = CString::new("key-press-event").unwrap(); + let compose_widget = control_widget(compose_entry.cast()); + if !compose_widget.is_null() { + g_signal_connect_data( + compose_widget, + signal.as_ptr(), + Some(std::mem::transmute::< + unsafe extern "C" fn(*mut c_void, *mut GdkEventKey, *mut c_void) -> c_int, + unsafe extern "C" fn(), + >(on_compose_key_press)), + null_mut(), + None, + 0, + ); + } + let contact_widget = control_widget(contact_combo.cast()); + if !contact_widget.is_null() { + g_signal_connect_data( + contact_widget, + signal.as_ptr(), + Some(std::mem::transmute::< + unsafe extern "C" fn(*mut c_void, *mut GdkEventKey, *mut c_void) -> c_int, + unsafe extern "C" fn(), + >(on_contact_combo_key_press)), + null_mut(), + None, + 0, + ); + } + } +} + unsafe fn set_entry_text(entry: *mut libui::uiEntry, text: &str) { if entry.is_null() { return; @@ -425,6 +663,7 @@ unsafe fn refill_contact_combo_filtered() { render_contact_list(); render_thread(); update_send_enabled(); + focus_message_input_if_ready(); } unsafe fn refresh_contact_status_only() { @@ -753,6 +992,7 @@ unsafe extern "C" fn on_contact_changed(_: *mut libui::uiCombobox, _: *mut c_voi update_thread_header(); render_thread(); update_send_enabled(); + focus_message_input_if_ready(); } unsafe extern "C" fn on_search_changed(_: *mut libui::uiEntry, _: *mut c_void) { @@ -764,79 +1004,7 @@ unsafe extern "C" fn on_search_changed(_: *mut libui::uiEntry, _: *mut c_void) { } unsafe extern "C" fn on_send_clicked(_: *mut libui::uiButton, _: *mut c_void) { - if compose_entry.is_null() || CTX.is_null() { - return; - } - let body = entry_text(compose_entry).trim().to_string(); - if body.is_empty() { - return; - } - - let network_id = match selected_network_id() { - Some(id) => id, - None => return, - }; - let cv = match selected_contact() { - Some(c) => c, - None => return, - }; - - let c = ctx(); - let name = self_display_name(&c.client); - let sender_id = c.client.self_address(); - let target_host = preferred_chat_host(&c.client, &cv.contact.peer_id, &cv.contact.ip); - let now = crate::serviceclient::ms_since_epoch(); - let mut ports = vec![cv.contact.port]; - if !ports.contains(&DEFAULT_WEBHUB_PORT) { - ports.push(DEFAULT_WEBHUB_PORT); - } - if !ports.contains(&DEFAULT_BACKONE_PORT) { - ports.push(DEFAULT_BACKONE_PORT); - } - if c.realtime_chat_available { - let _ = c.hub_cmd.send(HubCommand::SendChatRemote { - host: target_host.clone(), - ports, - network_id: network_id.to_lowercase(), - sender_id: sender_id.to_lowercase(), - target_id: cv.contact.peer_id.to_lowercase(), - sender_name: name.clone(), - body: body.clone(), - }); - } - c.session - .thread_for(&cv.contact.peer_id) - .push(ChatLine { - sender: String::from("me"), - sender_name: name, - body: body.clone(), - at: now, - }); - set_entry_text(compose_entry, ""); - if !c.realtime_chat_available { - let relayed = relay::relay_chat_to_peer_paths( - &c.client, - &network_id, - &sender_id, - &self_display_name(&c.client), - &cv.contact.peer_id, - &target_host, - &body, - ); - if !relayed { - c.session - .thread_for(&cv.contact.peer_id) - .push(ChatLine { - sender: String::from("system"), - sender_name: String::from("system"), - body: String::from( - "Message not delivered — check contact IP:port, webhub bind, and install-secure-chat-deps.sh on both machines", - ), - at: crate::serviceclient::ms_since_epoch(), - }); - } - } - render_thread(); + let _ = send_current_message(); } unsafe fn ingest_chat_message(msg: webhub::ChatIncoming) { @@ -1255,7 +1423,7 @@ pub fn secure_chat_main() { let compose_row = libui::uiNewHorizontalBox(); libui::uiBoxSetPadded(compose_row, 1); - compose_entry = libui::uiNewEntry(); + compose_entry = libui::uiNewMultilineEntry(); libui::uiBoxAppend(compose_row, compose_entry.cast(), 1); let send_lbl = CString::new("Send").unwrap(); @@ -1268,6 +1436,7 @@ pub fn secure_chat_main() { libui::uiBoxAppend(body, right.cast(), 1); libui::uiBoxAppend(root, body.cast(), 1); + install_keyboard_shortcuts(); libui::uiWindowSetChild(main_window, root.cast());