diff --git a/.vscode/launch.json b/.vscode/launch.json index f907882..5f03c9a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,9 +4,8 @@ { "name": "Launch Debug", "preLaunchTask": "rust: cargo build", - "type": "cppvsdbg", + "type": "cppdbg", "request": "launch", - "console": "externalTerminal", "program": "${workspaceFolder}/target/debug/edit", "args": [ "${workspaceFolder}/README.md" diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 8013a6f..f35101b 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -8,22 +8,27 @@ use std::ptr::{null, null_mut}; use std::thread; use std::time; -pub fn preferred_languages() -> Vec { - let mut locales = Vec::new(); - - for key in ["LANGUAGE", "LC_ALL", "LANG"] { - if let Ok(val) = std::env::var(key) { - locales.extend( - val.split(':') - .filter(|val| !val.is_empty()) - .map(String::from), - ); - } - } - - locales +struct State { + stdin: libc::c_int, + stdin_flags: libc::c_int, + stdout: libc::c_int, + stdout_initial_termios: libc::termios, + inject_resize: bool, + // Buffer for incomplete UTF-8 sequences (max 4 bytes needed) + utf8_buf: [u8; 4], + utf8_len: usize, } +static mut STATE: State = State { + stdin: libc::STDIN_FILENO, + stdin_flags: 0, + stdout: libc::STDOUT_FILENO, + stdout_initial_termios: unsafe { mem::zeroed() }, + inject_resize: false, + utf8_buf: [0; 4], + utf8_len: 0, +}; + extern "C" fn sigwinch_handler(_: libc::c_int) { unsafe { STATE.inject_resize = true; @@ -40,6 +45,8 @@ pub fn init() -> apperr::Result<()> { STATE.stdout = check_int_return(libc::open(c"/dev/tty".as_ptr(), libc::O_WRONLY))?; } + STATE.stdin_flags = check_int_return(libc::fcntl(STATE.stdin, libc::F_GETFL))?; + check_int_return(libc::tcgetattr( STATE.stdout, &raw mut STATE.stdout_initial_termios, @@ -108,25 +115,6 @@ fn get_window_size() -> (u16, u16) { (w, h) } -struct State { - stdin: libc::c_int, - stdout: libc::c_int, - stdout_initial_termios: libc::termios, - inject_resize: bool, - // Buffer for incomplete UTF-8 sequences (max 4 bytes needed) - utf8_buf: [u8; 4], - utf8_len: usize, -} - -static mut STATE: State = State { - stdin: libc::STDIN_FILENO, - stdout: libc::STDOUT_FILENO, - stdout_initial_termios: unsafe { mem::zeroed() }, - inject_resize: false, - utf8_buf: [0; 4], - utf8_len: 0, -}; - /// Reads from stdin. /// /// Returns `None` if there was an error reading from stdin. @@ -134,6 +122,13 @@ static mut STATE: State = State { /// Otherwise, it returns the read, non-empty string. pub fn read_stdin(timeout: time::Duration) -> Option { unsafe { + let mut read_more = true; + let mut read_poll = false; + + if STATE.inject_resize { + read_poll = true; + } + if timeout != time::Duration::MAX { let mut pollfd = libc::pollfd { fd: STATE.stdin, @@ -148,62 +143,78 @@ pub fn read_stdin(timeout: time::Duration) -> Option { if ret < 0 { return None; } + + read_poll = true; // there is a timeout -> don't block in read() if ret == 0 { - return Some(String::new()); + read_more = false; // if timeout -> don't read } + }; + + let mut input = String::new(); + + if read_more { + let mut buf = Vec::with_capacity(1024); + + if STATE.utf8_len != 0 { + buf.extend_from_slice(&STATE.utf8_buf[..STATE.utf8_len]); + STATE.utf8_len = 0; + } + + let is_nonblock = (STATE.stdin_flags & libc::O_NONBLOCK) != 0; + if read_poll != is_nonblock { + STATE.stdin_flags ^= libc::O_NONBLOCK; + let _ = libc::fcntl(STATE.stdin, libc::F_SETFL, STATE.stdin_flags); + } + + loop { + let spare = buf.spare_capacity_mut(); + let ret = libc::read(STATE.stdin, spare.as_mut_ptr() as *mut _, spare.len()); + if ret > 0 { + buf.set_len(buf.len() + ret as usize); + break; + } + if ret == 0 { + return None; + } + match *libc::__errno_location() { + libc::EINTR => continue, + libc::EAGAIN => break, + _ => return None, + } + } + + if !buf.is_empty() { + // We only need to check the last 3 bytes for UTF-8 continuation bytes, + // because we should be able to assume that any 4 byte sequence is complete. + let lim = buf.len().saturating_sub(3); + let mut off = buf.len() - 1; + + // Find the start of the last potentially incomplete UTF-8 sequence. + while off > lim && buf[off] & 0b1100_0000 == 0b1000_0000 { + off -= 1; + } + + let seq_len = match buf[off] { + b if b & 0b1000_0000 == 0 => 1, + b if b & 0b1110_0000 == 0b1100_0000 => 2, + b if b & 0b1111_0000 == 0b1110_0000 => 3, + b if b & 0b1111_1000 == 0b1111_0000 => 4, + // If the lead byte we found isn't actually one, we don't cache it. + // `string_from_utf8_lossy_owned` will replace it with U+FFFD. + _ => 0, + }; + + // Cache incomplete sequence if any. + if off + seq_len > buf.len() { + STATE.utf8_len = buf.len() - off; + STATE.utf8_buf[..STATE.utf8_len].copy_from_slice(&buf[off..]); + buf.truncate(off); + } + } + + input = helpers::string_from_utf8_lossy_owned(buf); } - let mut buf = Vec::with_capacity(1024); - - if STATE.utf8_len != 0 { - buf.extend_from_slice(&STATE.utf8_buf[..STATE.utf8_len]); - STATE.utf8_len = 0; - } - - // Read new data - loop { - let spare = buf.spare_capacity_mut(); - let ret = libc::read(STATE.stdin, spare.as_mut_ptr() as *mut _, spare.len()); - if ret > 0 { - buf.set_len(buf.len() + ret as usize); - break; - } - if ret == 0 || *libc::__errno_location() != libc::EINTR { - return None; - } - } - - if !buf.is_empty() { - // We only need to check the last 3 bytes for UTF-8 continuation bytes, - // because we should be able to assume that any 4 byte sequence is complete. - let lim = buf.len().saturating_sub(3); - let mut off = buf.len() - 1; - - // Find the start of the last potentially incomplete UTF-8 sequence. - while off > lim && buf[off] & 0b1100_0000 == 0b1000_0000 { - off -= 1; - } - - let seq_len = match buf[off] { - b if b & 0b1000_0000 == 0 => 1, - b if b & 0b1110_0000 == 0b1100_0000 => 2, - b if b & 0b1111_0000 == 0b1110_0000 => 3, - b if b & 0b1111_1000 == 0b1111_0000 => 4, - // If the lead byte we found isn't actually one, we don't cache it. - // `string_from_utf8_lossy_owned` will replace it with U+FFFD. - _ => 0, - }; - - // Cache incomplete sequence if any. - if off + seq_len > buf.len() { - STATE.utf8_len = buf.len() - off; - STATE.utf8_buf[..STATE.utf8_len].copy_from_slice(&buf[off..]); - buf.truncate(off); - } - } - - let mut input = helpers::string_from_utf8_lossy_owned(buf); - if STATE.inject_resize { STATE.inject_resize = false; let (w, h) = get_window_size(); @@ -310,6 +321,22 @@ pub unsafe fn load_icu() -> apperr::Result<*mut c_void> { unsafe { load_library(c"icu.dll") } } +pub fn preferred_languages() -> Vec { + let mut locales = Vec::new(); + + for key in ["LANGUAGE", "LC_ALL", "LANG"] { + if let Ok(val) = std::env::var(key) { + locales.extend( + val.split(':') + .filter(|val| !val.is_empty()) + .map(String::from), + ); + } + } + + locales +} + #[inline] pub fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { unsafe { apperr::Error::new(err.raw_os_error().unwrap_or(0) as u32) } diff --git a/src/sys/windows.rs b/src/sys/windows.rs index cc69dc4..64e726a 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -19,51 +19,6 @@ use windows_sys::Win32::System::Memory; use windows_sys::Win32::System::Threading; use windows_sys::w; -pub fn preferred_languages() -> Vec { - unsafe { - const LEN: usize = 256; - - let mut lang_num = 0; - let mut lang_buf = [const { MaybeUninit::::uninit() }; LEN]; - let mut lang_buf_len = lang_buf.len() as u32; - if Globalization::GetUserPreferredUILanguages( - Globalization::MUI_LANGUAGE_NAME, - &mut lang_num, - lang_buf[0].as_mut_ptr(), - &mut lang_buf_len, - ) == 0 - || lang_num == 0 - { - return Vec::new(); - } - - // Drop the terminating double-null character. - lang_buf_len = lang_buf_len.saturating_sub(1); - - let mut lang_buf_utf8 = [const { MaybeUninit::::uninit() }; 3 * LEN]; - let lang_buf_utf8_len = Globalization::WideCharToMultiByte( - Globalization::CP_UTF8, - 0, - lang_buf[0].as_mut_ptr(), - lang_buf_len as i32, - lang_buf_utf8[0].as_mut_ptr(), - lang_buf_utf8.len() as i32, - null(), - null_mut(), - ); - if lang_buf_utf8_len == 0 { - return Vec::new(); - } - - let result = helpers::str_from_raw_parts_mut( - lang_buf_utf8[0].as_mut_ptr(), - lang_buf_utf8_len as usize, - ); - result.make_ascii_lowercase(); - result.split_terminator('\0').map(String::from).collect() - } -} - type ReadConsoleInputExW = unsafe extern "system" fn( h_console_input: Foundation::HANDLE, lp_buffer: *mut Console::INPUT_RECORD, @@ -72,6 +27,16 @@ type ReadConsoleInputExW = unsafe extern "system" fn( w_flags: u16, ) -> Foundation::BOOL; +unsafe extern "system" fn read_console_input_ex_placeholder( + _: Foundation::HANDLE, + _: *mut Console::INPUT_RECORD, + _: u32, + _: *mut u32, + _: u16, +) -> Foundation::BOOL { + panic!(); +} + const CONSOLE_READ_NOWAIT: u16 = 0x0002; struct State { @@ -100,16 +65,6 @@ static mut STATE: State = State { wants_exit: false, }; -unsafe extern "system" fn read_console_input_ex_placeholder( - _: Foundation::HANDLE, - _: *mut Console::INPUT_RECORD, - _: u32, - _: *mut u32, - _: u16, -) -> Foundation::BOOL { - panic!(); -} - extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL { unsafe { STATE.wants_exit = true; @@ -235,13 +190,6 @@ pub fn read_stdin(timeout: time::Duration) -> Option { unsafe { STATE.inject_resize = false }; } - if unsafe { STATE.leading_surrogate } != 0 { - utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate }); - utf16_buf_len = 1; - input_buf_cap -= 1; - unsafe { STATE.leading_surrogate = 0 }; - } - if timeout != time::Duration::MAX { read_poll = true; let wait_result = @@ -260,6 +208,13 @@ pub fn read_stdin(timeout: time::Duration) -> Option { // This loops exists, just in case there's events in the input buffer that we aren't interested in. // It should be rare for this to loop. while read_more { + if unsafe { STATE.leading_surrogate } != 0 { + utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate }); + utf16_buf_len = 1; + input_buf_cap -= 1; + unsafe { STATE.leading_surrogate = 0 }; + } + let input = unsafe { // If we had a `inject_resize`, we don't want to block indefinitely for other pending input on startup, // but are still interested in any other pending input that may be waiting for us. @@ -476,6 +431,51 @@ pub unsafe fn load_icu() -> apperr::Result { unsafe { load_library(w!("icu.dll")) } } +pub fn preferred_languages() -> Vec { + unsafe { + const LEN: usize = 256; + + let mut lang_num = 0; + let mut lang_buf = [const { MaybeUninit::::uninit() }; LEN]; + let mut lang_buf_len = lang_buf.len() as u32; + if Globalization::GetUserPreferredUILanguages( + Globalization::MUI_LANGUAGE_NAME, + &mut lang_num, + lang_buf[0].as_mut_ptr(), + &mut lang_buf_len, + ) == 0 + || lang_num == 0 + { + return Vec::new(); + } + + // Drop the terminating double-null character. + lang_buf_len = lang_buf_len.saturating_sub(1); + + let mut lang_buf_utf8 = [const { MaybeUninit::::uninit() }; 3 * LEN]; + let lang_buf_utf8_len = Globalization::WideCharToMultiByte( + Globalization::CP_UTF8, + 0, + lang_buf[0].as_mut_ptr(), + lang_buf_len as i32, + lang_buf_utf8[0].as_mut_ptr(), + lang_buf_utf8.len() as i32, + null(), + null_mut(), + ); + if lang_buf_utf8_len == 0 { + return Vec::new(); + } + + let result = helpers::str_from_raw_parts_mut( + lang_buf_utf8[0].as_mut_ptr(), + lang_buf_utf8_len as usize, + ); + result.make_ascii_lowercase(); + result.split_terminator('\0').map(String::from).collect() + } +} + #[cold] fn get_last_error() -> apperr::Error { unsafe { gle_to_apperr(Foundation::GetLastError()) }