use std::io;
use std::time::Duration;
use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
MouseButton, MouseEvent, MouseEventKind,
};
#[cfg(windows)]
pub fn send_mouse_enable() {
const MOUSE_ENABLE: &[u8] = b"\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h";
ssh_debug_log("send_mouse_enable: writing mouse-enable VT sequences to stdout");
unsafe {
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
fn WriteFile(
hFile: *mut std::ffi::c_void,
lpBuffer: *const u8,
nNumberOfBytesToWrite: u32,
lpNumberOfBytesWritten: *mut u32,
lpOverlapped: *mut std::ffi::c_void,
) -> i32;
}
const STD_OUTPUT_HANDLE: u32 = (-11i32) as u32;
let h = GetStdHandle(STD_OUTPUT_HANDLE);
if !h.is_null() && h != (-1isize) as *mut std::ffi::c_void {
let mut written: u32 = 0;
let ok = WriteFile(
h,
MOUSE_ENABLE.as_ptr(),
MOUSE_ENABLE.len() as u32,
&mut written,
std::ptr::null_mut(),
);
ssh_debug_log(&format!(
"send_mouse_enable: WriteFile ok={} written={}",
ok, written,
));
} else {
ssh_debug_log("send_mouse_enable: GetStdHandle(STDOUT) failed");
}
}
use std::io::Write;
let mut out = io::stdout().lock();
let _ = out.write_all(MOUSE_ENABLE);
let _ = out.flush();
ssh_debug_log("send_mouse_enable: stdout write_all done");
const DSR_PROBE: &[u8] = b"\x1b[5n";
let _ = out.write_all(DSR_PROBE);
let _ = out.flush();
ssh_debug_log("send_mouse_enable: DSR probe \\x1b[5n sent (expect \\x1b[0n response)");
unsafe {
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
fn GetConsoleMode(h: *mut std::ffi::c_void, mode: *mut u32) -> i32;
fn SetConsoleMode(h: *mut std::ffi::c_void, mode: u32) -> i32;
}
const STD_OUTPUT_HANDLE: u32 = (-11i32) as u32;
const STD_INPUT_HANDLE: u32 = (-10i32) as u32;
let h = GetStdHandle(STD_OUTPUT_HANDLE);
if !h.is_null() && h != (-1isize) as *mut std::ffi::c_void {
let mut mode: u32 = 0;
if GetConsoleMode(h, &mut mode) != 0 {
let vtp = mode & 0x0004 != 0; ssh_debug_log(&format!(
"stdout console mode: 0x{:04X} VTP={} (pass-through={})",
mode, vtp, if vtp { "likely" } else { "NO" },
));
}
}
let hin = GetStdHandle(STD_INPUT_HANDLE);
if !hin.is_null() && hin != (-1isize) as *mut std::ffi::c_void {
let mut mode: u32 = 0;
if GetConsoleMode(hin, &mut mode) != 0 {
let vti = mode & 0x0200 != 0;
let mouse = mode & 0x0010 != 0;
ssh_debug_log(&format!(
"stdin console mode: 0x{:04X} VTI={} MOUSE={}",
mode, vti, mouse,
));
if !vti || !mouse {
let fixed = mode | 0x0200 | 0x0010; SetConsoleMode(hin, fixed);
ssh_debug_log(&format!(
"stdin mode restored: 0x{:04X} -> 0x{:04X}",
mode, fixed,
));
}
}
}
}
}
#[cfg(not(windows))]
pub fn send_mouse_enable() {
}
pub fn is_ssh_session() -> bool {
std::env::var_os("SSH_CONNECTION").is_some()
|| std::env::var_os("SSH_CLIENT").is_some()
|| std::env::var_os("SSH_TTY").is_some()
}
pub fn needs_vt_input() -> bool {
is_ssh_session()
|| std::env::var("TERMINAL_EMULATOR")
.map_or(false, |v| v.contains("JetBrains"))
}
#[cfg(windows)]
pub fn windows_build_number() -> Option<u32> {
#[repr(C)]
struct OSVERSIONINFOW {
os_version_info_size: u32,
major: u32,
minor: u32,
build: u32,
platform_id: u32,
sz_csd_version: [u16; 128],
}
#[link(name = "ntdll")]
extern "system" {
fn RtlGetVersion(info: *mut OSVERSIONINFOW) -> i32;
}
let mut info: OSVERSIONINFOW = unsafe { std::mem::zeroed() };
info.os_version_info_size = std::mem::size_of::<OSVERSIONINFOW>() as u32;
let status = unsafe { RtlGetVersion(&mut info) };
if status == 0 { Some(info.build) } else { None }
}
#[cfg(not(windows))]
pub fn windows_build_number() -> Option<u32> {
None
}
pub enum InputSource {
Crossterm,
#[cfg(windows)]
Ssh {
rx: std::sync::mpsc::Receiver<Event>,
},
}
impl InputSource {
pub fn new(ssh: bool) -> io::Result<Self> {
if !ssh {
return Ok(InputSource::Crossterm);
}
#[cfg(windows)]
{
match start_ssh_reader() {
Ok(rx) => Ok(InputSource::Ssh { rx }),
Err(e) => {
ssh_debug_log(&format!("SSH VT input init failed: {}; falling back to crossterm", e));
Ok(InputSource::Crossterm)
}
}
}
#[cfg(not(windows))]
{
let _ = ssh;
Ok(InputSource::Crossterm)
}
}
#[inline]
pub fn read_timeout(&self, timeout: Duration) -> io::Result<Option<Event>> {
match self {
InputSource::Crossterm => {
if crossterm::event::poll(timeout)? {
Ok(Some(crossterm::event::read()?))
} else {
Ok(None)
}
}
#[cfg(windows)]
InputSource::Ssh { rx } => match rx.recv_timeout(timeout) {
Ok(evt) => Ok(Some(evt)),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Ok(None),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Ok(None),
},
}
}
#[inline]
pub fn try_read(&self) -> io::Result<Option<Event>> {
match self {
InputSource::Crossterm => {
if crossterm::event::poll(Duration::ZERO)? {
Ok(Some(crossterm::event::read()?))
} else {
Ok(None)
}
}
#[cfg(windows)]
InputSource::Ssh { rx } => match rx.try_recv() {
Ok(evt) => Ok(Some(evt)),
Err(_) => Ok(None),
},
}
}
}
#[inline(always)]
fn make_key(code: KeyCode, modifiers: KeyModifiers) -> Event {
Event::Key(KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: crossterm::event::KeyEventState::empty(),
})
}
#[inline]
fn decode_modifiers(n: u16) -> KeyModifiers {
let m = n.saturating_sub(1);
let mut mods = KeyModifiers::empty();
if m & 1 != 0 {
mods |= KeyModifiers::SHIFT;
}
if m & 2 != 0 {
mods |= KeyModifiers::ALT;
}
if m & 4 != 0 {
mods |= KeyModifiers::CONTROL;
}
mods
}
#[inline]
fn decode_utf16_unit(unit: u16, high_surrogate: &mut Option<u16>) -> Option<char> {
if (0xD800..=0xDBFF).contains(&unit) {
*high_surrogate = Some(unit);
return None;
}
if (0xDC00..=0xDFFF).contains(&unit) {
if let Some(hi) = high_surrogate.take() {
let cp = 0x10000 + ((hi as u32 - 0xD800) << 10) + (unit as u32 - 0xDC00);
return char::from_u32(cp);
}
return None; }
*high_surrogate = None;
char::from_u32(unit as u32)
}
#[derive(Clone, Copy, PartialEq)]
enum PS {
Ground,
Escape, CsiEntry, CsiParam, X10Mouse, Ss3, Paste, PasteEsc, PasteBrk, PasteNum, Osc, OscEsc, }
struct VtParser {
state: PS,
params: [u16; 8],
pidx: u8,
cur: u16,
has_digit: bool,
priv_ch: u8,
x10_n: u8,
x10_buf: [u8; 3],
paste: String,
osc: String,
hi_sur: Option<u16>,
}
impl VtParser {
fn new() -> Self {
Self {
state: PS::Ground,
params: [0; 8],
pidx: 0,
cur: 0,
has_digit: false,
priv_ch: 0,
x10_n: 0,
x10_buf: [0; 3],
paste: String::new(),
osc: String::new(),
hi_sur: None,
}
}
#[inline(always)]
fn reset_csi(&mut self) {
self.params = [0; 8];
self.pidx = 0;
self.cur = 0;
self.has_digit = false;
self.priv_ch = 0;
}
#[inline]
fn feed<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
match self.state {
PS::Ground => self.on_ground(ch, emit),
PS::Escape => self.on_escape(ch, emit),
PS::CsiEntry => self.on_csi_entry(ch, emit),
PS::CsiParam => self.on_csi_param(ch, emit),
PS::X10Mouse => self.on_x10(ch, emit),
PS::Ss3 => self.on_ss3(ch, emit),
PS::Paste => self.on_paste(ch, emit),
PS::PasteEsc => self.on_paste_esc(ch, emit),
PS::PasteBrk => self.on_paste_brk(ch, emit),
PS::PasteNum => self.on_paste_num(ch, emit),
PS::Osc => self.on_osc(ch, emit),
PS::OscEsc => self.on_osc_esc(ch, emit),
}
}
#[inline(always)]
fn has_pending_escape(&self) -> bool {
self.state == PS::Escape
}
fn flush_escape<F: FnMut(Event)>(&mut self, emit: &mut F) {
if self.state == PS::Escape {
emit(make_key(KeyCode::Esc, KeyModifiers::empty()));
self.state = PS::Ground;
}
}
fn cancel_escape(&mut self) {
if self.state == PS::Escape {
self.state = PS::Ground;
}
}
#[inline]
fn on_ground<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
match ch {
'\x1b' => {
self.state = PS::Escape;
}
'\r' | '\n' => emit(make_key(KeyCode::Enter, KeyModifiers::empty())),
'\t' => emit(make_key(KeyCode::Tab, KeyModifiers::empty())),
'\x7f' => emit(make_key(KeyCode::Backspace, KeyModifiers::empty())),
'\x08' => emit(make_key(KeyCode::Backspace, KeyModifiers::empty())),
'\0' => emit(make_key(KeyCode::Char(' '), KeyModifiers::CONTROL)),
c if c as u32 >= 1 && (c as u32) <= 26 => {
let letter = (b'a' + (c as u8) - 1) as char;
emit(make_key(KeyCode::Char(letter), KeyModifiers::CONTROL));
}
c if c as u32 == 28 => emit(make_key(KeyCode::Char('\\'), KeyModifiers::CONTROL)),
c if c as u32 == 29 => emit(make_key(KeyCode::Char(']'), KeyModifiers::CONTROL)),
c if c as u32 == 30 => emit(make_key(KeyCode::Char('^'), KeyModifiers::CONTROL)),
c if c as u32 == 31 => emit(make_key(KeyCode::Char('_'), KeyModifiers::CONTROL)),
c => emit(make_key(KeyCode::Char(c), KeyModifiers::empty())),
}
}
fn on_escape<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
match ch {
'[' => {
self.reset_csi();
self.state = PS::CsiEntry;
}
'O' => {
self.state = PS::Ss3;
}
'\x1b' => {
emit(make_key(KeyCode::Esc, KeyModifiers::empty()));
}
']' => {
self.osc.clear();
self.state = PS::Osc;
}
c if c >= ' ' && c <= '~' => {
emit(make_key(KeyCode::Char(c), KeyModifiers::ALT));
self.state = PS::Ground;
}
c => {
emit(make_key(KeyCode::Esc, KeyModifiers::empty()));
self.state = PS::Ground;
self.on_ground(c, emit);
}
}
}
fn on_csi_entry<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
match ch {
'<' => {
self.priv_ch = b'<';
self.state = PS::CsiParam;
}
'?' => {
self.priv_ch = b'?';
self.state = PS::CsiParam;
}
'0'..='9' => {
self.cur = (ch as u16) - (b'0' as u16);
self.has_digit = true;
self.state = PS::CsiParam;
}
';' => {
self.finish_param();
self.state = PS::CsiParam;
}
'M' => {
self.x10_n = 0;
self.state = PS::X10Mouse;
}
c @ ('A'..='Z' | 'a'..='z' | '~') => {
self.finish_param();
self.dispatch_csi(c, emit);
}
'\x1b' => {
self.state = PS::Escape;
}
_ => {
self.state = PS::Ground;
}
}
}
fn on_csi_param<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
match ch {
'0'..='9' => {
self.cur = self.cur.saturating_mul(10).saturating_add((ch as u16) - (b'0' as u16));
self.has_digit = true;
}
';' => {
self.finish_param();
}
':' => {
self.finish_param();
}
c @ ('A'..='Z' | 'a'..='z' | '~') => {
self.finish_param();
self.dispatch_csi(c, emit);
}
'\x1b' => {
self.state = PS::Escape;
}
_ => {
self.state = PS::Ground;
}
}
}
#[inline]
fn finish_param(&mut self) {
if (self.pidx as usize) < self.params.len() {
self.params[self.pidx as usize] = self.cur;
self.pidx += 1;
}
self.cur = 0;
self.has_digit = false;
}
fn dispatch_csi<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
if self.priv_ch == b'<' {
self.dispatch_sgr_mouse(ch, emit);
self.state = PS::Ground;
return;
}
if self.priv_ch == b'?' {
self.state = PS::Ground;
return;
}
if ch == '~' && self.pidx >= 1 && self.params[0] == 200 {
self.paste.clear();
self.state = PS::Paste;
return;
}
let mods = if self.pidx >= 2 {
decode_modifiers(self.params[1])
} else {
KeyModifiers::empty()
};
match ch {
'A' => emit(make_key(KeyCode::Up, mods)),
'B' => emit(make_key(KeyCode::Down, mods)),
'C' => emit(make_key(KeyCode::Right, mods)),
'D' => emit(make_key(KeyCode::Left, mods)),
'H' => emit(make_key(KeyCode::Home, mods)),
'F' => emit(make_key(KeyCode::End, mods)),
'P' => emit(make_key(KeyCode::F(1), mods)),
'Q' => emit(make_key(KeyCode::F(2), mods)),
'R' => emit(make_key(KeyCode::F(3), mods)),
'S' => emit(make_key(KeyCode::F(4), mods)),
'Z' => emit(make_key(KeyCode::BackTab, KeyModifiers::SHIFT)),
'I' if self.pidx <= 1 && self.params[0] == 0 => emit(Event::FocusGained),
'O' if self.pidx <= 1 && self.params[0] == 0 => emit(Event::FocusLost),
'~' => self.dispatch_tilde(mods, emit),
_ => {} }
self.state = PS::Ground;
}
fn dispatch_tilde<F: FnMut(Event)>(&self, mods: KeyModifiers, emit: &mut F) {
let n = self.params[0];
let code = match n {
1 | 7 => KeyCode::Home,
2 => KeyCode::Insert,
3 => KeyCode::Delete,
4 | 8 => KeyCode::End,
5 => KeyCode::PageUp,
6 => KeyCode::PageDown,
11 => KeyCode::F(1),
12 => KeyCode::F(2),
13 => KeyCode::F(3),
14 => KeyCode::F(4),
15 => KeyCode::F(5),
17 => KeyCode::F(6),
18 => KeyCode::F(7),
19 => KeyCode::F(8),
20 => KeyCode::F(9),
21 => KeyCode::F(10),
23 => KeyCode::F(11),
24 => KeyCode::F(12),
_ => return,
};
emit(make_key(code, mods));
}
fn dispatch_sgr_mouse<F: FnMut(Event)>(&self, final_ch: char, emit: &mut F) {
if self.pidx < 3 {
return;
}
let pb = self.params[0];
let px = self.params[1].saturating_sub(1); let py = self.params[2].saturating_sub(1); let is_release = final_ch == 'm';
let btn_id = pb & 0x03;
let is_shift = pb & 0x04 != 0;
let is_alt = pb & 0x08 != 0;
let is_ctrl = pb & 0x10 != 0;
let is_motion = pb & 0x20 != 0;
let is_scroll = pb & 0x40 != 0;
let mut modifiers = KeyModifiers::empty();
if is_shift { modifiers |= KeyModifiers::SHIFT; }
if is_alt { modifiers |= KeyModifiers::ALT; }
if is_ctrl { modifiers |= KeyModifiers::CONTROL; }
let kind = if is_scroll {
if btn_id == 0 {
MouseEventKind::ScrollUp
} else {
MouseEventKind::ScrollDown
}
} else if is_release {
let button = match btn_id {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::Left,
};
MouseEventKind::Up(button)
} else if is_motion {
if btn_id == 3 {
MouseEventKind::Moved
} else {
let button = match btn_id {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::Left,
};
MouseEventKind::Drag(button)
}
} else {
let button = match btn_id {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::Left,
};
MouseEventKind::Down(button)
};
emit(Event::Mouse(MouseEvent {
kind,
column: px,
row: py,
modifiers,
}));
}
fn on_x10<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
let byte = (ch as u32).min(255) as u8;
self.x10_buf[self.x10_n as usize] = byte;
self.x10_n += 1;
if self.x10_n < 3 {
return;
}
self.state = PS::Ground;
let raw_btn = self.x10_buf[0].wrapping_sub(32);
let col = self.x10_buf[1].wrapping_sub(33) as u16;
let row = self.x10_buf[2].wrapping_sub(33) as u16;
let btn_id = raw_btn & 0x03;
let is_motion = raw_btn & 0x20 != 0;
let is_scroll = raw_btn & 0x40 != 0;
let mut modifiers = KeyModifiers::empty();
if raw_btn & 0x04 != 0 { modifiers |= KeyModifiers::SHIFT; }
if raw_btn & 0x08 != 0 { modifiers |= KeyModifiers::ALT; }
if raw_btn & 0x10 != 0 { modifiers |= KeyModifiers::CONTROL; }
let kind = if is_scroll {
if btn_id == 0 { MouseEventKind::ScrollUp } else { MouseEventKind::ScrollDown }
} else if is_motion {
match btn_id {
0 => MouseEventKind::Drag(MouseButton::Left),
1 => MouseEventKind::Drag(MouseButton::Middle),
2 => MouseEventKind::Drag(MouseButton::Right),
_ => MouseEventKind::Moved,
}
} else if btn_id == 3 {
MouseEventKind::Up(MouseButton::Left)
} else {
let button = match btn_id {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::Left,
};
MouseEventKind::Down(button)
};
emit(Event::Mouse(MouseEvent { kind, column: col, row: row, modifiers }));
}
fn on_ss3<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
self.state = PS::Ground;
match ch {
'A' => emit(make_key(KeyCode::Up, KeyModifiers::empty())),
'B' => emit(make_key(KeyCode::Down, KeyModifiers::empty())),
'C' => emit(make_key(KeyCode::Right, KeyModifiers::empty())),
'D' => emit(make_key(KeyCode::Left, KeyModifiers::empty())),
'H' => emit(make_key(KeyCode::Home, KeyModifiers::empty())),
'F' => emit(make_key(KeyCode::End, KeyModifiers::empty())),
'P' => emit(make_key(KeyCode::F(1), KeyModifiers::empty())),
'Q' => emit(make_key(KeyCode::F(2), KeyModifiers::empty())),
'R' => emit(make_key(KeyCode::F(3), KeyModifiers::empty())),
'S' => emit(make_key(KeyCode::F(4), KeyModifiers::empty())),
_ => {
emit(make_key(KeyCode::Char(ch), KeyModifiers::ALT));
}
}
}
fn on_paste<F: FnMut(Event)>(&mut self, ch: char, _emit: &mut F) {
if ch == '\x1b' {
self.state = PS::PasteEsc;
} else {
self.paste.push(ch);
}
}
fn on_paste_esc<F: FnMut(Event)>(&mut self, ch: char, _emit: &mut F) {
if ch == '[' {
self.state = PS::PasteBrk;
} else {
self.paste.push('\x1b');
self.paste.push(ch);
self.state = PS::Paste;
}
}
fn on_paste_brk<F: FnMut(Event)>(&mut self, ch: char, _emit: &mut F) {
if ch.is_ascii_digit() {
self.cur = (ch as u16) - (b'0' as u16);
self.state = PS::PasteNum;
} else {
self.paste.push('\x1b');
self.paste.push('[');
self.paste.push(ch);
self.state = PS::Paste;
}
}
fn on_paste_num<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
if ch.is_ascii_digit() {
self.cur = self.cur.saturating_mul(10).saturating_add((ch as u16) - (b'0' as u16));
} else if ch == '~' && self.cur == 201 {
let text = std::mem::take(&mut self.paste);
emit(Event::Paste(text));
self.state = PS::Ground;
} else {
self.paste.push('\x1b');
self.paste.push('[');
let s = self.cur.to_string();
self.paste.push_str(&s);
self.paste.push(ch);
self.cur = 0;
self.state = PS::Paste;
}
}
fn on_osc<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
match ch {
'\x07' => {
self.dispatch_osc(emit);
self.state = PS::Ground;
}
'\x1b' => {
self.state = PS::OscEsc;
}
c => {
if self.osc.len() < 131072 {
self.osc.push(c);
}
}
}
}
fn on_osc_esc<F: FnMut(Event)>(&mut self, ch: char, emit: &mut F) {
if ch == '\\' {
self.dispatch_osc(emit);
self.state = PS::Ground;
} else {
self.osc.clear();
self.state = PS::Escape;
self.on_escape(ch, emit);
}
}
fn dispatch_osc<F: FnMut(Event)>(&self, emit: &mut F) {
if let Some(rest) = self.osc.strip_prefix("52;") {
if let Some(sc_idx) = rest.find(';') {
let data = &rest[sc_idx + 1..];
if data != "?" && !data.is_empty() {
if let Some(text) = crate::util::base64_decode(data) {
if !text.is_empty() {
emit(Event::Paste(text));
}
}
}
}
}
}
}
#[cfg(windows)]
fn vk_to_keycode(vk: u16) -> Option<KeyCode> {
match vk {
0x08 => Some(KeyCode::Backspace), 0x09 => Some(KeyCode::Tab), 0x0D => Some(KeyCode::Enter), 0x1B => Some(KeyCode::Esc), 0x20 => Some(KeyCode::Char(' ')), 0x21 => Some(KeyCode::PageUp), 0x22 => Some(KeyCode::PageDown), 0x23 => Some(KeyCode::End), 0x24 => Some(KeyCode::Home), 0x25 => Some(KeyCode::Left), 0x26 => Some(KeyCode::Up), 0x27 => Some(KeyCode::Right), 0x28 => Some(KeyCode::Down), 0x2D => Some(KeyCode::Insert), 0x2E => Some(KeyCode::Delete), 0x70 => Some(KeyCode::F(1)), 0x71 => Some(KeyCode::F(2)),
0x72 => Some(KeyCode::F(3)),
0x73 => Some(KeyCode::F(4)),
0x74 => Some(KeyCode::F(5)),
0x75 => Some(KeyCode::F(6)),
0x76 => Some(KeyCode::F(7)),
0x77 => Some(KeyCode::F(8)),
0x78 => Some(KeyCode::F(9)),
0x79 => Some(KeyCode::F(10)),
0x7A => Some(KeyCode::F(11)),
0x7B => Some(KeyCode::F(12)), _ => None,
}
}
#[cfg(windows)]
fn vk_modifiers(state: u32) -> KeyModifiers {
let mut m = KeyModifiers::empty();
if state & 0x0010 != 0 { m |= KeyModifiers::SHIFT; } if state & (0x0001 | 0x0002) != 0 { m |= KeyModifiers::ALT; } if state & (0x0004 | 0x0008) != 0 { m |= KeyModifiers::CONTROL; } m
}
#[cfg(windows)]
static SSH_LOG: std::sync::LazyLock<std::sync::Mutex<Option<std::fs::File>>> =
std::sync::LazyLock::new(|| {
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.unwrap_or_default();
let dir = format!("{}/.psmux", home);
let _ = std::fs::create_dir_all(&dir);
let f = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(format!("{}/ssh_input.log", dir))
.ok();
std::sync::Mutex::new(f)
});
#[cfg(windows)]
fn ssh_debug_log(msg: &str) {
use std::io::Write;
if let Ok(mut guard) = SSH_LOG.lock() {
if let Some(f) = guard.as_mut() {
let _ = writeln!(f, "{}", msg);
let _ = f.flush();
}
}
}
#[cfg(windows)]
fn ssh_verbose() -> bool {
std::env::var("PSMUX_SSH_DEBUG").ok().as_deref() == Some("1")
}
#[cfg(windows)]
fn start_ssh_reader() -> io::Result<std::sync::mpsc::Receiver<Event>> {
use std::ffi::c_void;
use std::sync::mpsc;
const STD_INPUT_HANDLE: u32 = (-10i32) as u32;
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
const ENABLE_WINDOW_INPUT: u32 = 0x0008;
const ENABLE_MOUSE_INPUT: u32 = 0x0010;
const ENABLE_EXTENDED_FLAGS: u32 = 0x0080;
const ENABLE_LINE_INPUT: u32 = 0x0002;
const ENABLE_ECHO_INPUT: u32 = 0x0004;
const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
const ENABLE_QUICK_EDIT_MODE: u32 = 0x0040;
const KEY_EVENT: u16 = 0x0001;
const MOUSE_EVENT: u16 = 0x0002;
const WINDOW_BUFFER_SIZE_EVENT: u16 = 0x0004;
const WAIT_OBJECT_0: u32 = 0x00000000;
const WAIT_TIMEOUT: u32 = 0x00000102;
#[repr(C)]
#[derive(Copy, Clone)]
struct KEY_EVENT_RECORD {
key_down: i32,
repeat_count: u16,
virtual_key_code: u16,
virtual_scan_code: u16,
u_char: u16,
control_key_state: u32,
}
#[repr(C)]
#[derive(Copy, Clone)]
struct MOUSE_EVENT_RECORD {
mouse_x: i16,
mouse_y: i16,
button_state: u32,
control_key_state: u32,
event_flags: u32,
}
#[repr(C)]
#[derive(Copy, Clone)]
struct WINDOW_BUFFER_SIZE_RECORD {
size_x: i16,
size_y: i16,
}
#[repr(C)]
struct INPUT_RECORD {
event_type: u16,
_pad: u16,
data: [u8; 16], }
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut c_void;
fn GetConsoleMode(h: *mut c_void, mode: *mut u32) -> i32;
fn SetConsoleMode(h: *mut c_void, mode: u32) -> i32;
fn ReadConsoleInputW(
h: *mut c_void,
buf: *mut INPUT_RECORD,
len: u32,
read: *mut u32,
) -> i32;
fn WaitForSingleObject(h: *mut c_void, ms: u32) -> u32;
}
const FROM_LEFT_1ST: u32 = 0x0001;
const RIGHTMOST: u32 = 0x0002;
const FROM_LEFT_2ND: u32 = 0x0004;
const ME_MOVED: u32 = 0x0001;
const ME_WHEELED: u32 = 0x0004;
fn convert_native_mouse(rec: &MOUSE_EVENT_RECORD) -> Option<Event> {
let col = rec.mouse_x.max(0) as u16;
let row = rec.mouse_y.max(0) as u16;
let mods = {
let s = rec.control_key_state;
let mut m = KeyModifiers::empty();
if s & 0x0010 != 0 { m |= KeyModifiers::SHIFT; } if s & (0x0001 | 0x0002) != 0 { m |= KeyModifiers::ALT; } if s & (0x0004 | 0x0008) != 0 { m |= KeyModifiers::CONTROL; } m
};
if rec.event_flags & ME_WHEELED != 0 {
let delta = (rec.button_state >> 16) as i16;
let kind = if delta > 0 { MouseEventKind::ScrollUp } else { MouseEventKind::ScrollDown };
return Some(Event::Mouse(MouseEvent { kind, column: col, row, modifiers: mods }));
}
if rec.event_flags & ME_MOVED != 0 {
if rec.button_state & FROM_LEFT_1ST != 0 {
return Some(Event::Mouse(MouseEvent { kind: MouseEventKind::Drag(MouseButton::Left), column: col, row, modifiers: mods }));
}
if rec.button_state & RIGHTMOST != 0 {
return Some(Event::Mouse(MouseEvent { kind: MouseEventKind::Drag(MouseButton::Right), column: col, row, modifiers: mods }));
}
return Some(Event::Mouse(MouseEvent { kind: MouseEventKind::Moved, column: col, row, modifiers: mods }));
}
if rec.button_state & FROM_LEFT_1ST != 0 {
return Some(Event::Mouse(MouseEvent { kind: MouseEventKind::Down(MouseButton::Left), column: col, row, modifiers: mods }));
}
if rec.button_state & RIGHTMOST != 0 {
return Some(Event::Mouse(MouseEvent { kind: MouseEventKind::Down(MouseButton::Right), column: col, row, modifiers: mods }));
}
if rec.button_state & FROM_LEFT_2ND != 0 {
return Some(Event::Mouse(MouseEvent { kind: MouseEventKind::Down(MouseButton::Middle), column: col, row, modifiers: mods }));
}
if rec.button_state == 0 && rec.event_flags == 0 {
return Some(Event::Mouse(MouseEvent { kind: MouseEventKind::Up(MouseButton::Left), column: col, row, modifiers: mods }));
}
None
}
let (tx, rx) = mpsc::sync_channel::<Event>(1024);
ssh_debug_log("=== psmux SSH input module starting ===");
{
#[repr(C)]
struct OSVERSIONINFOW {
os_version_info_size: u32,
major: u32,
minor: u32,
build: u32,
platform_id: u32,
sz_csd_version: [u16; 128],
}
#[link(name = "ntdll")]
extern "system" {
fn RtlGetVersion(info: *mut OSVERSIONINFOW) -> i32;
}
let mut info: OSVERSIONINFOW = unsafe { std::mem::zeroed() };
info.os_version_info_size = std::mem::size_of::<OSVERSIONINFOW>() as u32;
unsafe { RtlGetVersion(&mut info) };
ssh_debug_log(&format!(
"Windows {}.{} build {}",
info.major, info.minor, info.build,
));
if info.build < 22523 {
ssh_debug_log(&format!(
"WARNING: Windows build {} < 22523 — ConPTY does NOT support \
mouse over SSH. Mouse clicks will not work. \
Upgrade to Windows 11 22H2+ for SSH mouse support.",
info.build,
));
} else {
ssh_debug_log("ConPTY build >= 22523 — mouse over SSH should be supported");
}
}
for var in &["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"] {
if let Ok(val) = std::env::var(var) {
ssh_debug_log(&format!(" {}={}", var, val));
}
}
let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
if handle.is_null() || handle == (-1isize) as *mut c_void {
return Err(io::Error::new(io::ErrorKind::Other, "GetStdHandle(STDIN) failed"));
}
let mut orig_mode: u32 = 0;
if unsafe { GetConsoleMode(handle, &mut orig_mode) } == 0 {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("GetConsoleMode failed (err {})", io::Error::last_os_error()),
));
}
let new_mode = (orig_mode
& !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE))
| ENABLE_VIRTUAL_TERMINAL_INPUT
| ENABLE_WINDOW_INPUT
| ENABLE_MOUSE_INPUT
| ENABLE_EXTENDED_FLAGS;
if unsafe { SetConsoleMode(handle, new_mode) } == 0 {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"SetConsoleMode(+VTI) failed (err {})",
io::Error::last_os_error()
),
));
}
let mut actual_mode: u32 = 0;
if unsafe { GetConsoleMode(handle, &mut actual_mode) } != 0 {
let vti_ok = actual_mode & ENABLE_VIRTUAL_TERMINAL_INPUT != 0;
ssh_debug_log(&format!(
"Console mode: orig=0x{:04X} requested=0x{:04X} actual=0x{:04X} VTI={}",
orig_mode, new_mode, actual_mode, if vti_ok { "YES" } else { "NO" },
));
if !vti_ok {
ssh_debug_log("WARNING: VTI not set — ConPTY may swallow mouse sequences");
}
} else {
ssh_debug_log("WARNING: re-read GetConsoleMode failed after SetConsoleMode");
}
let handle_val = handle as usize;
std::thread::Builder::new()
.name("ssh-vt-input".into())
.spawn(move || {
let handle = handle_val as *mut c_void;
let mut parser = VtParser::new();
let mut records: Vec<INPUT_RECORD> = Vec::with_capacity(64);
records.resize_with(64, || unsafe { std::mem::zeroed() });
const ESC_TIMEOUT_MS: u32 = 50;
let mut alive = true;
let verbose = ssh_verbose();
let mut total_records: u64 = 0;
let mut key_char_count: u64 = 0;
let mut key_vk_count: u64 = 0;
let mut mouse_count: u64 = 0;
let mut loop_count: u64 = 0;
ssh_debug_log(&format!("Reader thread started (verbose={})", verbose));
loop {
loop_count += 1;
let wait_ms = if parser.has_pending_escape() { ESC_TIMEOUT_MS } else { 500 };
let wait = unsafe { WaitForSingleObject(handle, wait_ms) };
if wait == WAIT_TIMEOUT {
if loop_count % 60 == 0 {
ssh_debug_log(&format!(
"heartbeat: loops={} records={} chars={} vk={} mouse={}",
loop_count, total_records, key_char_count, key_vk_count, mouse_count,
));
let mut cur_mode: u32 = 0;
if unsafe { GetConsoleMode(handle, &mut cur_mode) } != 0 {
if cur_mode & ENABLE_VIRTUAL_TERMINAL_INPUT == 0 {
ssh_debug_log("WARNING: VTI cleared! Re-enabling...");
let fixed = cur_mode | ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_MOUSE_INPUT;
unsafe { SetConsoleMode(handle, fixed) };
}
}
}
parser.flush_escape(&mut |evt| {
if tx.send(evt).is_err() { alive = false; }
});
if !alive { break; }
continue;
}
if wait != WAIT_OBJECT_0 {
break; }
let mut count: u32 = 0;
let ok = unsafe {
ReadConsoleInputW(
handle,
records.as_mut_ptr(),
records.len() as u32,
&mut count,
)
};
if ok == 0 || count == 0 {
break;
}
for i in 0..count as usize {
let rec = &records[i];
total_records += 1;
match rec.event_type {
KEY_EVENT => {
let key = unsafe { &*(rec.data.as_ptr() as *const KEY_EVENT_RECORD) };
if key.key_down == 0 { continue; }
if verbose {
ssh_debug_log(&format!(
"KEY vk=0x{:04X} scan=0x{:04X} u_char=0x{:04X}({}) ctrl=0x{:08X}",
key.virtual_key_code, key.virtual_scan_code,
key.u_char, char::from_u32(key.u_char as u32).unwrap_or('.'),
key.control_key_state,
));
}
if key.u_char != 0 {
key_char_count += 1;
if let Some(ch) = decode_utf16_unit(key.u_char, &mut parser.hi_sur) {
parser.feed(ch, &mut |evt| {
if verbose {
ssh_debug_log(&format!(" → emit(char): {:?}", evt));
}
if !verbose && matches!(evt, Event::Mouse(_)) {
ssh_debug_log(&format!("MOUSE via VT parser: {:?}", evt));
}
if tx.send(evt).is_err() { alive = false; }
});
}
} else {
key_vk_count += 1;
parser.cancel_escape();
let mods = vk_modifiers(key.control_key_state);
if let Some(code) = vk_to_keycode(key.virtual_key_code) {
let evt = make_key(code, mods);
if verbose {
ssh_debug_log(&format!(" → emit(vk): {:?}", evt));
}
if tx.send(evt).is_err() { alive = false; }
}
}
}
WINDOW_BUFFER_SIZE_EVENT => {
let w = unsafe {
&*(rec.data.as_ptr() as *const WINDOW_BUFFER_SIZE_RECORD)
};
ssh_debug_log(&format!("RESIZE {}x{}", w.size_x, w.size_y));
let _ = tx.send(Event::Resize(w.size_x as u16, w.size_y as u16));
}
MOUSE_EVENT => {
mouse_count += 1;
let m = unsafe {
&*(rec.data.as_ptr() as *const MOUSE_EVENT_RECORD)
};
ssh_debug_log(&format!(
"NATIVE MOUSE ({},{}) btn=0x{:X} flags=0x{:X}",
m.mouse_x, m.mouse_y, m.button_state, m.event_flags,
));
if let Some(evt) = convert_native_mouse(m) {
let _ = tx.send(evt);
}
}
other => {
if verbose {
ssh_debug_log(&format!("OTHER event_type={}", other));
}
}
}
if !alive { break; }
}
if parser.has_pending_escape() {
let peek_wait = unsafe { WaitForSingleObject(handle, ESC_TIMEOUT_MS) };
if peek_wait == WAIT_TIMEOUT {
parser.flush_escape(&mut |evt| {
if tx.send(evt).is_err() { alive = false; }
});
}
}
if !alive { break; }
}
})?;
Ok(rx)
}