Implement retained virtual terminal presentation

This commit is contained in:
ms2k 2026-08-03 22:51:05 +09:00
commit f468592d95

View file

@ -0,0 +1,123 @@
use std::time::Instant;
use lazaret_core::records::{DeveloperDecisionKind, TerminalResize};
use super::{PROMPT_ESCAPE_INTERVAL, PromptOutcome};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum PromptChoice {
Approve,
Deny,
}
pub(super) enum PromptInput {
Continue,
Decide(PromptChoice),
}
pub(super) struct PromptInputDecoder {
pub(super) choice: PromptChoice,
escape: Vec<u8>,
escape_deadline: Option<Instant>,
}
impl PromptInputDecoder {
pub(super) fn new() -> Self {
Self {
choice: PromptChoice::Deny,
escape: Vec::new(),
escape_deadline: None,
}
}
pub(super) fn feed(&mut self, bytes: &[u8], now: Instant) -> PromptInput {
if self.escape_deadline.is_some_and(|deadline| now >= deadline) {
self.clear_escape();
return PromptInput::Decide(PromptChoice::Deny);
}
for &byte in bytes {
if !self.escape.is_empty() || byte == 0x1b {
self.escape.push(byte);
self.escape_deadline = Some(now + PROMPT_ESCAPE_INTERVAL);
match self.escape.as_slice() {
b"\x1b[D" | b"\x1bOD" => {
self.choice = PromptChoice::Approve;
self.clear_escape();
}
b"\x1b[C" | b"\x1bOC" => {
self.choice = PromptChoice::Deny;
self.clear_escape();
}
sequence if supported_prompt_key_prefix(sequence) => {}
_ => return PromptInput::Decide(PromptChoice::Deny),
}
continue;
}
match byte {
b'y' | b'Y' => return PromptInput::Decide(PromptChoice::Approve),
b'n' | b'N' | 0x03 | 0x04 | 0x1a => {
return PromptInput::Decide(PromptChoice::Deny);
}
b'\r' | b'\n' => return PromptInput::Decide(self.choice),
_ => return PromptInput::Decide(PromptChoice::Deny),
}
}
PromptInput::Continue
}
pub(super) fn expire(&mut self, now: Instant) -> PromptInput {
if self.escape_deadline.is_some_and(|deadline| now >= deadline) {
self.clear_escape();
PromptInput::Decide(PromptChoice::Deny)
} else {
PromptInput::Continue
}
}
fn clear_escape(&mut self) {
self.escape.clear();
self.escape_deadline = None;
}
}
fn supported_prompt_key_prefix(bytes: &[u8]) -> bool {
[b"\x1b[D".as_slice(), b"\x1b[C", b"\x1bOD", b"\x1bOC"]
.iter()
.any(|sequence| sequence.starts_with(bytes))
}
pub(super) fn prompt_outcome(
choice: PromptChoice,
resumed_size: Option<TerminalResize>,
) -> PromptOutcome {
PromptOutcome {
decision: match choice {
PromptChoice::Approve => DeveloperDecisionKind::Approve,
PromptChoice::Deny => DeveloperDecisionKind::Deny,
},
resumed_size,
}
}
pub(super) fn display_argv(argv: &[Vec<u8>]) -> String {
let arguments = argv
.iter()
.map(|argument| format!("\"{}\"", display_bytes(argument)))
.collect::<Vec<_>>()
.join(", ");
format!("[{arguments}]")
}
pub(super) fn display_bytes(bytes: &[u8]) -> String {
bytes.iter().fold(String::new(), |mut output, &byte| {
use std::fmt::Write as _;
match byte {
b'\\' => output.push_str("\\\\"),
b'\"' => output.push_str("\\\""),
0x20..=0x7e => output.push(char::from(byte)),
_ => write!(output, "\\x{byte:02x}").expect("writing to a String cannot fail"),
}
output
})
}