Implement retained virtual terminal presentation
This commit is contained in:
parent
aef24440a5
commit
f468592d95
1 changed files with 811 additions and 71 deletions
123
crates/lazaret-client/src/terminal/prompt.rs
Normal file
123
crates/lazaret-client/src/terminal/prompt.rs
Normal 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
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue