mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
1 Commits
input-vali
...
dev/icewea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb86dbd80d |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -1428,6 +1428,7 @@ dependencies = [
|
||||
"unicode-width 0.2.1",
|
||||
"url",
|
||||
"vt100",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -186,6 +186,7 @@ uuid = "1"
|
||||
vt100 = "0.16.2"
|
||||
walkdir = "2.5.0"
|
||||
webbrowser = "1.0"
|
||||
windows-sys = "0.59.0"
|
||||
which = "6"
|
||||
wildmatch = "2.5.0"
|
||||
wiremock = "0.6"
|
||||
|
||||
@@ -87,6 +87,14 @@ url = { workspace = true }
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
# Clipboard support via `arboard` is not available on Android/Termux.
|
||||
# Only include it for non-Android targets so the crate builds on Android.
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#[cfg(any(all(unix, not(test)), all(windows, not(test))))]
|
||||
#[path = "terminal_palette/common.rs"]
|
||||
mod terminal_palette_common;
|
||||
|
||||
pub fn terminal_palette() -> Option<[(u8, u8, u8); 256]> {
|
||||
imp::terminal_palette()
|
||||
}
|
||||
@@ -22,6 +26,7 @@ pub fn default_fg() -> Option<(u8, u8, u8)> {
|
||||
default_colors().map(|c| c.fg)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn default_bg() -> Option<(u8, u8, u8)> {
|
||||
default_colors().map(|c| c.bg)
|
||||
}
|
||||
@@ -29,41 +34,14 @@ pub fn default_bg() -> Option<(u8, u8, u8)> {
|
||||
#[cfg(all(unix, not(test)))]
|
||||
mod imp {
|
||||
use super::DefaultColors;
|
||||
use super::terminal_palette_common::Cache;
|
||||
use super::terminal_palette_common::apply_palette_responses;
|
||||
use super::terminal_palette_common::parse_osc_color;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
struct Cache<T> {
|
||||
attempted: bool,
|
||||
value: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for Cache<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
attempted: false,
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Cache<T> {
|
||||
fn get_or_init_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
|
||||
if !self.attempted {
|
||||
self.value = init();
|
||||
self.attempted = true;
|
||||
}
|
||||
self.value
|
||||
}
|
||||
|
||||
fn refresh_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
|
||||
self.value = init();
|
||||
self.attempted = true;
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
fn default_colors_cache() -> &'static Mutex<Cache<DefaultColors>> {
|
||||
static CACHE: OnceLock<Mutex<Cache<DefaultColors>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Mutex::new(Cache::default()))
|
||||
@@ -302,134 +280,13 @@ mod imp {
|
||||
original: termios,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_palette_responses(
|
||||
buffer: &mut Vec<u8>,
|
||||
palette: &mut [Option<(u8, u8, u8)>; 256],
|
||||
) -> usize {
|
||||
let mut newly_filled = 0;
|
||||
|
||||
while let Some(start) = buffer.windows(2).position(|window| window == [0x1b, b']']) {
|
||||
if start > 0 {
|
||||
buffer.drain(..start);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut index = 2; // skip ESC ]
|
||||
let mut terminator_len = None;
|
||||
while index < buffer.len() {
|
||||
match buffer[index] {
|
||||
0x07 => {
|
||||
terminator_len = Some(1);
|
||||
break;
|
||||
}
|
||||
0x1b if index + 1 < buffer.len() && buffer[index + 1] == b'\\' => {
|
||||
terminator_len = Some(2);
|
||||
break;
|
||||
}
|
||||
_ => index += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let Some(terminator_len) = terminator_len else {
|
||||
break;
|
||||
};
|
||||
|
||||
let end = index;
|
||||
let parsed = std::str::from_utf8(&buffer[2..end])
|
||||
.ok()
|
||||
.and_then(parse_palette_message);
|
||||
let processed = end + terminator_len;
|
||||
buffer.drain(..processed);
|
||||
|
||||
if let Some((slot, color)) = parsed
|
||||
&& palette[slot].is_none()
|
||||
{
|
||||
palette[slot] = Some(color);
|
||||
newly_filled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
newly_filled
|
||||
}
|
||||
|
||||
fn parse_palette_message(message: &str) -> Option<(usize, (u8, u8, u8))> {
|
||||
let mut parts = message.splitn(3, ';');
|
||||
if parts.next()? != "4" {
|
||||
return None;
|
||||
}
|
||||
let index: usize = parts.next()?.trim().parse().ok()?;
|
||||
if index >= 256 {
|
||||
return None;
|
||||
}
|
||||
let payload = parts.next()?;
|
||||
let (model, values) = payload.split_once(':')?;
|
||||
if model != "rgb" && model != "rgba" {
|
||||
return None;
|
||||
}
|
||||
let mut components = values.split('/');
|
||||
let r = parse_component(components.next()?)?;
|
||||
let g = parse_component(components.next()?)?;
|
||||
let b = parse_component(components.next()?)?;
|
||||
Some((index, (r, g, b)))
|
||||
}
|
||||
|
||||
fn parse_component(component: &str) -> Option<u8> {
|
||||
let trimmed = component.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let bits = trimmed.len().checked_mul(4)?;
|
||||
if bits == 0 || bits > 64 {
|
||||
return None;
|
||||
}
|
||||
let max = if bits == 64 {
|
||||
u64::MAX
|
||||
} else {
|
||||
(1u64 << bits) - 1
|
||||
};
|
||||
let value = u64::from_str_radix(trimmed, 16).ok()?;
|
||||
Some(((value * 255 + max / 2) / max) as u8)
|
||||
}
|
||||
|
||||
fn parse_osc_color(buffer: &[u8], code: u8) -> Option<(u8, u8, u8)> {
|
||||
let text = std::str::from_utf8(buffer).ok()?;
|
||||
let prefix = match code {
|
||||
10 => "\u{1b}]10;",
|
||||
11 => "\u{1b}]11;",
|
||||
_ => return None,
|
||||
};
|
||||
let start = text.rfind(prefix)?;
|
||||
let after_prefix = &text[start + prefix.len()..];
|
||||
let end_bel = after_prefix.find('\u{7}');
|
||||
let end_st = after_prefix.find("\u{1b}\\");
|
||||
let end_idx = match (end_bel, end_st) {
|
||||
(Some(bel), Some(st)) => bel.min(st),
|
||||
(Some(bel), None) => bel,
|
||||
(None, Some(st)) => st,
|
||||
(None, None) => return None,
|
||||
};
|
||||
let payload = after_prefix[..end_idx].trim();
|
||||
parse_color_payload(payload)
|
||||
}
|
||||
|
||||
fn parse_color_payload(payload: &str) -> Option<(u8, u8, u8)> {
|
||||
if payload.is_empty() || payload == "?" {
|
||||
return None;
|
||||
}
|
||||
let (model, values) = payload.split_once(':')?;
|
||||
if model != "rgb" && model != "rgba" {
|
||||
return None;
|
||||
}
|
||||
let mut parts = values.split('/');
|
||||
let r = parse_component(parts.next()?)?;
|
||||
let g = parse_component(parts.next()?)?;
|
||||
let b = parse_component(parts.next()?)?;
|
||||
Some((r, g, b))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(unix, not(test))))]
|
||||
#[cfg(all(windows, not(test)))]
|
||||
#[path = "terminal_palette_windows.rs"]
|
||||
mod imp;
|
||||
|
||||
#[cfg(not(any(all(unix, not(test)), all(windows, not(test)))))]
|
||||
mod imp {
|
||||
use super::DefaultColors;
|
||||
|
||||
|
||||
156
codex-rs/tui/src/terminal_palette/common.rs
Normal file
156
codex-rs/tui/src/terminal_palette/common.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use std::str;
|
||||
|
||||
pub(crate) struct Cache<T> {
|
||||
attempted: bool,
|
||||
value: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for Cache<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
attempted: false,
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Cache<T> {
|
||||
pub(crate) fn get_or_init_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
|
||||
if !self.attempted {
|
||||
self.value = init();
|
||||
self.attempted = true;
|
||||
}
|
||||
self.value
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
|
||||
self.value = init();
|
||||
self.attempted = true;
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_palette_responses(
|
||||
buffer: &mut Vec<u8>,
|
||||
palette: &mut [Option<(u8, u8, u8)>; 256],
|
||||
) -> usize {
|
||||
let mut newly_filled = 0;
|
||||
|
||||
while let Some(start) = buffer.windows(2).position(|window| window == [0x1b, b']']) {
|
||||
if start > 0 {
|
||||
buffer.drain(..start);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut index = 2; // skip ESC ]
|
||||
let mut terminator_len = None;
|
||||
while index < buffer.len() {
|
||||
match buffer[index] {
|
||||
0x07 => {
|
||||
terminator_len = Some(1);
|
||||
break;
|
||||
}
|
||||
0x1b if index + 1 < buffer.len() && buffer[index + 1] == b'\\' => {
|
||||
terminator_len = Some(2);
|
||||
break;
|
||||
}
|
||||
_ => index += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let Some(terminator_len) = terminator_len else {
|
||||
break;
|
||||
};
|
||||
|
||||
let end = index;
|
||||
let parsed = str::from_utf8(&buffer[2..end])
|
||||
.ok()
|
||||
.and_then(parse_palette_message);
|
||||
let processed = end + terminator_len;
|
||||
buffer.drain(..processed);
|
||||
|
||||
if let Some((slot, color)) = parsed
|
||||
&& palette[slot].is_none()
|
||||
{
|
||||
palette[slot] = Some(color);
|
||||
newly_filled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
newly_filled
|
||||
}
|
||||
|
||||
pub(crate) fn parse_osc_color(buffer: &[u8], code: u8) -> Option<(u8, u8, u8)> {
|
||||
let text = str::from_utf8(buffer).ok()?;
|
||||
let prefix = match code {
|
||||
10 => "\u{1b}]10;",
|
||||
11 => "\u{1b}]11;",
|
||||
_ => return None,
|
||||
};
|
||||
let start = text.rfind(prefix)?;
|
||||
let after_prefix = &text[start + prefix.len()..];
|
||||
let end_bel = after_prefix.find('\u{7}');
|
||||
let end_st = after_prefix.find("\u{1b}\\");
|
||||
let end_idx = match (end_bel, end_st) {
|
||||
(Some(bel), Some(st)) => bel.min(st),
|
||||
(Some(bel), None) => bel,
|
||||
(None, Some(st)) => st,
|
||||
(None, None) => return None,
|
||||
};
|
||||
let payload = after_prefix[..end_idx].trim();
|
||||
parse_color_payload(payload)
|
||||
}
|
||||
|
||||
fn parse_palette_message(message: &str) -> Option<(usize, (u8, u8, u8))> {
|
||||
let mut parts = message.splitn(3, ';');
|
||||
if parts.next()? != "4" {
|
||||
return None;
|
||||
}
|
||||
let index: usize = parts.next()?.trim().parse().ok()?;
|
||||
if index >= 256 {
|
||||
return None;
|
||||
}
|
||||
let payload = parts.next()?;
|
||||
let (model, values) = payload.split_once(':')?;
|
||||
if model != "rgb" && model != "rgba" {
|
||||
return None;
|
||||
}
|
||||
let mut components = values.split('/');
|
||||
let r = parse_component(components.next()?)?;
|
||||
let g = parse_component(components.next()?)?;
|
||||
let b = parse_component(components.next()?)?;
|
||||
Some((index, (r, g, b)))
|
||||
}
|
||||
|
||||
fn parse_color_payload(payload: &str) -> Option<(u8, u8, u8)> {
|
||||
if payload.is_empty() || payload == "?" {
|
||||
return None;
|
||||
}
|
||||
let (model, values) = payload.split_once(':')?;
|
||||
if model != "rgb" && model != "rgba" {
|
||||
return None;
|
||||
}
|
||||
let mut parts = values.split('/');
|
||||
let r = parse_component(parts.next()?)?;
|
||||
let g = parse_component(parts.next()?)?;
|
||||
let b = parse_component(parts.next()?)?;
|
||||
Some((r, g, b))
|
||||
}
|
||||
|
||||
fn parse_component(component: &str) -> Option<u8> {
|
||||
let trimmed = component.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let bits = trimmed.len().checked_mul(4)?;
|
||||
if bits == 0 || bits > 64 {
|
||||
return None;
|
||||
}
|
||||
let max = if bits == 64 {
|
||||
u64::MAX
|
||||
} else {
|
||||
(1u64 << bits) - 1
|
||||
};
|
||||
let value = u64::from_str_radix(trimmed, 16).ok()?;
|
||||
Some(((value * 255 + max / 2) / max) as u8)
|
||||
}
|
||||
328
codex-rs/tui/src/terminal_palette_windows.rs
Normal file
328
codex-rs/tui/src/terminal_palette_windows.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
use super::DefaultColors;
|
||||
use super::terminal_palette_common::Cache;
|
||||
use super::terminal_palette_common::apply_palette_responses;
|
||||
use super::terminal_palette_common::parse_osc_color;
|
||||
use std::env;
|
||||
use std::ffi::c_void;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::IsTerminal;
|
||||
use std::io::Write;
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
|
||||
use windows_sys::Win32::Storage::FileSystem::ReadFile;
|
||||
use windows_sys::Win32::System::Console::ENABLE_ECHO_INPUT;
|
||||
use windows_sys::Win32::System::Console::ENABLE_LINE_INPUT;
|
||||
use windows_sys::Win32::System::Console::ENABLE_PROCESSED_INPUT;
|
||||
use windows_sys::Win32::System::Console::ENABLE_PROCESSED_OUTPUT;
|
||||
use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_INPUT;
|
||||
use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
use windows_sys::Win32::System::Console::GetConsoleMode;
|
||||
use windows_sys::Win32::System::Console::GetStdHandle;
|
||||
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
|
||||
use windows_sys::Win32::System::Console::SetConsoleMode;
|
||||
use windows_sys::Win32::System::Threading::WAIT_FAILED;
|
||||
use windows_sys::Win32::System::Threading::WAIT_OBJECT_0;
|
||||
use windows_sys::Win32::System::Threading::WAIT_TIMEOUT;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
|
||||
const RESPONSE_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||
|
||||
pub(super) fn terminal_palette() -> Option<[(u8, u8, u8); 256]> {
|
||||
static CACHE: OnceLock<Option<[(u8, u8, u8); 256]>> = OnceLock::new();
|
||||
*CACHE.get_or_init(|| match query_terminal_palette() {
|
||||
Ok(Some(palette)) => Some(palette),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn default_colors() -> Option<DefaultColors> {
|
||||
let cache = default_colors_cache();
|
||||
let mut cache = cache.lock().ok()?;
|
||||
cache.get_or_init_with(|| query_default_colors().unwrap_or_default())
|
||||
}
|
||||
|
||||
pub(super) fn requery_default_colors() {
|
||||
if let Ok(mut cache) = default_colors_cache().lock() {
|
||||
cache.refresh_with(|| query_default_colors().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
fn default_colors_cache() -> &'static Mutex<Cache<DefaultColors>> {
|
||||
static CACHE: OnceLock<Mutex<Cache<DefaultColors>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Mutex::new(Cache::default()))
|
||||
}
|
||||
|
||||
fn is_windows_terminal() -> bool {
|
||||
env::var("WT_SESSION")
|
||||
.map(|value| !value.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn query_terminal_palette() -> io::Result<Option<[(u8, u8, u8); 256]>> {
|
||||
if !is_windows_terminal() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut stdout_handle = io::stdout();
|
||||
if !stdout_handle.is_terminal() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let _output_guard = ConsoleOutputGuard::acquire();
|
||||
for index in 0..256 {
|
||||
write!(stdout_handle, "\x1b]4;{index};?\x07")?;
|
||||
}
|
||||
stdout_handle.flush()?;
|
||||
|
||||
let mut reader = match ConsoleInputReader::new() {
|
||||
Ok(reader) => reader,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
let mut palette: [Option<(u8, u8, u8)>; 256] = [None; 256];
|
||||
let mut buffer = Vec::new();
|
||||
let mut remaining = palette.len();
|
||||
let deadline = Instant::now() + RESPONSE_TIMEOUT;
|
||||
|
||||
while remaining > 0 && Instant::now() < deadline {
|
||||
if !reader.read_available(&mut buffer, Duration::from_millis(25))? {
|
||||
continue;
|
||||
}
|
||||
let newly = apply_palette_responses(&mut buffer, &mut palette);
|
||||
if newly == 0 {
|
||||
continue;
|
||||
}
|
||||
remaining = remaining.saturating_sub(newly);
|
||||
}
|
||||
|
||||
if remaining > 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut colors = [(0, 0, 0); 256];
|
||||
for (slot, value) in colors.iter_mut().zip(palette.into_iter()) {
|
||||
if let Some(rgb) = value {
|
||||
*slot = rgb;
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(colors))
|
||||
}
|
||||
|
||||
fn query_default_colors() -> io::Result<Option<DefaultColors>> {
|
||||
if !is_windows_terminal() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut stdout_handle = io::stdout();
|
||||
if !stdout_handle.is_terminal() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let _output_guard = ConsoleOutputGuard::acquire();
|
||||
stdout_handle.write_all(b"\x1b]10;?\x07\x1b]11;?\x07")?;
|
||||
stdout_handle.flush()?;
|
||||
|
||||
let mut reader = match ConsoleInputReader::new() {
|
||||
Ok(reader) => reader,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut fg = None;
|
||||
let mut bg = None;
|
||||
let deadline = Instant::now() + Duration::from_millis(250);
|
||||
|
||||
while Instant::now() < deadline {
|
||||
reader.read_available(&mut buffer, Duration::from_millis(20))?;
|
||||
if fg.is_none() {
|
||||
fg = parse_osc_color(&buffer, 10);
|
||||
}
|
||||
if bg.is_none() {
|
||||
bg = parse_osc_color(&buffer, 11);
|
||||
}
|
||||
if fg.is_some() && bg.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if fg.is_none() {
|
||||
fg = parse_osc_color(&buffer, 10);
|
||||
}
|
||||
if bg.is_none() {
|
||||
bg = parse_osc_color(&buffer, 11);
|
||||
}
|
||||
|
||||
Ok(fg.zip(bg).map(|(fg, bg)| DefaultColors { fg, bg }))
|
||||
}
|
||||
|
||||
struct ConsoleOutputGuard {
|
||||
handle: HANDLE,
|
||||
original_mode: Option<u32>,
|
||||
}
|
||||
|
||||
impl ConsoleOutputGuard {
|
||||
fn acquire() -> Option<Self> {
|
||||
unsafe {
|
||||
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if handle == INVALID_HANDLE_VALUE || handle == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut original = 0u32;
|
||||
if GetConsoleMode(handle, &mut original) == 0 {
|
||||
return None;
|
||||
}
|
||||
let desired = original | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
if desired == original {
|
||||
return Some(Self {
|
||||
handle,
|
||||
original_mode: Some(original),
|
||||
});
|
||||
}
|
||||
if SetConsoleMode(handle, desired) == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
handle,
|
||||
original_mode: Some(original),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConsoleOutputGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(original) = self.original_mode {
|
||||
unsafe {
|
||||
let _ = SetConsoleMode(self.handle, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleInputReader {
|
||||
_file: std::fs::File,
|
||||
handle: HANDLE,
|
||||
_guard: ConsoleInputModeGuard,
|
||||
}
|
||||
|
||||
impl ConsoleInputReader {
|
||||
fn new() -> io::Result<Self> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true);
|
||||
options.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE);
|
||||
let file = options.open("CONIN$")?;
|
||||
let handle = file.as_raw_handle() as HANDLE;
|
||||
if handle == INVALID_HANDLE_VALUE || handle == 0 {
|
||||
return Err(io::Error::other("invalid console handle"));
|
||||
}
|
||||
let guard = ConsoleInputModeGuard::acquire(handle)?;
|
||||
Ok(Self {
|
||||
_file: file,
|
||||
handle,
|
||||
_guard: guard,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_available(&mut self, buffer: &mut Vec<u8>, wait: Duration) -> io::Result<bool> {
|
||||
let mut any = false;
|
||||
let mut current_wait = duration_to_millis(wait);
|
||||
loop {
|
||||
let status = unsafe { WaitForSingleObject(self.handle, current_wait) };
|
||||
match status {
|
||||
WAIT_OBJECT_0 => {
|
||||
if self.read_once(buffer)? {
|
||||
any = true;
|
||||
}
|
||||
current_wait = 0;
|
||||
continue;
|
||||
}
|
||||
WAIT_TIMEOUT => {
|
||||
break;
|
||||
}
|
||||
WAIT_FAILED => {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
Ok(any)
|
||||
}
|
||||
|
||||
fn read_once(&mut self, buffer: &mut Vec<u8>) -> io::Result<bool> {
|
||||
let mut chunk = [0u8; 512];
|
||||
let mut read = 0u32;
|
||||
let success = unsafe {
|
||||
ReadFile(
|
||||
self.handle,
|
||||
chunk.as_mut_ptr() as *mut c_void,
|
||||
chunk.len() as u32,
|
||||
&mut read,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if success == 0 {
|
||||
let err = io::Error::last_os_error();
|
||||
if err.kind() == io::ErrorKind::Interrupted || err.kind() == io::ErrorKind::WouldBlock {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
if read == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
buffer.extend_from_slice(&chunk[..read as usize]);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleInputModeGuard {
|
||||
handle: HANDLE,
|
||||
original_mode: u32,
|
||||
}
|
||||
|
||||
impl ConsoleInputModeGuard {
|
||||
fn acquire(handle: HANDLE) -> io::Result<Self> {
|
||||
unsafe {
|
||||
let mut original = 0u32;
|
||||
if GetConsoleMode(handle, &mut original) == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let mut desired = original;
|
||||
desired |= ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_PROCESSED_INPUT;
|
||||
desired &= !ENABLE_LINE_INPUT;
|
||||
desired &= !ENABLE_ECHO_INPUT;
|
||||
if desired != original && SetConsoleMode(handle, desired) == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(Self {
|
||||
handle,
|
||||
original_mode: original,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConsoleInputModeGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = SetConsoleMode(self.handle, self.original_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_to_millis(duration: Duration) -> u32 {
|
||||
duration.as_millis().try_into().unwrap_or(u32::MAX)
|
||||
}
|
||||
Reference in New Issue
Block a user