mod ffi;
mod frame;
mod input;
#[cfg(test)]
mod tests;
use crate::model::*;
use crate::projection::Projection;
use crate::Error;
use std::cell::RefCell;
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::Arc;
#[derive(Default)]
struct Events {
reply: Vec<u8>,
title: bool,
directory: bool,
bell: bool,
clipboard_denied: bool,
overflow: bool,
}
#[derive(Default)]
pub(crate) struct Emission {
pub reply: Vec<u8>,
pub effects: Vec<Effect>,
}
pub(crate) struct Vt {
handle: NonNull<c_void>,
events: Box<RefCell<Events>>,
session: SessionId,
geometry: Geometry,
revision: u64,
projection: Projection,
screen: Vec<Arc<Row>>,
palette: Option<Arc<Palette>>,
native_history: usize,
alternate: bool,
rebuild: bool,
history_limited: bool,
}
impl Vt {
pub fn new(session: SessionId, geometry: Geometry) -> Result<Self, Error> {
if !geometry.valid() {
return Err(Error::Capacity("invalid terminal geometry"));
}
let events = Box::new(RefCell::new(Events::default()));
let mut handle = std::ptr::null_mut();
let result = unsafe {
ffi::strop_vt_new(
geometry.columns,
geometry.rows,
MAX_HISTORY_LINES,
MAX_HISTORY_BYTES,
(&*events as *const RefCell<Events>).cast_mut().cast(),
event,
&mut handle,
)
};
check("create", result)?;
let handle = NonNull::new(handle)
.ok_or_else(|| Error::Protocol("native constructor returned no terminal".into()))?;
Ok(Self {
handle,
events,
session,
geometry,
revision: 0,
projection: Projection::default(),
screen: Vec::new(),
palette: None,
native_history: 0,
alternate: false,
rebuild: true,
history_limited: false,
})
}
pub fn feed(&mut self, bytes: &[u8]) -> Result<Emission, Error> {
if bytes.len() > 64 * 1024 {
return Err(Error::Capacity("VT input quantum"));
}
check("feed", unsafe {
ffi::strop_vt_feed(self.handle.as_ptr(), bytes.as_ptr(), bytes.len())
})?;
self.take_events()
}
pub fn resize(&mut self, geometry: Geometry) -> Result<(), Error> {
if !geometry.valid() {
return Err(Error::Capacity("invalid terminal geometry"));
}
if geometry == self.geometry {
return Ok(());
}
check("resize", unsafe {
ffi::strop_vt_resize(self.handle.as_ptr(), geometry.columns, geometry.rows)
})?;
self.geometry = geometry;
self.rebuild = true;
Ok(())
}
fn take_events(&mut self) -> Result<Emission, Error> {
let mut events = self.events.borrow_mut();
if events.overflow {
return Err(Error::State("callback data exceeded its response budget"));
}
let reply = std::mem::take(&mut events.reply);
let title = std::mem::take(&mut events.title);
let directory = std::mem::take(&mut events.directory);
let bell = std::mem::take(&mut events.bell);
let clipboard = std::mem::take(&mut events.clipboard_denied);
drop(events);
let mut effects = Vec::new();
if title {
let bytes = self.metadata(1)?;
let text =
strop_core::layout::printable_text(String::from_utf8_lossy(&bytes)).into_owned();
effects.push(Effect::Title(text));
}
if directory {
effects.push(Effect::ReportedDirectory(self.metadata(2)?));
}
if bell {
effects.push(Effect::Bell);
}
if clipboard {
effects.push(Effect::ClipboardWriteDenied);
}
Ok(Emission { reply, effects })
}
fn metadata(&self, kind: u32) -> Result<Vec<u8>, Error> {
let mut bytes = vec![0; 4096];
let mut written = 0;
check("metadata", unsafe {
ffi::strop_vt_text(
self.handle.as_ptr(),
kind,
bytes.as_mut_ptr(),
bytes.len(),
&mut written,
)
})?;
if written > bytes.len() {
return Err(Error::Capacity("terminal metadata"));
}
bytes.truncate(written);
Ok(bytes)
}
}
impl Drop for Vt {
fn drop(&mut self) {
unsafe { ffi::strop_vt_free(self.handle.as_ptr()) };
}
}
fn check(operation: &'static str, code: i32) -> Result<(), Error> {
match code {
0 => Ok(()),
-900 | -901 => Err(Error::Native {
operation: "allocation budget",
code,
}),
_ => Err(Error::Native { operation, code }),
}
}
unsafe extern "C" fn event(userdata: *mut c_void, kind: u32, bytes: *const u8, len: usize) {
let state = unsafe { &*(userdata as *const RefCell<Events>) };
let mut state = state.borrow_mut();
match kind {
0 => {
if len > MAX_INPUT_BYTES.saturating_sub(state.reply.len())
|| (len != 0 && bytes.is_null())
{
state.overflow = true;
return;
}
if len != 0 {
state
.reply
.extend_from_slice(unsafe { std::slice::from_raw_parts(bytes, len) });
}
}
1 => state.title = true,
2 => state.directory = true,
3 => state.bell = true,
4 => state.clipboard_denied = true,
_ => state.overflow = true,
}
}