From aae15d81835ce4557dc1d8f21f3798e9d28d588c Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:05:29 +0500 Subject: [PATCH 01/10] #954 WIP: Implement clipboard set --- src/netxs/desktopio/gui.hpp | 55 ++++++++++++++++++++++++++++++++++++- src/netxs/desktopio/x11.hpp | 41 +++++++++++++++++++++------ 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index d290f81e49..58b790c63e 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -3321,7 +3321,7 @@ namespace netxs::gui auto& item = lock.thing; if (item.form == mime::disabled) input::board::normalize(item); else item.set(); - os::clipboard::set(item); + owner.clipboard_set(item); auto crop = utf::trunc(item.utf8, owner.gridsz.y / 2); // Trim preview before sending. s11n::sysboard.send(intio, id_t{}, item.size, crop.str(), item.form); } @@ -3621,6 +3621,7 @@ namespace netxs::gui virtual void window_make_exposed() = 0; virtual void window_make_topmost(bool) = 0; + virtual void clipboard_set(input::clipdata& clipdata) = 0; virtual void sync_os_settings() = 0; void window_send_command(arch target, si32 command, arch lParam = {}) @@ -5449,6 +5450,10 @@ namespace netxs::gui if (proc) proc(2/*PROCESS_PER_MONITOR_DPI_AWARE*/); } + void clipboard_set(input::clipdata& clipdata) + { + os::clipboard::set(clipdata); + } void layer_sync_bits(layer& s, rect area, bool zeroize = faux) { if (s.hdc && area) @@ -6708,6 +6713,8 @@ namespace netxs::gui si32 wm_resized_count{}; // window: Number of requests executed to modify the geometry of wm-layers. ui32 last_x11_timestamp{}; // window: The last user input timestamp. + input::clipdata clipdata{}; // window: X11 clipboard buffer. + window(auto&& ...Args) : winbase{ Args... } { @@ -7075,6 +7082,7 @@ namespace netxs::gui { if constexpr (debugmode) log("_post_command: seq=%% target_id=%% command=%%", session.sync_sequence_counter + (ui16)1, utf::to_hex(target_id), command); session.syncrq(x11::req::send_event{ .destination_id = (ui32)target_id, + .event_type = x11::event::ClientMessage, .originator_id = 0, .message_type = session.atom_vtmx, .command = (ui32)command, @@ -7091,6 +7099,7 @@ namespace netxs::gui auto serial = session.sync_sequence_counter + (ui16)1; if constexpr (debugmode) log("_send_command: seq=%%", session.sync_sequence_counter + (ui16)1); session.syncrq(session.sync_buffer, x11::req::send_event{ .destination_id = (ui32)target_id, + .event_type = x11::event::ClientMessage, .originator_id = session.sync_msg_window_id, .message_type = session.atom_vtmx, .serial = (ui16)serial, @@ -7554,6 +7563,13 @@ namespace netxs::gui } } + void clipboard_set(input::clipdata& new_clipdata) + { + clipdata = new_clipdata; + session.sendrq(x11::req::set_selection_owner{ .owner_id = (ui32)master.wm_hWnd, + .selection_id = session.atom_clipboard, + .time = last_x11_timestamp }); + } bool keybd_test_pressed(si32 virtcod, si32 key_all = 0) { return keybd_test_pressed_ex(virtcod, key_all); @@ -8010,6 +8026,7 @@ namespace netxs::gui // Drop input focus (request to minimize to taskbar). session.sendrq(x11::req::send_event{ .destination_id = session.root_window_id, .event_mask = 0x00180000, // SubstructureNotifyMask | SubstructureRedirectMask + .event_type = x11::event::ClientMessage, .originator_id = (ui32)master.wm_hWnd, .message_type = session.atom_wm_change_state, .serial = 3u }); // 3: IconicState @@ -8121,6 +8138,7 @@ namespace netxs::gui if (!originator_id) return; if constexpr (debugmode) log("_reply_command: seq=%%", session.sync_sequence_counter + (ui16)1); session.syncrq(x11::req::send_event{ .destination_id = originator_id, + .event_type = x11::event::ClientMessage, .originator_id = 0, .message_type = session.atom_vtmx, .serial = serial, @@ -8132,6 +8150,7 @@ namespace netxs::gui if (!originator_id) return; if constexpr (debugmode) log("_close_command: seq=%%", session.sync_sequence_counter + (ui16)1); session.sendrq(x11::req::send_event{ .destination_id = originator_id, + .event_type = x11::event::ClientMessage, .originator_id = 0, .message_type = session.atom_wm_protocols, .serial = session.atom_wm_delete_window }); @@ -8578,6 +8597,39 @@ namespace netxs::gui read_buffer.resize(x11::recv_packet_size); // Restore classic read_buffer size. } } + else if (type == x11::event::SelectionRequest) + { + auto sr = netxs::start_lifetime_as(read_buffer.data()); + session.send_batch([&](auto& batch_buffer) + { + if (sr.target_id == session.atom_targets) // Supported clipboard formats requested (TARGETS, UTF8_STRING). + { + if constexpr (debugmode) log(ansi::clr(greenlt, "clipboard formats requested")); + auto targets = std::to_array({ session.atom_targets, session.atom_utf8_string }); + session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requestor_window_id, + .property = sr.property_id, + .type = session.atom_atom }, + targets); + } + else if (sr.target_id == session.atom_utf8_string || sr.target_id == session.atom_string) // Send clipboard text data. + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("clipboard data requested. data sent: '%%'", utf::debase437(clipdata.utf8)))); + session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requestor_window_id, + .property = sr.property_id, + .type = sr.target_id, + .format = 8 }, // 8-bit text. + clipdata.utf8); + } + // Notify remote window. + session.accumrq(batch_buffer, x11::req::send_event{ .destination_id = sr.requestor_window_id, + .event_type = x11::event::SelectionNotify, // SelectionNotify: + .originator_id = sr.time, // - time + .message_type = sr.requestor_window_id, // - requestor + .serial = sr.selection_id, // - selection + .command = sr.target_id, // - target + .lParam = sr.property_id }); // - property + }); + } //else if (type == session.xkb_first_event && is_focused_window) // XKB Notify. //{ // auto xkb_ev = netxs::start_lifetime_as(read_buffer.data()); @@ -9050,6 +9102,7 @@ namespace netxs::gui window(auto&& ...Args) : winbase{ Args... } { } + void clipboard_set(input::clipdata& /*clipdata*/) {} bool keybd_test_pressed(si32 /*virtcod*/, si32 /*keycode*/ = 0) { return true; /*!!(vkstat[virtcod] & 0x80);*/ } bool keybd_test_pressed_ex(si32 /*virtcod*/, si32 /*keycode*/ = 0) { return true; /*!!(vkstat[virtcod] & 0x80);*/ } bool keybd_test_toggled(si32 /*virtcod*/) { return true; /*!!(vkstat[virtcod] & 0x01);*/ } diff --git a/src/netxs/desktopio/x11.hpp b/src/netxs/desktopio/x11.hpp index d1dc061e93..bd92474a06 100644 --- a/src/netxs/desktopio/x11.hpp +++ b/src/netxs/desktopio/x11.hpp @@ -334,6 +334,15 @@ namespace netxs::x11 ui32 long_offset = 0; // Requested payload offset from beginning in quads. ui32 long_length = 1; // Requested payload length in quads. }; + struct set_selection_owner // Opcode 22 (set clipboard owner). + { + byte opcode = 22; + byte pad = 0; + ui16 length = 4; + ui32 owner_id; // wm window id. + ui32 selection_id; // "CLIPBOARD" atom. + ui32 time = 0; // last_x11_timestamp or 0 (CurrentTime). + }; struct send_event // Opcode 25 (send event). { struct reply // client_message @@ -341,7 +350,7 @@ namespace netxs::x11 byte type; // 33: ClientMessage. (or 35: GenericEvent?) byte format; // Data word format: 8, 16, 32 bits. ui16 sequence; - // Payload header: + // Payload body: ui32 originator_id; // Originator window id. ui32 message_type; // Atom message id (a-la WIN32_WM_USER). ui32 serial; // Serial number to sync replay. @@ -355,17 +364,17 @@ namespace netxs::x11 ui16 length = 11; ui32 destination_id; // Destination window id. ui32 event_mask = 0; // 0 for ClientMessage. - byte type = 33; // 33: ClientMessage. + // Payload header: + byte event_type = 33; // e.g., 33: ClientMessage. byte format = 32; // Data format: 8, 16, 32 bits. ui16 sequence = {}; - // Payload header: + // Payload body: ui32 originator_id; // Originator window id. ui32 message_type; // Atom message id (a-la WIN32_WM_USER). - ui32 serial = 0; // data[0]: Serial number to sync replay. =Protocols atom for WM_PROTOCOLS. - ui32 command = 0; // data[1]: User data. - ui32 lParam = 0; // data[2]: User data: =data_length if command==cmd_w_data. - // User data start: - ui32 data32[2] = {}; // data[3,4]: User data. + ui32 serial = 0; // data[0]: Serial number to sync replay. =Protocols atom for WM_PROTOCOLS. + ui32 command = 0; // data[1]: User data. + ui32 lParam = 0; // data[2]: User data: =data_length if command==cmd_w_data. + ui32 data32[2] = {}; // data[3,4]: User data. struct chunk // Subsequent chunk. Chunk count = data_length <= 4*2 ? 0 : (data_length-4*2 + 6*4-1) / 6*4. { byte type; // 33: ClientMessage. (or 35: GenericEvent?) @@ -1731,6 +1740,18 @@ namespace netxs::x11 byte state; // 0: PropertyNewValue, 1: PropertyDelete. byte pad1[15]; }; + struct selection_request // Type 31 (request clipboard). + { + byte type; + byte pad; + ui16 sequence; + ui32 time; + ui32 owner_window_id; // Clipboard owner window id (wm_hWnd). + ui32 requestor_window_id; // Remote window id. + ui32 selection_id; // Atom "CLIPBOARD". + ui32 target_id; // Required clipboard format (atom UTF8_STRING/TARGETS). + ui32 property_id; // Destination property id of remote window. + }; using client_message = x11::req::send_event::reply; } template @@ -1980,6 +2001,8 @@ namespace netxs::x11 ui32 atom_string = 0; ui32 atom_utf8_string = 0; ui32 atom_window = 0; + ui32 atom_clipboard = 0; + ui32 atom_targets = 0; ui32 atom_net_active_window = 0; ui32 atom_net_number_of_desktops = 0; ui32 atom_net_current_desktop = 0; @@ -2660,6 +2683,8 @@ namespace netxs::x11 // Server related. atom_atom = get_atom_id("ATOM", faux); atom_window = get_atom_id("WINDOW", faux); + atom_clipboard = get_atom_id("CLIPBOARD", faux); + atom_targets = get_atom_id("TARGETS", faux); atom_string = get_atom_id("STRING", faux); atom_cardinal = get_atom_id("CARDINAL", faux); atom_wm_transient_for = get_atom_id("WM_TRANSIENT_FOR", faux); From dec1ecf600358d0a81127fd0a405a314c3931c2e Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:41:10 +0500 Subject: [PATCH 02/10] #954 WIP: Implement clipboard get (small blocks) --- src/netxs/desktopio/directvt.hpp | 18 ++-- src/netxs/desktopio/gui.hpp | 17 +++- src/netxs/desktopio/system.hpp | 3 +- src/netxs/desktopio/x11.hpp | 161 ++++++++++++++++++++++++++++++- 4 files changed, 181 insertions(+), 18 deletions(-) diff --git a/src/netxs/desktopio/directvt.hpp b/src/netxs/desktopio/directvt.hpp index bb3fd9b85a..24a0395c0e 100644 --- a/src/netxs/desktopio/directvt.hpp +++ b/src/netxs/desktopio/directvt.hpp @@ -2199,21 +2199,21 @@ namespace netxs::directvt } } // s11n: Recycle clipboard request. - void recycle_cliprequest(auto& master, s11n::xs::clipdata_request& lock) + void recycle_cliprequest(auto& master, s11n::xs::clipdata& clipdata_lock, s11n::xs::clipdata_request& lock) { auto& item = lock.thing; - auto data = s11n::clipdata.freeze(); - if (data.thing.hash != item.hash) + auto& clipdata = clipdata_lock.thing; + if (clipdata.hash != item.hash) { - data.thing.template sendby(master); //todo gcc 11.4.0 requires template keyword + clipdata.template sendby(master); //todo gcc 11.4.0 requires template keyword } else // Send without payload if hash the same. { - auto temp = std::move(data.thing.utf8); - data.thing.set(); - data.thing.template sendby(master); //todo gcc 11.4.0 requires template keyword - data.thing.utf8 = std::move(temp); - data.thing.set(); + auto temp = std::move(clipdata.utf8); + clipdata.set(); + clipdata.template sendby(master); //todo gcc 11.4.0 requires template keyword + clipdata.utf8 = std::move(temp); + clipdata.set(); } } // s11n: Wake up waiting objects. diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index 58b790c63e..0826627022 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -3327,7 +3327,9 @@ namespace netxs::gui } void handle(s11n::xs::clipdata_request lock) { - s11n::recycle_cliprequest(intio, lock); + auto clipdata_lock = s11n::clipdata.freeze(); + owner.clipboard_get(clipdata_lock.thing); + s11n::recycle_cliprequest(intio, clipdata_lock, lock); } //todo use xs::screenmode void handle(s11n::xs::fullscrn /*lock*/) @@ -3622,6 +3624,7 @@ namespace netxs::gui virtual void window_make_topmost(bool) = 0; virtual void clipboard_set(input::clipdata& clipdata) = 0; + virtual void clipboard_get(input::clipdata& /*clipdata*/) {} virtual void sync_os_settings() = 0; void window_send_command(arch target, si32 command, arch lParam = {}) @@ -7570,6 +7573,11 @@ namespace netxs::gui .selection_id = session.atom_clipboard, .time = last_x11_timestamp }); } + void clipboard_get(input::clipdata& clipdata) + { + session.get_clipboard(clipdata); + clipdata.set(); + } bool keybd_test_pressed(si32 virtcod, si32 key_all = 0) { return keybd_test_pressed_ex(virtcod, key_all); @@ -8494,9 +8502,9 @@ namespace netxs::gui { if constexpr (debugmode) log("Request atom_net_workarea value"); session.sendrq({ .window_id = session.root_window_id, - .property = session.atom_net_workarea, - .prop_type = session.atom_cardinal, - .long_length = 4 }, {}, + .property = session.atom_net_workarea, + .prop_type = session.atom_cardinal, + .long_length = 4 }, {}, [&](auto& ev, view payload) { if (ev.type == x11::event::Error) @@ -8613,6 +8621,7 @@ namespace netxs::gui } else if (sr.target_id == session.atom_utf8_string || sr.target_id == session.atom_string) // Send clipboard text data. { + //todo use INCR for large utf8 blocks if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("clipboard data requested. data sent: '%%'", utf::debase437(clipdata.utf8)))); session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requestor_window_id, .property = sr.property_id, diff --git a/src/netxs/desktopio/system.hpp b/src/netxs/desktopio/system.hpp index cb3a76e342..1c94cca334 100644 --- a/src/netxs/desktopio/system.hpp +++ b/src/netxs/desktopio/system.hpp @@ -5142,7 +5142,8 @@ namespace netxs::os } void handle(s11n::xs::clipdata_request lock) { - s11n::recycle_cliprequest(dtvt::client, lock); + auto clipdata_lock = s11n::clipdata.freeze(); + s11n::recycle_cliprequest(dtvt::client, clipdata_lock, lock); } adapter() diff --git a/src/netxs/desktopio/x11.hpp b/src/netxs/desktopio/x11.hpp index bd92474a06..9532a4fde4 100644 --- a/src/netxs/desktopio/x11.hpp +++ b/src/netxs/desktopio/x11.hpp @@ -65,6 +65,8 @@ namespace netxs::x11 data_bytes = sizeof(data); packet.data_len = (ui32)(sizeof(data) * 8 / packet.format); } + //todo change_property overflow + //todo assert((ui16)(sizeof(packet) / 4 + (data_bytes + 3) / 4) < 65536); packet.length = (ui16)(sizeof(packet) / 4 + (data_bytes + 3) / 4); yield += view{ (char*)&packet, sizeof(packet) }; if constexpr (is_string || is_vector) @@ -343,6 +345,29 @@ namespace netxs::x11 ui32 selection_id; // "CLIPBOARD" atom. ui32 time = 0; // last_x11_timestamp or 0 (CurrentTime). }; + struct convert_selection // Opcode 24 (request clipboard data). + { + struct selection_notify // Type 31 + { + byte type; // 31 + byte pad; + ui16 sequence; + ui32 time; + ui32 requestor_window_id; // sync_msg_window_id + ui32 selection_id; // atom_clipboard + ui32 target_id; // atom_utf8_string + ui32 property_id; // atom_vtm_clipboard (if 0(none) - request rejected). + }; + + byte opcode = 24; + byte pad = 0; + ui16 length = 6; + ui32 requestor_window_id; // sync_msg_window_id + ui32 selection_id; // atom_clipboard + ui32 target_id; // atom_utf8_string + ui32 property_id; // Our side clipboard buffer id: atom_vtm_clipboard + ui32 time = 0; // last_x11_timestamp + }; struct send_event // Opcode 25 (send event). { struct reply // client_message @@ -1970,6 +1995,7 @@ namespace netxs::x11 //ui32 atom_my_ping = 0; // _MY_PING ui32 atom_vtmx = 0; // VTMX WIN32: WM_USER ui32 atom_vtm_always_on_top = 0; // VTM_ALWAYS_ON_TOP + ui32 atom_vtm_clipboard = 0; // VTM_CLIPBOARD ui32 atom_wm_hints = 35; // WM_HINTS ui32 atom_wm_transient_for = 68; // WM_TRANSIENT_FOR @@ -2561,7 +2587,8 @@ namespace netxs::x11 .window_id = new_window_id }); if (override_redirect == 0) // Wm master layer only. { - auto wm_class_data = x11::wm_inst_name +"\0vtm\0"s; // "instance_name\0class_name\0". Bind to the taskbar icon in .desktop-file: StartupWMClass=vtm. + if (x11::wm_inst_name.size() > 4096u) x11::wm_inst_name.resize(4096u); // change_property::length is ui16. + auto wm_class_data = x11::wm_inst_name + "\0vtm\0"s; // "instance_name\0class_name\0". Bind to the taskbar icon in .desktop-file: StartupWMClass=vtm. sendrq({ .window_id = new_window_id, .property = atom_wm_class, .type = atom_string, @@ -2638,6 +2665,7 @@ namespace netxs::x11 } void window_set_title(arch window_id, qiew title) { + if (title.size() > 65536 / 2) title = title.substr(0, 65536 / 2); // change_property::length is ui16. sendrq({ .window_id = (ui32)window_id, .property = atom_net_wm_name, .type = atom_utf8_string, @@ -2652,7 +2680,7 @@ namespace netxs::x11 auto reply = x11::req::intern_atom::reply{}; if (x11connection->recv_all((char*)&reply, sizeof(reply)).size() == 32 && reply.type == x11::event::Reply) { - if constexpr (debugmode) log("%%Received atom for '%%'=0x%%", prompt::x11, name, reply.atom_id); + if constexpr (debugmode) log("%%Received atom for '%%'=%%", prompt::x11, name, utf::to_hex_0x(reply.atom_id)); return reply.atom_id; } log("%%Failed to intern atom: %%", prompt::x11, name); @@ -2679,6 +2707,7 @@ namespace netxs::x11 atom_utf8_string = get_atom_id("UTF8_STRING", true); atom_vtmx = get_atom_id("VTMX", true); atom_vtm_always_on_top = get_atom_id("VTM_ALWAYS_ON_TOP", true); + atom_vtm_clipboard = get_atom_id("VTM_CLIPBOARD", true); // Server related. atom_atom = get_atom_id("ATOM", faux); @@ -2732,10 +2761,9 @@ namespace netxs::x11 } return true; } - auto get_atom_name(ui32 atom) + auto _get_atom_name(ui32 atom) { auto atom_name = text{}; - auto lock = std::lock_guard{ sync_mutex }; auto seq_num = syncrq(sync_buffer, x11::req::get_atom_name{ .atom = atom }); if constexpr (debugmode) log("get_atom_name: atom=0x%% seq=%%", utf::to_hex(atom), seq_num); sync_x11connection->send(sync_buffer); @@ -2762,6 +2790,131 @@ namespace netxs::x11 sync_buffer.clear(); return atom_name; } + auto get_atom_name(ui32 atom) + { + auto lock = std::lock_guard{ sync_mutex }; + return _get_atom_name(atom); + } + void get_clipboard(input::clipdata& clipdata) + { + static constexpr auto read_step = 4096u; // 16 KB + auto lock = std::lock_guard{ sync_mutex }; + clipdata.utf8 = {}; + clipdata.form = mime::textonly; + clipdata.hash = datetime::now(); + auto seq_num = syncrq(sync_buffer, x11::req::convert_selection{ .requestor_window_id = sync_msg_window_id, + .selection_id = atom_clipboard, + .target_id = atom_utf8_string, + .property_id = atom_vtm_clipboard, + .time = 0 }); // last_x11_timestamp + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("send convert_selection: seq=%%", seq_num))); + sync_x11connection->send(sync_buffer); + auto ev = x11::event::any{}; + while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) + { + auto type = ev.type & 0x7f; + if (type == x11::event::Error) + { + if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("get_clipboard: seq=%% error: %%", ev.sequence, get_error(ev)))); + } + else if (type == x11::event::Reply && ev.length) // Consume all unexpected payload. + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("unexpected reply with %% bytes payload", ev.length * 4))); + sync_buffer.assign(ev.length * 4, '\0'); + sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); + } + else if (type == x11::event::SelectionNotify) + { + sync_buffer.clear(); + auto sn = netxs::start_lifetime_as(ev); + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("Received clipboard reply: seq=%% property_id=0x%%(%%) requestor_window_id=0x%% selection_id=0x%%(%%) target_id=0x%%(%%)", + sn.sequence, utf::to_hex(sn.property_id), _get_atom_name(sn.property_id), + utf::to_hex(sn.requestor_window_id), + utf::to_hex(sn.selection_id), _get_atom_name(sn.selection_id), + utf::to_hex(sn.target_id), _get_atom_name(sn.target_id)))); + if (sn.property_id != 0) + { + auto seq_num2 = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, // 0: Keep property. + .window_id = sync_msg_window_id, + .property = sn.property_id,// atom_vtm_clipboard + .prop_type = sn.target_id, // atom_utf8_string + .long_length = read_step }); // Buffer limit for the first chunk. + sync_x11connection->send(sync_buffer); + auto ev2 = x11::event::any{}; + while (sync_x11connection->recv_all((char*)&ev2, sizeof(ev2)).size() == sizeof(ev2)) + { + auto type2 = ev2.type & 0x7f; + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("sync recv: seq=%% event=%% (%%) ev2.length=%%", ev2.sequence, event_str(type2), type2, ev2.length))); + if (type2 == x11::event::Error) + { + if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("get clipboard data: seq=%% error: %%", ev2.sequence, get_error(ev2)))); + } + else if ((type2 == x11::event::Reply || type2 == x11::event::GenericEvent) && ev2.length) + { + sync_buffer.assign(ev2.length * 4, '\0'); + sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); + if (ev2.sequence == seq_num2) // Got clipboard data. + { + auto prop_reply = netxs::start_lifetime_as(ev2); + clipdata.utf8.append(sync_buffer.data(), prop_reply.value_len); + if constexpr (debugmode) log("prop_reply.bytes_after=%% ", prop_reply.bytes_after, ansi::clr(greenlt, utf::fprint(" clipboard start: '%%'", utf::debase437(clipdata.utf8)))); + auto bytes_after = prop_reply.bytes_after; + auto current_offset = 0u; + while (bytes_after > 0u) + { + sync_buffer.clear(); + auto seq_next = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, + .window_id = sync_msg_window_id, + .property = sn.property_id, + .prop_type = sn.target_id, + .long_offset = current_offset, + .long_length = read_step }); + sync_x11connection->send(sync_buffer); + auto ev_next = x11::event::any{}; + while (sync_x11connection->recv_all((char*)&ev_next, sizeof(ev_next)).size() == sizeof(ev_next)) + { + auto type_next = ev_next.type & 0x7f; + if (type_next == x11::event::Error) + { + if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("get clipboard tail error: %%", get_error(ev_next)))); + bytes_after = 0; + break; + } + else if ((type_next == x11::event::Reply || type2 == x11::event::GenericEvent) && ev_next.length) + { + sync_buffer.assign(ev_next.length * 4, '\0'); + sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); + if (ev_next.sequence == seq_next) + { + auto reply_chunk = netxs::start_lifetime_as(ev_next); + clipdata.utf8.append(sync_buffer.data(), reply_chunk.value_len); + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" clipboard tail: '%%'", utf::debase437(view{ sync_buffer.data(), reply_chunk.value_len })))); + bytes_after = reply_chunk.bytes_after; + current_offset += read_step; + break; + } + } + } + } + sync_buffer.clear(); + // Request to remove clipboard data. + syncrq(sync_buffer, x11::req::get_property{ .remove = 1, // 1: Remove property. + .window_id = sync_msg_window_id, + .property = sn.property_id, + .prop_type = sn.target_id, + .long_length = 0u }); + sync_x11connection->send(sync_buffer); + } + } + if (ev2.sequence == seq_num2) break; + } + } + break; + } + if (ev.sequence == seq_num) break; + } + sync_buffer.clear(); + } auto enable_detectable_autorepeat() { sendrq({ .major_opcode = xkb_major_opcode, From 6cefeaaf99546cc7b8b90e51cfc9cfbf56b0b0bb Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:53:31 +0500 Subject: [PATCH 03/10] #954 WIP: Refactor clipboard get (get_window_property) --- src/netxs/desktopio/gui.hpp | 4 +- src/netxs/desktopio/x11.hpp | 196 ++++++++++++++++++++++-------------- 2 files changed, 123 insertions(+), 77 deletions(-) diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index 0826627022..1fa123385b 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -8613,7 +8613,9 @@ namespace netxs::gui if (sr.target_id == session.atom_targets) // Supported clipboard formats requested (TARGETS, UTF8_STRING). { if constexpr (debugmode) log(ansi::clr(greenlt, "clipboard formats requested")); - auto targets = std::to_array({ session.atom_targets, session.atom_utf8_string }); + auto targets = std::to_array({ session.atom_targets, + session.atom_utf8_string, + session.atom_string }); session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requestor_window_id, .property = sr.property_id, .type = session.atom_atom }, diff --git a/src/netxs/desktopio/x11.hpp b/src/netxs/desktopio/x11.hpp index 9532a4fde4..0704adf159 100644 --- a/src/netxs/desktopio/x11.hpp +++ b/src/netxs/desktopio/x11.hpp @@ -9,6 +9,7 @@ namespace netxs::x11 static auto wm_inst_name = "vtm"s; //todo unify (app::vtm::id) static constexpr auto fps = 60; // FPS //todo calc at startup static constexpr auto vbi = std::chrono::microseconds(1s) / x11::fps; // V-blank interval (VBI) + static constexpr auto buffer_read_step = 4096u; // 16 KB in quads. using fd_t = os::fd_t; @@ -2027,8 +2028,17 @@ namespace netxs::x11 ui32 atom_string = 0; ui32 atom_utf8_string = 0; ui32 atom_window = 0; + ui32 atom_clipboard = 0; - ui32 atom_targets = 0; + ui32 atom_targets = 0; // Request format list. + ui32 atom_timestamp = 0; // "TIMESTAMP" + ui32 atom_multiple = 0; // "MULTIPLE" Request multiple formats. + ui32 atom_text = 0; // "TEXT" in current LC encoding. + ui32 atom_text_plain = 0; // "text/plain" + ui32 atom_text_plain_utf8 = 0; // "text/plain;charset=utf-8" + ui32 atom_text_html = 0; // "text/html" + ui32 atom_text_richtext = 0; // "text/richtext" + ui32 atom_net_active_window = 0; ui32 atom_net_number_of_desktops = 0; ui32 atom_net_current_desktop = 0; @@ -2705,6 +2715,14 @@ namespace netxs::x11 //atom_net_wm_sync_request_counter = get_atom_id("_NET_WM_SYNC_REQUEST_COUNTER", true); //atom_net_wm_bypass_compositor = get_atom_id("_NET_WM_BYPASS_COMPOSITOR", true); atom_utf8_string = get_atom_id("UTF8_STRING", true); + atom_timestamp = get_atom_id("TIMESTAMP", true); + atom_multiple = get_atom_id("MULTIPLE", true); + atom_text = get_atom_id("TEXT", true); + atom_text_plain = get_atom_id("text/plain", true); + atom_text_plain_utf8 = get_atom_id("text/plain;charset=utf-8", true); + atom_text_html = get_atom_id("text/html", true); + atom_text_richtext = get_atom_id("text/richtext", true); + atom_vtmx = get_atom_id("VTMX", true); atom_vtm_always_on_top = get_atom_id("VTM_ALWAYS_ON_TOP", true); atom_vtm_clipboard = get_atom_id("VTM_CLIPBOARD", true); @@ -2795,9 +2813,107 @@ namespace netxs::x11 auto lock = std::lock_guard{ sync_mutex }; return _get_atom_name(atom); } + auto _sync_get_window_property(ui32 window_id, ui32 property_id, ui32 prop_type_id, bool remove, auto& dest_buffer) + { + using dest_t = std::decay_t; + using item_t = std::decay_t; + auto append_buffer = [&](ui32 chunk_size) + { + if constexpr (std::is_same_v) + { + dest_buffer.append(sync_buffer.data(), chunk_size); + } + else + { + auto initial_size = dest_buffer.size(); + dest_buffer.resize(initial_size + chunk_size); + std::memcpy(dest_buffer.data() + initial_size, sync_buffer.data(), chunk_size * sizeof(item_t)); + } + }; + auto seq_num = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, + .window_id = window_id, + .property = property_id, // e.g., atom_vtm_clipboard + .prop_type = prop_type_id, // e.g., atom_utf8_string + .long_length = x11::buffer_read_step }); // Buffer limit for the first chunk. + sync_x11connection->send(sync_buffer); + auto ev = x11::event::any{}; + while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) + { + auto type = ev.type & 0x7f; + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("_get_window_property recv: seq=%% event=%% (%%) ev.length=%%", ev.sequence, event_str(type), type, ev.length))); + if (type == x11::event::Error) + { + if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("_get_window_property error: seq=%% error: %%", ev.sequence, get_error(ev)))); + } + else if ((type == x11::event::Reply || type == x11::event::GenericEvent) && ev.length) + { + sync_buffer.assign(ev.length * sizeof(ui32), '\0'); + sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); + if (ev.sequence == seq_num) // Got data. + { + auto prop_reply = netxs::start_lifetime_as(ev); + append_buffer(prop_reply.value_len); + if constexpr (debugmode) log("_get_window_property prop_reply.bytes_after=%% ", prop_reply.bytes_after); + auto bytes_after = prop_reply.bytes_after; + auto current_offset = x11::buffer_read_step; // In quads. + while (bytes_after > 0u) + { + sync_buffer.clear(); + auto seq_next = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, + .window_id = window_id, + .property = property_id, + .prop_type = prop_type_id, + .long_offset = current_offset, + .long_length = x11::buffer_read_step }); + sync_x11connection->send(sync_buffer); + auto ev_next = x11::event::any{}; + while (sync_x11connection->recv_all((char*)&ev_next, sizeof(ev_next)).size() == sizeof(ev_next)) + { + auto type_next = ev_next.type & 0x7f; + if (type_next == x11::event::Error) + { + if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("_get_window_property get next error: %%", get_error(ev_next)))); + bytes_after = 0; + break; + } + else if ((type_next == x11::event::Reply || type_next == x11::event::GenericEvent) && ev_next.length) + { + sync_buffer.assign(ev_next.length * sizeof(ui32), '\0'); + sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); + if (ev_next.sequence == seq_next) + { + auto reply_chunk = netxs::start_lifetime_as(ev_next); + append_buffer(reply_chunk.value_len); + bytes_after = reply_chunk.bytes_after; + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("_get_window_property get next: %% bytes, bytes_after=%%", reply_chunk.value_len * sizeof(item_t), bytes_after))); + current_offset += x11::buffer_read_step; + break; + } + } + } + } + sync_buffer.clear(); + // Request to remove data if required. + if (remove) + { + syncrq(sync_buffer, x11::req::get_property{ .remove = 1, // 1: Remove property. + .window_id = sync_msg_window_id, + .property = property_id, + .prop_type = prop_type_id, + .long_length = 0u }); + sync_x11connection->send(sync_buffer); + } + } + } + if (ev.sequence == seq_num) break; + } + } + auto _get_clipboard_format_list(ui32 atom) + { + // + } void get_clipboard(input::clipdata& clipdata) { - static constexpr auto read_step = 4096u; // 16 KB auto lock = std::lock_guard{ sync_mutex }; clipdata.utf8 = {}; clipdata.form = mime::textonly; @@ -2834,80 +2950,8 @@ namespace netxs::x11 utf::to_hex(sn.target_id), _get_atom_name(sn.target_id)))); if (sn.property_id != 0) { - auto seq_num2 = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, // 0: Keep property. - .window_id = sync_msg_window_id, - .property = sn.property_id,// atom_vtm_clipboard - .prop_type = sn.target_id, // atom_utf8_string - .long_length = read_step }); // Buffer limit for the first chunk. - sync_x11connection->send(sync_buffer); - auto ev2 = x11::event::any{}; - while (sync_x11connection->recv_all((char*)&ev2, sizeof(ev2)).size() == sizeof(ev2)) - { - auto type2 = ev2.type & 0x7f; - if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("sync recv: seq=%% event=%% (%%) ev2.length=%%", ev2.sequence, event_str(type2), type2, ev2.length))); - if (type2 == x11::event::Error) - { - if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("get clipboard data: seq=%% error: %%", ev2.sequence, get_error(ev2)))); - } - else if ((type2 == x11::event::Reply || type2 == x11::event::GenericEvent) && ev2.length) - { - sync_buffer.assign(ev2.length * 4, '\0'); - sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); - if (ev2.sequence == seq_num2) // Got clipboard data. - { - auto prop_reply = netxs::start_lifetime_as(ev2); - clipdata.utf8.append(sync_buffer.data(), prop_reply.value_len); - if constexpr (debugmode) log("prop_reply.bytes_after=%% ", prop_reply.bytes_after, ansi::clr(greenlt, utf::fprint(" clipboard start: '%%'", utf::debase437(clipdata.utf8)))); - auto bytes_after = prop_reply.bytes_after; - auto current_offset = 0u; - while (bytes_after > 0u) - { - sync_buffer.clear(); - auto seq_next = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, - .window_id = sync_msg_window_id, - .property = sn.property_id, - .prop_type = sn.target_id, - .long_offset = current_offset, - .long_length = read_step }); - sync_x11connection->send(sync_buffer); - auto ev_next = x11::event::any{}; - while (sync_x11connection->recv_all((char*)&ev_next, sizeof(ev_next)).size() == sizeof(ev_next)) - { - auto type_next = ev_next.type & 0x7f; - if (type_next == x11::event::Error) - { - if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("get clipboard tail error: %%", get_error(ev_next)))); - bytes_after = 0; - break; - } - else if ((type_next == x11::event::Reply || type2 == x11::event::GenericEvent) && ev_next.length) - { - sync_buffer.assign(ev_next.length * 4, '\0'); - sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); - if (ev_next.sequence == seq_next) - { - auto reply_chunk = netxs::start_lifetime_as(ev_next); - clipdata.utf8.append(sync_buffer.data(), reply_chunk.value_len); - if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" clipboard tail: '%%'", utf::debase437(view{ sync_buffer.data(), reply_chunk.value_len })))); - bytes_after = reply_chunk.bytes_after; - current_offset += read_step; - break; - } - } - } - } - sync_buffer.clear(); - // Request to remove clipboard data. - syncrq(sync_buffer, x11::req::get_property{ .remove = 1, // 1: Remove property. - .window_id = sync_msg_window_id, - .property = sn.property_id, - .prop_type = sn.target_id, - .long_length = 0u }); - sync_x11connection->send(sync_buffer); - } - } - if (ev2.sequence == seq_num2) break; - } + _sync_get_window_property(sync_msg_window_id, sn.property_id/*atom_vtm_clipboard*/, sn.target_id/*atom_utf8_string*/, true, clipdata.utf8); + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" got clipboard: '%%'", utf::debase437(clipdata.utf8)))); } break; } From 179852e869c59a903dd74bc31a03d5626a27a323 Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:04:41 +0500 Subject: [PATCH 04/10] #954 WIP: Refactor clipboard get (get clipboard format list) --- src/netxs/desktopio/x11.hpp | 73 +++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/netxs/desktopio/x11.hpp b/src/netxs/desktopio/x11.hpp index 0704adf159..99cbbba1fa 100644 --- a/src/netxs/desktopio/x11.hpp +++ b/src/netxs/desktopio/x11.hpp @@ -2782,6 +2782,7 @@ namespace netxs::x11 auto _get_atom_name(ui32 atom) { auto atom_name = text{}; + sync_buffer.clear(); auto seq_num = syncrq(sync_buffer, x11::req::get_atom_name{ .atom = atom }); if constexpr (debugmode) log("get_atom_name: atom=0x%% seq=%%", utf::to_hex(atom), seq_num); sync_x11connection->send(sync_buffer); @@ -2830,6 +2831,7 @@ namespace netxs::x11 std::memcpy(dest_buffer.data() + initial_size, sync_buffer.data(), chunk_size * sizeof(item_t)); } }; + sync_buffer.clear(); auto seq_num = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, .window_id = window_id, .property = property_id, // e.g., atom_vtm_clipboard @@ -2892,10 +2894,10 @@ namespace netxs::x11 } } } - sync_buffer.clear(); // Request to remove data if required. if (remove) { + sync_buffer.clear(); syncrq(sync_buffer, x11::req::get_property{ .remove = 1, // 1: Remove property. .window_id = sync_msg_window_id, .property = property_id, @@ -2907,10 +2909,25 @@ namespace netxs::x11 } if (ev.sequence == seq_num) break; } + sync_buffer.clear(); } - auto _get_clipboard_format_list(ui32 atom) + auto _sync_filter_unexpected(x11::event::any& ev) { - // + auto type = ev.type & 0x7f; + if (type == x11::event::Error) + { + if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("got error: seq=%% error: %%", ev.sequence, get_error(ev)))); + return faux; + } + else if (type == x11::event::Reply && ev.length) // Consume all unexpected payload. + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("unexpected reply with %% bytes in payload", ev.length * 4))); + sync_buffer.assign(ev.length * 4, '\0'); + sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); + sync_buffer.clear(); + return faux; + } + return true; } void get_clipboard(input::clipdata& clipdata) { @@ -2918,28 +2935,54 @@ namespace netxs::x11 clipdata.utf8 = {}; clipdata.form = mime::textonly; clipdata.hash = datetime::now(); + // Request format list (TARGETS). + if constexpr (debugmode) log(ansi::clr(yellowlt, "Requesting clipboard format list")); auto seq_num = syncrq(sync_buffer, x11::req::convert_selection{ .requestor_window_id = sync_msg_window_id, .selection_id = atom_clipboard, - .target_id = atom_utf8_string, + .target_id = atom_targets, // TARGETS .property_id = atom_vtm_clipboard, - .time = 0 }); // last_x11_timestamp - if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("send convert_selection: seq=%%", seq_num))); + .time = 0 }); sync_x11connection->send(sync_buffer); + auto available_formats = std::vector{}; auto ev = x11::event::any{}; while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) { auto type = ev.type & 0x7f; - if (type == x11::event::Error) + if (_sync_filter_unexpected(ev) && type == x11::event::SelectionNotify) { - if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("get_clipboard: seq=%% error: %%", ev.sequence, get_error(ev)))); - } - else if (type == x11::event::Reply && ev.length) // Consume all unexpected payload. - { - if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("unexpected reply with %% bytes payload", ev.length * 4))); - sync_buffer.assign(ev.length * 4, '\0'); - sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); + auto sn = netxs::start_lifetime_as(ev); + if (sn.property_id != 0) + { + if constexpr (debugmode) log(ansi::clr(greenlt, "Try to get format list")); + _sync_get_window_property(sync_msg_window_id, sn.property_id, atom_atom/*atom list*/, true, available_formats); + if constexpr (debugmode) + { + auto s = utf::fprint("Available formats (%%):\n", available_formats.size()); + for (auto format : available_formats) + { + s = utf::fprint("\t%% (%%)\n", _get_atom_name(format), utf::to_hex_0x(format)); + } + log(s); + } + } + break; } - else if (type == x11::event::SelectionNotify) + if (ev.sequence == seq_num) break; + } + auto has_utf8 = std::find(available_formats.begin(), available_formats.end(), atom_utf8_string) != available_formats.end(); + auto target_to_request = has_utf8 ? atom_utf8_string : atom_string; + // Request clipboard data. + seq_num = syncrq(sync_buffer, x11::req::convert_selection{ .requestor_window_id = sync_msg_window_id, + .selection_id = atom_clipboard, + .target_id = target_to_request, + .property_id = atom_vtm_clipboard, + .time = 0 }); // last_x11_timestamp + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("send convert_selection: seq=%%", seq_num))); + sync_x11connection->send(sync_buffer); + while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) + { + auto type = ev.type & 0x7f; + if (_sync_filter_unexpected(ev) && type == x11::event::SelectionNotify) { sync_buffer.clear(); auto sn = netxs::start_lifetime_as(ev); From e6b1612884581ad4c6d1d304e06da8c8f0673c99 Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:17:23 +0500 Subject: [PATCH 05/10] #954 WIP: Refactor clipboard set (implement INCR) --- src/netxs/desktopio/gui.hpp | 149 +++++++++++++++++++++++++++++++----- src/netxs/desktopio/x11.hpp | 29 ++++--- 2 files changed, 149 insertions(+), 29 deletions(-) diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index 1fa123385b..8d01719c76 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -6684,6 +6684,15 @@ namespace netxs::gui si32 latin_key_count{}; bool is_latin() { return latin_key_count > 'Z' - 'A'; } }; + struct incr_transfer_t + { + size_t current_pos{}; // Current offset in bytes. + text text_data; // Local copy. + ui32 requester_id{}; // Peer window id. + ui32 property_id{}; // Peer property id. + ui32 target_id{}; // Data format id (UTF8_STRING/STRING). + ui32 time{}; // Copy time stamp. + }; using modifier_map_t = x11::req::xkb::get_map::reply::modifier_map; @@ -6717,6 +6726,7 @@ namespace netxs::gui ui32 last_x11_timestamp{}; // window: The last user input timestamp. input::clipdata clipdata{}; // window: X11 clipboard buffer. + std::list incr_transfer; // window: Current INCR transfers. window(auto&& ...Args) : winbase{ Args... } @@ -7568,15 +7578,20 @@ namespace netxs::gui void clipboard_set(input::clipdata& new_clipdata) { - clipdata = new_clipdata; - session.sendrq(x11::req::set_selection_owner{ .owner_id = (ui32)master.wm_hWnd, - .selection_id = session.atom_clipboard, - .time = last_x11_timestamp }); + session.send_batch([&](auto& batch_buffer) + { + clipdata = new_clipdata; // Assign under the session.mutex. + session.accumrq(batch_buffer, x11::req::set_selection_owner{ .owner_id = (ui32)master.wm_hWnd, + .selection_id = session.atom_clipboard, + .time = last_x11_timestamp }); + }); } - void clipboard_get(input::clipdata& clipdata) + void clipboard_get(input::clipdata& empty_clipdata) { - session.get_clipboard(clipdata); - clipdata.set(); + session.get_clipboard(empty_clipdata); + empty_clipdata.set(); + auto lock = std::lock_guard{ session.mutex }; + clipdata = empty_clipdata; // Assign under the session.mutex. } bool keybd_test_pressed(si32 virtcod, si32 key_all = 0) { @@ -8422,7 +8437,7 @@ namespace netxs::gui goto break_break; } } - else if (type == x11::event::PropertyNotify) // Tracking refocus. + else if (type == x11::event::PropertyNotify) // Tracking global refocus and remote clipboard INCR. { auto e = netxs::start_lifetime_as(read_buffer.data()); if constexpr (debugmode) log("%%PropertyNotify atom=%% (%%)", prompt::x11, e.atom, session.get_atom_name(e.atom)); @@ -8535,6 +8550,50 @@ namespace netxs::gui _keybd_load_layouts(); } } + if (incr_transfer.size()) + { + auto iter = std::find_if(incr_transfer.begin(), incr_transfer.end(), [&](auto& peer) + { + return peer.requester_id == e.window_id + && peer.property_id == e.atom + && e.state == 1/*deleted*/; + }); + if (iter != incr_transfer.end()) + { + auto chunk_size = session.max_chunk_bytes(); + auto& transfer = *iter; + auto remains = transfer.text_data.size() - transfer.current_pos; + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" INCR transfer: remains=%%", remains))); + if (remains > 0) + { + auto send_now = std::min(chunk_size, remains); + auto chunk_view = view{ &transfer.text_data[transfer.current_pos], send_now }; + session.send_batch([&](auto& batch_buffer) + { + session.accumrq(batch_buffer, x11::req::change_property{ .window_id = transfer.requester_id, + .property = transfer.property_id, + .type = transfer.target_id, + .format = 8 }, + chunk_view); + }); + transfer.current_pos += send_now; + } + else // Finalize transfer. + { + session.send_batch([&](auto& batch_buffer) + { + session.accumrq(batch_buffer, x11::req::change_window_attrs{ .window_id = transfer.requester_id }, // Stop tracking property delete on remote window. + x11::req::change_window_attrs::payload{ .event_mask = 0 }); + session.accumrq(batch_buffer, x11::req::change_property{ .window_id = transfer.requester_id, + .property = transfer.property_id, + .type = transfer.target_id, + .format = 8 }, + view{}); + }); + incr_transfer.erase(iter); + } + } + } continue; // Skip sys_command(syscmd::update). } else if (type == x11::event::GenericEvent) @@ -8605,41 +8664,95 @@ namespace netxs::gui read_buffer.resize(x11::recv_packet_size); // Restore classic read_buffer size. } } + else if (type == x11::event::SelectionClear) + { + // Clipboard is invalidated (someone takes ownership). + } else if (type == x11::event::SelectionRequest) { auto sr = netxs::start_lifetime_as(read_buffer.data()); session.send_batch([&](auto& batch_buffer) { + auto data_format = (byte)32; // 32-bit. + auto destination_id = sr.requester_window_id; if (sr.target_id == session.atom_targets) // Supported clipboard formats requested (TARGETS, UTF8_STRING). { if constexpr (debugmode) log(ansi::clr(greenlt, "clipboard formats requested")); auto targets = std::to_array({ session.atom_targets, session.atom_utf8_string, session.atom_string }); - session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requestor_window_id, + session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requester_window_id, .property = sr.property_id, .type = session.atom_atom }, targets); } + else if (sr.target_id == session.atom_multiple) // Send multiple clipboard data forms. + { + //todo + if constexpr (debugmode) log(ansi::err("not implemented: Requested multiple clipboard data forms")); + sr.target_id = 0; + } else if (sr.target_id == session.atom_utf8_string || sr.target_id == session.atom_string) // Send clipboard text data. { - //todo use INCR for large utf8 blocks - if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("clipboard data requested. data sent: '%%'", utf::debase437(clipdata.utf8)))); - session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requestor_window_id, - .property = sr.property_id, - .type = sr.target_id, - .format = 8 }, // 8-bit text. - clipdata.utf8); + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("clipboard data requested by %%. data size=%%", utf::to_hex_0x(sr.requester_window_id), clipdata.utf8.size()))); + if (clipdata.utf8.size() <= session.max_chunk_bytes()) // Send directly. + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" send clipdata directly (%% bytes): '%%...'", clipdata.utf8.size(), utf::debase437(clipdata.utf8).substr(0, std::min(clipdata.utf8.size(), 20ul))))); + session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requester_window_id, + .property = sr.property_id, + .type = sr.target_id, + .format = 8 }, // 8-bit text. + clipdata.utf8); + } + else // Activate INCR. + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" activate INCR with peer=%%", utf::to_hex_0x(sr.requester_window_id)))); + auto iter = std::find_if(incr_transfer.begin(), incr_transfer.end(), [&](auto& peer) + { + return peer.requester_id == sr.requester_window_id + && peer.property_id == sr.property_id; + }); + //if (iter != incr_transfer.end()) // Abort incomplete transfer. + //{ + // session.accumrq(batch_buffer, x11::req::send_event{ .destination_id = sr.requester_window_id, + // .event_type = x11::event::SelectionNotify, // SelectionNotify: + // .originator_id = iter->time, // - time + // .message_type = iter->requester_id, // - requester + // .serial = sr.selection_id, // - selection + // .command = iter->target_id, // - target + // .lParam = iter->property_id }); // - property + //} + //else // Add a new peer. + if (iter == incr_transfer.end()) + { + iter = incr_transfer.emplace(iter); + } + iter->text_data = clipdata.utf8; // Make a copy under the session.mutex (session.send_batch). + iter->requester_id = sr.requester_window_id; + iter->property_id = sr.property_id; + iter->target_id = sr.target_id; + iter->time = sr.time; + session.accumrq(batch_buffer, x11::req::change_window_attrs{ .window_id = sr.requester_window_id }, // Track property delete on remote window. + x11::req::change_window_attrs::payload{ .event_mask = x11::event::mask::PropertyChange }); + data_format = 0; // Change to AnyFormat. + sr.target_id = session.atom_incr; // Change to INCR. + session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requester_window_id, + .property = sr.property_id, + .type = sr.target_id }, // "INCR" + netxs::saturate_cast(clipdata.utf8.size())); + } } // Notify remote window. - session.accumrq(batch_buffer, x11::req::send_event{ .destination_id = sr.requestor_window_id, + session.accumrq(batch_buffer, x11::req::send_event{ .destination_id = destination_id, .event_type = x11::event::SelectionNotify, // SelectionNotify: + .format = data_format, .originator_id = sr.time, // - time - .message_type = sr.requestor_window_id, // - requestor + .message_type = sr.requester_window_id, // - requester .serial = sr.selection_id, // - selection .command = sr.target_id, // - target .lParam = sr.property_id }); // - property }); + continue; } //else if (type == session.xkb_first_event && is_focused_window) // XKB Notify. //{ diff --git a/src/netxs/desktopio/x11.hpp b/src/netxs/desktopio/x11.hpp index 99cbbba1fa..cb254219a3 100644 --- a/src/netxs/desktopio/x11.hpp +++ b/src/netxs/desktopio/x11.hpp @@ -73,12 +73,12 @@ namespace netxs::x11 if constexpr (is_string || is_vector) { yield += view{ (char const*)data.data(), data_bytes }; - yield.append(-data_bytes & 3, '\0'); } else { yield += view{ (char*)&data, data_bytes }; } + yield.append(-data_bytes & 3, '\0'); } #pragma pack(push, 1) @@ -354,7 +354,7 @@ namespace netxs::x11 byte pad; ui16 sequence; ui32 time; - ui32 requestor_window_id; // sync_msg_window_id + ui32 requester_window_id; // sync_msg_window_id ui32 selection_id; // atom_clipboard ui32 target_id; // atom_utf8_string ui32 property_id; // atom_vtm_clipboard (if 0(none) - request rejected). @@ -363,7 +363,7 @@ namespace netxs::x11 byte opcode = 24; byte pad = 0; ui16 length = 6; - ui32 requestor_window_id; // sync_msg_window_id + ui32 requester_window_id; // sync_msg_window_id ui32 selection_id; // atom_clipboard ui32 target_id; // atom_utf8_string ui32 property_id; // Our side clipboard buffer id: atom_vtm_clipboard @@ -1773,7 +1773,7 @@ namespace netxs::x11 ui16 sequence; ui32 time; ui32 owner_window_id; // Clipboard owner window id (wm_hWnd). - ui32 requestor_window_id; // Remote window id. + ui32 requester_window_id; // Remote window id. ui32 selection_id; // Atom "CLIPBOARD". ui32 target_id; // Required clipboard format (atom UTF8_STRING/TARGETS). ui32 property_id; // Destination property id of remote window. @@ -1881,7 +1881,7 @@ namespace netxs::x11 ui32 resource_id_mask; // 4 ui32 buffer[8..11] = resource_id_mask ui32 motion_buffer_size; // 4 ui32 buffer[12..15] = motion_buffer_size ui16 vendor_length; // 2 ui16 buffer[16..17] = vendor_length - ui16 maximum_request_length; // 2 ui16 buffer[18..19] = maximum_request_length + ui16 maximum_request_length; // 2 ui16 buffer[18..19] = maximum_request_length in quads byte number_of_screens; // 1 byte buffer[20] = number_of_screens in roots byte number_of_formats; // 1 byte buffer[21] = number_of_formats in pixmap_formats byte image_byte_order; // 1 byte buffer[22] = 0: LSBFirst, 1: MSBFirst @@ -2030,6 +2030,7 @@ namespace netxs::x11 ui32 atom_window = 0; ui32 atom_clipboard = 0; + ui32 atom_incr = 0; // "INCR" ui32 atom_targets = 0; // Request format list. ui32 atom_timestamp = 0; // "TIMESTAMP" ui32 atom_multiple = 0; // "MULTIPLE" Request multiple formats. @@ -2113,6 +2114,10 @@ namespace netxs::x11 shm_buffer.deallocate(); } + auto max_chunk_bytes() + { + return (size_t)s.maximum_request_length * sizeof(ui32) - 64; + } void set_seq_num(ui16 seq_num) { received_replies[seq_num].store(true, std::memory_order_release); @@ -2326,7 +2331,7 @@ namespace netxs::x11 "\n motion_buffer_size: ", s.motion_buffer_size, "\n vendor_length: ", s.vendor_length, "\n vendor: '", utf::debase(vendor_str), '\'', - "\n maximum_request_length: ", s.maximum_request_length, + "\n max_request_length (quads): ", s.maximum_request_length, "\n number_of_screens: ", (si32)s.number_of_screens, "\n number_of_formats: ", (si32)s.number_of_formats, "\n image_byte_order: ", (si32)s.image_byte_order, @@ -2549,6 +2554,7 @@ namespace netxs::x11 .event_mask = 0u | (override_redirect ? 0 : x11::event::mask::Exposure) // KDE doesn't redraw our layers in background. | x11::event::mask::StructureNotify // For ConfigureNotify events. + | x11::event::mask::PropertyChange // For PropertyNotify events (clipboard). , .colormap_id = argb_colormap_id }); // Mandatory: own colormap. // Mark our windows. @@ -2714,6 +2720,7 @@ namespace netxs::x11 //atom_net_wm_sync_request = get_atom_id("_NET_WM_SYNC_REQUEST", true); //atom_net_wm_sync_request_counter = get_atom_id("_NET_WM_SYNC_REQUEST_COUNTER", true); //atom_net_wm_bypass_compositor = get_atom_id("_NET_WM_BYPASS_COMPOSITOR", true); + atom_incr = get_atom_id("INCR", true); atom_utf8_string = get_atom_id("UTF8_STRING", true); atom_timestamp = get_atom_id("TIMESTAMP", true); atom_multiple = get_atom_id("MULTIPLE", true); @@ -2937,7 +2944,7 @@ namespace netxs::x11 clipdata.hash = datetime::now(); // Request format list (TARGETS). if constexpr (debugmode) log(ansi::clr(yellowlt, "Requesting clipboard format list")); - auto seq_num = syncrq(sync_buffer, x11::req::convert_selection{ .requestor_window_id = sync_msg_window_id, + auto seq_num = syncrq(sync_buffer, x11::req::convert_selection{ .requester_window_id = sync_msg_window_id, .selection_id = atom_clipboard, .target_id = atom_targets, // TARGETS .property_id = atom_vtm_clipboard, @@ -2953,7 +2960,7 @@ namespace netxs::x11 auto sn = netxs::start_lifetime_as(ev); if (sn.property_id != 0) { - if constexpr (debugmode) log(ansi::clr(greenlt, "Try to get format list")); + if constexpr (debugmode) log(ansi::clr(greenlt, "Try to get clipboard format list")); _sync_get_window_property(sync_msg_window_id, sn.property_id, atom_atom/*atom list*/, true, available_formats); if constexpr (debugmode) { @@ -2972,7 +2979,7 @@ namespace netxs::x11 auto has_utf8 = std::find(available_formats.begin(), available_formats.end(), atom_utf8_string) != available_formats.end(); auto target_to_request = has_utf8 ? atom_utf8_string : atom_string; // Request clipboard data. - seq_num = syncrq(sync_buffer, x11::req::convert_selection{ .requestor_window_id = sync_msg_window_id, + seq_num = syncrq(sync_buffer, x11::req::convert_selection{ .requester_window_id = sync_msg_window_id, .selection_id = atom_clipboard, .target_id = target_to_request, .property_id = atom_vtm_clipboard, @@ -2986,9 +2993,9 @@ namespace netxs::x11 { sync_buffer.clear(); auto sn = netxs::start_lifetime_as(ev); - if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("Received clipboard reply: seq=%% property_id=0x%%(%%) requestor_window_id=0x%% selection_id=0x%%(%%) target_id=0x%%(%%)", + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("Received clipboard reply: seq=%% property_id=0x%%(%%) requester_window_id=0x%% selection_id=0x%%(%%) target_id=0x%%(%%)", sn.sequence, utf::to_hex(sn.property_id), _get_atom_name(sn.property_id), - utf::to_hex(sn.requestor_window_id), + utf::to_hex(sn.requester_window_id), utf::to_hex(sn.selection_id), _get_atom_name(sn.selection_id), utf::to_hex(sn.target_id), _get_atom_name(sn.target_id)))); if (sn.property_id != 0) From 586bcc1255f9cace215e395d92c88014022294a6 Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:46:25 +0500 Subject: [PATCH 06/10] #954 WIP: Refactor clipboard set (done INCR) --- src/netxs/desktopio/gui.hpp | 25 ++++-- src/netxs/desktopio/x11.hpp | 147 +++++++++++++++++++++++++----------- 2 files changed, 123 insertions(+), 49 deletions(-) diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index 8d01719c76..c2d8873be2 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -8440,7 +8440,17 @@ namespace netxs::gui else if (type == x11::event::PropertyNotify) // Tracking global refocus and remote clipboard INCR. { auto e = netxs::start_lifetime_as(read_buffer.data()); - if constexpr (debugmode) log("%%PropertyNotify atom=%% (%%)", prompt::x11, e.atom, session.get_atom_name(e.atom)); + if constexpr (debugmode) + { + if (e.window_id == session.sync_msg_window_id) // To avoid deadlock on session.sync_mutex in get_atom_name(). + { + log("%%PropertyNotify atom=%% e.state='%%'(%%) e.window_id=%%", prompt::x11, e.atom, e.state ? "DELETED" : "NEW VALUE", (si32)e.state, utf::to_hex_0x(e.window_id)); + } + else + { + log("%%PropertyNotify atom=%% (%%) e.state='%%'(%%) e.window_id=%%", prompt::x11, e.atom, session.get_atom_name(e.atom), e.state ? "DELETED" : "NEW VALUE", (si32)e.state, utf::to_hex_0x(e.window_id)); + } + } if (e.window_id == session.root_window_id) { if (ontop_state && _is_foreground_window() && e.atom == session.atom_vtm_always_on_top) // Update OnTop z-order if AlwaysOnTop enabled. @@ -8554,11 +8564,16 @@ namespace netxs::gui { auto iter = std::find_if(incr_transfer.begin(), incr_transfer.end(), [&](auto& peer) { - return peer.requester_id == e.window_id - && peer.property_id == e.atom - && e.state == 1/*deleted*/; + if constexpr (debugmode) log("find_if: peer.requester_id=%%", utf::to_hex_0x(peer.requester_id)); + return e.window_id == peer.requester_id + && e.atom == peer.property_id + && e.state == 1/*deleted*/; }); - if (iter != incr_transfer.end()) + if (iter == incr_transfer.end()) + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" No peers found: "))); + } + else { auto chunk_size = session.max_chunk_bytes(); auto& transfer = *iter; diff --git a/src/netxs/desktopio/x11.hpp b/src/netxs/desktopio/x11.hpp index cb254219a3..b0047a8097 100644 --- a/src/netxs/desktopio/x11.hpp +++ b/src/netxs/desktopio/x11.hpp @@ -314,6 +314,14 @@ namespace netxs::x11 auto serialize(text& yield, auto const& data) { x11::serialize_str(yield, *this, data); } }; + struct del_property // Opcode 19 (delete property). + { + byte opcode = 19; + byte pad = 0; + ui16 length = 3; + ui32 window_id; + ui32 property; // Atom (e.g., WM_NAME) + }; struct get_property // Opcode 20 (get property). { struct reply @@ -2144,9 +2152,9 @@ namespace netxs::x11 // session.sendrq({ .window_id = 0 }, {}, // [&](auto& ev, view payload) // ev stored in payload. // { - // if (ev.type == x11::event::Error) + // if (!_filter_errors(ev)) // { - // if constexpr (debugmode) log("message"); + // if constexpr (debugmode) log("error message"); // } // else // { @@ -2797,11 +2805,7 @@ namespace netxs::x11 while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) { auto type = ev.type & 0x7f; - if (type == x11::event::Error) - { - if constexpr (debugmode) log(ansi::err("get_atom_name: seq=%% error: %%", ev.sequence, get_error(ev))); - } - else if ((type == x11::event::Reply || type == x11::event::GenericEvent) && ev.length) + if (_filter_errors(ev) && (type == x11::event::Reply || type == x11::event::GenericEvent) && ev.length) { sync_buffer.assign(ev.length * 4, '\0'); if (sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()).size() == sync_buffer.size()) @@ -2824,19 +2828,37 @@ namespace netxs::x11 auto _sync_get_window_property(ui32 window_id, ui32 property_id, ui32 prop_type_id, bool remove, auto& dest_buffer) { using dest_t = std::decay_t; - using item_t = std::decay_t; + static constexpr auto is_vector = requires{ typename dest_t::value_type; dest_buffer.data(); }; + using item_t = decltype([]() + { + if constexpr (is_vector) return typename dest_t::value_type{}; + else return dest_t{}; + }()); + auto append_buffer = [&](ui32 chunk_size) { if constexpr (std::is_same_v) { dest_buffer.append(sync_buffer.data(), chunk_size); } - else + else if constexpr (is_vector) { auto initial_size = dest_buffer.size(); dest_buffer.resize(initial_size + chunk_size); std::memcpy(dest_buffer.data() + initial_size, sync_buffer.data(), chunk_size * sizeof(item_t)); } + else // POD + { + if (chunk_size) + { + assert(chunk_size == 1); + std::memcpy(&dest_buffer, sync_buffer.data(), chunk_size * sizeof(item_t)); + } + else + { + dest_buffer = {}; + } + } }; sync_buffer.clear(); auto seq_num = syncrq(sync_buffer, x11::req::get_property{ .remove = 0, @@ -2845,24 +2867,22 @@ namespace netxs::x11 .prop_type = prop_type_id, // e.g., atom_utf8_string .long_length = x11::buffer_read_step }); // Buffer limit for the first chunk. sync_x11connection->send(sync_buffer); + sync_buffer.clear(); auto ev = x11::event::any{}; while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) { auto type = ev.type & 0x7f; if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("_get_window_property recv: seq=%% event=%% (%%) ev.length=%%", ev.sequence, event_str(type), type, ev.length))); - if (type == x11::event::Error) - { - if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("_get_window_property error: seq=%% error: %%", ev.sequence, get_error(ev)))); - } - else if ((type == x11::event::Reply || type == x11::event::GenericEvent) && ev.length) + if (_filter_errors(ev) && (type == x11::event::Reply || type == x11::event::GenericEvent) && ev.length) { + //todo ?how to get an empty string? sync_buffer.assign(ev.length * sizeof(ui32), '\0'); sync_x11connection->recv_all(sync_buffer.data(), sync_buffer.size()); if (ev.sequence == seq_num) // Got data. { auto prop_reply = netxs::start_lifetime_as(ev); append_buffer(prop_reply.value_len); - if constexpr (debugmode) log("_get_window_property prop_reply.bytes_after=%% ", prop_reply.bytes_after); + if constexpr (debugmode) log("_get_window_property prop_reply.value_len=%% prop_reply.bytes_after=%% ", prop_reply.value_len, prop_reply.bytes_after); auto bytes_after = prop_reply.bytes_after; auto current_offset = x11::buffer_read_step; // In quads. while (bytes_after > 0u) @@ -2901,24 +2921,24 @@ namespace netxs::x11 } } } - // Request to remove data if required. - if (remove) - { - sync_buffer.clear(); - syncrq(sync_buffer, x11::req::get_property{ .remove = 1, // 1: Remove property. - .window_id = sync_msg_window_id, - .property = property_id, - .prop_type = prop_type_id, - .long_length = 0u }); - sync_x11connection->send(sync_buffer); - } } } - if (ev.sequence == seq_num) break; + if (ev.sequence == seq_num) + { + if (remove) // Request to remove data if required. + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("request to detele property: %%", _get_atom_name(property_id)))); + sync_buffer.clear(); + syncrq(sync_buffer, x11::req::del_property{ .window_id = sync_msg_window_id, + .property = property_id }); + sync_x11connection->send(sync_buffer); + } + break; + } } sync_buffer.clear(); } - auto _sync_filter_unexpected(x11::event::any& ev) + bool _filter_errors(x11::event::any const& ev) { auto type = ev.type & 0x7f; if (type == x11::event::Error) @@ -2926,7 +2946,12 @@ namespace netxs::x11 if constexpr (debugmode) log(ansi::clr(reddk, utf::fprint("got error: seq=%% error: %%", ev.sequence, get_error(ev)))); return faux; } - else if (type == x11::event::Reply && ev.length) // Consume all unexpected payload. + return true; + } + auto _sync_filter_unexpected(x11::event::any& ev) + { + auto type = ev.type & 0x7f; + if (_filter_errors(ev) && type == x11::event::Reply && ev.length) // Consume all unexpected payload. { if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("unexpected reply with %% bytes in payload", ev.length * 4))); sync_buffer.assign(ev.length * 4, '\0'); @@ -2938,6 +2963,7 @@ namespace netxs::x11 } void get_clipboard(input::clipdata& clipdata) { + //todo check if we are the clipboard owner auto lock = std::lock_guard{ sync_mutex }; clipdata.utf8 = {}; clipdata.form = mime::textonly; @@ -2950,12 +2976,14 @@ namespace netxs::x11 .property_id = atom_vtm_clipboard, .time = 0 }); sync_x11connection->send(sync_buffer); + sync_buffer.clear(); auto available_formats = std::vector{}; auto ev = x11::event::any{}; while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) { auto type = ev.type & 0x7f; - if (_sync_filter_unexpected(ev) && type == x11::event::SelectionNotify) + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" list recv: type=%% (%%)", event_str(type), type))); + if (_sync_filter_unexpected(ev) && type == x11::event::SelectionNotify && ev.sequence == seq_num) { auto sn = netxs::start_lifetime_as(ev); if (sn.property_id != 0) @@ -2967,14 +2995,14 @@ namespace netxs::x11 auto s = utf::fprint("Available formats (%%):\n", available_formats.size()); for (auto format : available_formats) { - s = utf::fprint("\t%% (%%)\n", _get_atom_name(format), utf::to_hex_0x(format)); + s += utf::fprint("\t%% (%%)\n", _get_atom_name(format), utf::to_hex_0x(format)); } log(s); } } break; } - if (ev.sequence == seq_num) break; + //if (ev.sequence == seq_num) break; } auto has_utf8 = std::find(available_formats.begin(), available_formats.end(), atom_utf8_string) != available_formats.end(); auto target_to_request = has_utf8 ? atom_utf8_string : atom_string; @@ -2986,26 +3014,61 @@ namespace netxs::x11 .time = 0 }); // last_x11_timestamp if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("send convert_selection: seq=%%", seq_num))); sync_x11connection->send(sync_buffer); + sync_buffer.clear(); while (sync_x11connection->recv_all((char*)&ev, sizeof(ev)).size() == sizeof(ev)) { auto type = ev.type & 0x7f; - if (_sync_filter_unexpected(ev) && type == x11::event::SelectionNotify) + if (_sync_filter_unexpected(ev) && type == x11::event::SelectionNotify && ev.sequence == seq_num) { - sync_buffer.clear(); auto sn = netxs::start_lifetime_as(ev); if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("Received clipboard reply: seq=%% property_id=0x%%(%%) requester_window_id=0x%% selection_id=0x%%(%%) target_id=0x%%(%%)", sn.sequence, utf::to_hex(sn.property_id), _get_atom_name(sn.property_id), utf::to_hex(sn.requester_window_id), utf::to_hex(sn.selection_id), _get_atom_name(sn.selection_id), utf::to_hex(sn.target_id), _get_atom_name(sn.target_id)))); - if (sn.property_id != 0) + if (sn.property_id == 0) + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" no clipboard data available"))); + } + else if(sn.target_id == atom_incr) // Huge data block. INCR requested. { + // Read INCR property to preallocate recv buffer and remove it signaling to start INCR. + auto minimal_buffer_size = 0u; + _sync_get_window_property(sync_msg_window_id, sn.property_id, sn.target_id, true, minimal_buffer_size); + if (minimal_buffer_size) + { + clipdata.utf8.reserve(minimal_buffer_size); + } + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("clipboard data is large, INCR required, ~recv buffer size=%%", minimal_buffer_size))); + auto ev_loop = x11::event::any{}; + while (sync_x11connection->recv_all((char*)&ev_loop, sizeof(ev_loop)).size() == sizeof(ev_loop)) + { + auto type_loop = ev_loop.type & 0x7f; + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" recv: type=%% (%%)", event_str(type_loop), type_loop))); + if (_sync_filter_unexpected(ev_loop) && type_loop == x11::event::PropertyNotify) + { + auto pn = netxs::start_lifetime_as(&ev_loop); + if (pn.window_id == sync_msg_window_id && pn.atom == sn.property_id && pn.state == 0/*new value*/) + { + auto initial_size = clipdata.utf8.size(); + _sync_get_window_property(sync_msg_window_id, sn.property_id, 0, true, clipdata.utf8); + if (clipdata.utf8.size() == initial_size) // Finalize. + { + break; + } + } + } + } + } + else //if (sn.target_id != atom_incr) // Small data block. + { + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" Try to get small clipboard block..."))); _sync_get_window_property(sync_msg_window_id, sn.property_id/*atom_vtm_clipboard*/, sn.target_id/*atom_utf8_string*/, true, clipdata.utf8); if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" got clipboard: '%%'", utf::debase437(clipdata.utf8)))); } break; } - if (ev.sequence == seq_num) break; + //if (ev.sequence == seq_num) break; } sync_buffer.clear(); } @@ -3067,9 +3130,8 @@ namespace netxs::x11 //{ // auto ev = netxs::start_lifetime_as(buffer.data()); // if constexpr (debugmode) log("Next event: %%", event_str(ev.type)); - // if (ev.type == x11::event::Error) + // if (!_filter_errors(ev)) // { - // if constexpr (debugmode) log("Request error: %%", get_error(ev)); // break; // } // else if (ev.type == x11::event::ConfigureNotify) @@ -3235,11 +3297,7 @@ namespace netxs::x11 .device_id = device_id }, {}, [&](auto& ev, qiew q) { - if (ev.type == x11::event::Error) - { - log("%%Failed to query devices: %%", prompt::x11, get_error(ev)); - } - else + if (_filter_errors(ev)) { auto reply = netxs::start_lifetime_as(q.data()); q.remove_prefix(sizeof(ev)); @@ -3321,7 +3379,8 @@ namespace netxs::x11 // Create invisible window for sync receiving messages. if constexpr (debugmode) log("Create invisible window: id=0x%% seq=%%", utf::to_hex(sync_msg_window_id), sync_sequence_counter + (ui16)1); syncrq(x11::req::create_window{ .window_id = sync_msg_window_id, - .parent_id = root_window_id }); + .parent_id = root_window_id }, + x11::req::create_window::payload{ .event_mask = x11::event::mask::PropertyChange }); // For PropertyNotify events (clipboard). // Mark our windows. //syncrq({ .window_id = sync_msg_window_id, // .property = atom_vtmx, From f7c9ecddfd3519b962ad025eb8329615f4508fcc Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:07:57 +0500 Subject: [PATCH 07/10] Try to avoid a deadlock when ::SetWindowTextW() calls ::SendMessage() on win32 (in gui::window::window_set_title) --- src/netxs/desktopio/gui.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index c2d8873be2..899e88ea62 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -5644,7 +5644,13 @@ namespace netxs::gui layer_present(l); } } - void window_set_title(view utf8) { ::SetWindowTextW((HWND)master.hWnd, utf::to_utf(utf8).data()); } + void window_set_title(view utf8) + { + base::enqueue([&, utf8](auto& /*boss*/) // enqueue to avoid a deadlock when ::SetWindowTextW() calls ::SendMessage(). + { + ::SetWindowTextW((HWND)master.hWnd, utf::to_utf(utf8).data()); + }); + } bool keybd_test_pressed(si32 virtcod, si32 keycode = 0) { if (keycode == input::key::AltGr) return fake_ralt; From c9a0666c39713e3a758bd07c79e85b546e173beb Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:09:35 +0500 Subject: [PATCH 08/10] v2026.10.06 --- src/netxs/desktopio/application.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/netxs/desktopio/application.hpp b/src/netxs/desktopio/application.hpp index 2f4de3e56e..b91c156eee 100644 --- a/src/netxs/desktopio/application.hpp +++ b/src/netxs/desktopio/application.hpp @@ -21,7 +21,7 @@ namespace netxs::app namespace netxs::app::shared { - static const auto version = "v2026.10.05"; + static const auto version = "v2026.10.06"; static const auto repository = "https://github.com/directvt/vtm"; static const auto usr_config = "~/.config/vtm/settings.xml"s; static const auto sys_config = "/etc/vtm/settings.xml"s; From 7f83cb5f4cf73526524c859780296178de3df9c9 Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:23:55 +0500 Subject: [PATCH 09/10] Fix linux builds --- src/netxs/desktopio/gui.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index 899e88ea62..43b2b72236 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -8718,7 +8718,7 @@ namespace netxs::gui if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint("clipboard data requested by %%. data size=%%", utf::to_hex_0x(sr.requester_window_id), clipdata.utf8.size()))); if (clipdata.utf8.size() <= session.max_chunk_bytes()) // Send directly. { - if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" send clipdata directly (%% bytes): '%%...'", clipdata.utf8.size(), utf::debase437(clipdata.utf8).substr(0, std::min(clipdata.utf8.size(), 20ul))))); + if constexpr (debugmode) log(ansi::clr(greenlt, utf::fprint(" send clipdata directly (%% bytes): '%%...'", clipdata.utf8.size(), utf::debase437(clipdata.utf8).substr(0, std::min(clipdata.utf8.size(), (size_t)20))))); session.accumrq(batch_buffer, x11::req::change_property{ .window_id = sr.requester_window_id, .property = sr.property_id, .type = sr.target_id, From 24c15723907ae6ec0ae734e056e98caedffec8c1 Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:45:58 +0500 Subject: [PATCH 10/10] Minor edits --- src/netxs/desktopio/directvt.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/netxs/desktopio/directvt.hpp b/src/netxs/desktopio/directvt.hpp index 24a0395c0e..6306e4944e 100644 --- a/src/netxs/desktopio/directvt.hpp +++ b/src/netxs/desktopio/directvt.hpp @@ -2202,18 +2202,18 @@ namespace netxs::directvt void recycle_cliprequest(auto& master, s11n::xs::clipdata& clipdata_lock, s11n::xs::clipdata_request& lock) { auto& item = lock.thing; - auto& clipdata = clipdata_lock.thing; - if (clipdata.hash != item.hash) + auto& data = clipdata_lock.thing; + if (data.hash != item.hash) { - clipdata.template sendby(master); //todo gcc 11.4.0 requires template keyword + data.template sendby(master); //todo gcc 11.4.0 requires template keyword } else // Send without payload if hash the same. { - auto temp = std::move(clipdata.utf8); - clipdata.set(); - clipdata.template sendby(master); //todo gcc 11.4.0 requires template keyword - clipdata.utf8 = std::move(temp); - clipdata.set(); + auto temp = std::move(data.utf8); + data.set(); + data.template sendby(master); //todo gcc 11.4.0 requires template keyword + data.utf8 = std::move(temp); + data.set(); } } // s11n: Wake up waiting objects.