This commit is contained in:
Aram Drevekenin 2020-09-03 16:47:44 +02:00
parent 68a8422457
commit 9e204e0dcc
15 changed files with 674 additions and 47 deletions

127
Cargo.lock generated
View file

@ -118,6 +118,20 @@ dependencies = [
"cache-padded",
]
[[package]]
name = "console"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c0994e656bba7b922d8dd1245db90672ffb701e684e45be58f20719d69abc5a"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"terminal_size",
"termios",
"winapi",
]
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
@ -129,6 +143,24 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "dtoa"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "event-listener"
version = "2.3.0"
@ -283,6 +315,26 @@ dependencies = [
"libc",
]
[[package]]
name = "insta"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617e921abc813f96a3b00958c079e7bf1e2db998f8a04f1546dd967373a418ee"
dependencies = [
"console",
"difference",
"lazy_static",
"serde",
"serde_json",
"serde_yaml",
]
[[package]]
name = "itoa"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "js-sys"
version = "0.3.44"
@ -313,6 +365,12 @@ version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
[[package]]
name = "linked-hash-map"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
[[package]]
name = "log"
version = "0.4.11"
@ -334,6 +392,7 @@ version = "0.1.0"
dependencies = [
"async-std",
"futures",
"insta",
"libc",
"nix",
"signal-hook",
@ -446,6 +505,12 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "scoped-tls"
version = "1.0.0"
@ -458,6 +523,49 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0"
[[package]]
name = "serde"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5"
dependencies = [
"dtoa",
"linked-hash-map",
"serde",
"yaml-rust",
]
[[package]]
name = "signal-hook"
version = "0.1.16"
@ -528,6 +636,16 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "terminal_size"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a14cd9f8c72704232f0bfc8455c0e861f0ad4eb60cc9ec8a170e231414c1e13"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "termios"
version = "0.3.2"
@ -703,3 +821,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "yaml-rust"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d"
dependencies = [
"linked-hash-map",
]

View file

@ -19,3 +19,6 @@ futures = "0.3.5"
[dependencies.async-std]
version = "1.3.0"
features = ["unstable"]
[dev-dependencies]
insta = "0.16.1"

View file

@ -1,3 +1,6 @@
#[cfg(test)]
mod tests;
mod os_input_output;
use ::std::fmt::{self, Display, Formatter};
@ -14,7 +17,9 @@ use async_std::task::*;
use ::std::pin::*;
use std::sync::mpsc::{channel, Sender, Receiver};
use crate::os_input_output::{get_os_input, OsInputOutput, OsApi};
use crate::os_input_output::{get_os_input, OsApi};
use vte::Perform;
struct ReadFromPid {
pid: RawFd,
@ -32,16 +37,19 @@ impl ReadFromPid {
impl Stream for ReadFromPid {
type Item = Vec<u8>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut read_buffer = [0; 115200];
let read_result = &self.os_input.read(self.pid, &mut read_buffer);
let pid = self.pid;
let read_result = &self.os_input.read_from_tty_stdout(pid, &mut read_buffer);
match read_result {
Ok(res) => {
// TODO: this might become an issue with multiple panes sending data simultaneously
// ...consider returning None if res == 0 and handling it in the task (or sending
// Poll::Pending?)
if *res == 0 {
// indicates end of file
return Poll::Ready(None);
} else {
let res = Some(read_buffer[..=*res].to_vec());
return Poll::Ready(res)
return Poll::Ready(res);
}
},
Err(e) => {
match e {
@ -59,8 +67,8 @@ impl Stream for ReadFromPid {
}
}
#[derive(Clone, Debug)]
struct TerminalCharacter {
#[derive(Clone)]
pub struct TerminalCharacter {
pub character: char,
pub ansi_code: Option<String>,
}
@ -106,7 +114,14 @@ impl Display for TerminalCharacter {
}
}
struct TerminalOutput {
impl ::std::fmt::Debug for TerminalCharacter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.character);
Ok(())
}
}
pub struct TerminalOutput {
pub pid: RawFd,
pub characters: Vec<TerminalCharacter>,
pub display_rows: u16,
@ -155,7 +170,8 @@ impl TerminalOutput {
self.unhook();
},
VteEvent::OscDispatch(params, bell_terminated) => {
self.osc_dispatch(params, bell_terminated);
let params: Vec<&[u8]> = params.iter().map(|p| &p[..]).collect();
self.osc_dispatch(&params[..], bell_terminated);
},
VteEvent::CsiDispatch(params, intermediates, ignore, c) => {
self.csi_dispatch(&params, &intermediates, ignore, c);
@ -337,6 +353,14 @@ impl TerminalOutput {
(None, None) => 0
}
}
fn index_of_beginning_of_last_canonical_line (&self) -> usize {
if self.newline_indices.is_empty() {
0
} else {
// return last
*self.newline_indices.last().unwrap()
}
}
fn index_of_beginning_of_line (&self, index_in_line: usize) -> usize {
let last_newline_index = if self.newline_indices.is_empty() {
None
@ -404,8 +428,7 @@ impl TerminalOutput {
const DEBUGGING: bool = false;
// vte methods
impl TerminalOutput {
impl vte::Perform for TerminalOutput {
fn print(&mut self, c: char) {
if DEBUGGING {
println!("\r[print] {:?}", c);
@ -424,14 +447,15 @@ impl TerminalOutput {
self.characters.push(space_character.clone());
};
self.characters.push(terminal_character);
}
let start_of_last_line = self.index_of_beginning_of_line(self.cursor_position);
let difference_from_last_newline = self.cursor_position - start_of_last_line;
if difference_from_last_newline == self.display_cols as usize {
self.linebreak_indices.push(self.cursor_position);
}
}
self.cursor_position += 1;
}
}
@ -478,9 +502,8 @@ impl TerminalOutput {
}
}
// fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
// TODO: normalize vec/slices for all of these methods and the enum
fn osc_dispatch(&mut self, params: Vec<Vec<u8>>, bell_terminated: bool) {
if DEBUGGING {
println!("\r[osc_dispatch] params={:?} bell_terminated={}", params, bell_terminated);
}
@ -503,7 +526,7 @@ impl TerminalOutput {
let param_string = params.iter().map(|p| p.to_string()).collect::<Vec<String>>().join(";");
self.pending_ansi_code = Some(format!("\u{1b}[{}m", param_string));
}
} else if c == 'C' { // move cursor
} else if c == 'C' { // move cursor forward
self.cursor_position += params[0] as usize; // TODO: negative value?
} else if c == 'K' { // clear line (0 => right, 1 => left, 2 => all)
if params[0] == 0 {
@ -527,6 +550,25 @@ impl TerminalOutput {
self.characters.truncate(self.cursor_position + 1);
}
// TODO: implement 1, 2, and 3
} else if c == 'H' { // goto row/col
let row = params[0] as usize - 1; // we subtract 1 here because this csi is 1 indexed and we index from 0
let col = params[1] as usize - 1;
match self.newline_indices.get(row as usize) {
Some(index_of_next_row) => {
let index_of_row = index_of_next_row - self.display_cols as usize;
self.cursor_position = index_of_row + col as usize;
}
None => {
let start_of_last_line = self.index_of_beginning_of_last_canonical_line();
let num_of_lines_to_add = row - self.newline_indices.len();
for i in 0..num_of_lines_to_add {
self.newline_indices.push(start_of_last_line + ((i + 1) * self.display_cols as usize));
}
let index_of_row = self.newline_indices.last().unwrap_or(&0); // TODO: better
self.cursor_position = index_of_row + col as usize;
}
}
}
}
}
@ -542,7 +584,8 @@ impl TerminalOutput {
}
}
enum VteEvent { // TODO: try not to allocate Vecs
#[derive(Debug)]
pub enum VteEvent { // TODO: try not to allocate Vecs
Print(char),
Execute(u8), // byte
Hook(Vec<i64>, Vec<u8>, bool, char), // params, intermediates, ignore, char
@ -643,6 +686,7 @@ fn split_horizontally_with_gap (rect: &Winsize) -> (Winsize, Winsize) {
(first_rect, second_rect)
}
#[derive(Debug)]
enum ScreenInstruction {
Pty(RawFd, VteEvent),
Render,
@ -732,10 +776,10 @@ impl Screen {
let terminal_output = self.terminals.get_mut(&pid).unwrap();
terminal_output.handle_event(event);
}
pub fn write_to_active_terminal(&self, byte: u8) {
pub fn write_to_active_terminal(&mut self, byte: u8) {
if let Some(active_terminal_id) = &self.get_active_terminal_id() {
let mut buffer = [byte];
self.os_api.write(*active_terminal_id, &mut buffer).expect("failed to write to terminal");
self.os_api.write_to_tty_stdin(*active_terminal_id, &mut buffer).expect("failed to write to terminal");
self.os_api.tcdrain(*active_terminal_id).expect("failed to drain terminal");
}
}
@ -747,6 +791,7 @@ impl Screen {
let mut stdout = self.os_api.get_stdout_writer();
for (_pid, terminal) in self.terminals.iter_mut() {
if let Some(vte_output) = terminal.buffer_as_vte_output() {
// write boundaries
if terminal.x_coords + terminal.display_cols < self.full_screen_ws.ws_col {
let boundary_x_coords = terminal.x_coords + terminal.display_cols;
let mut vte_output_boundaries = String::new();
@ -990,19 +1035,19 @@ impl PtyBus {
}
}
fn main() {
pub fn main() {
let os_input = get_os_input();
start(os_input);
start(Box::new(os_input));
}
fn start(os_input: OsInputOutput) {
pub fn start(mut os_input: Box<dyn OsApi>) {
let mut active_threads = vec![];
let full_screen_ws = os_input.get_terminal_size_using_fd(0);
os_input.into_raw_mode(0);
let mut screen = Screen::new(&full_screen_ws, Box::new(os_input.clone()));
let mut screen = Screen::new(&full_screen_ws, os_input.clone());
let send_screen_instructions = screen.send_screen_instructions.clone();
let mut pty_bus = PtyBus::new(send_screen_instructions.clone(), Box::new(os_input.clone()));
let mut pty_bus = PtyBus::new(send_screen_instructions.clone(), os_input.clone());
let send_pty_instructions = pty_bus.send_pty_instructions.clone();
active_threads.push(
@ -1094,8 +1139,9 @@ fn start(os_input: OsInputOutput) {
}
// cleanup();
let reset_style = "\u{1b}[m";
let goodbye_message = format!("\r{}Bye from Mosaic!", reset_style);
let goodbye_message = format!("\n\r{}Bye from Mosaic!", reset_style);
os_input.get_stdout_writer().write(goodbye_message.as_bytes()).unwrap();
os_input.get_stdout_writer().flush().unwrap();
}

View file

@ -62,8 +62,8 @@ fn spawn_terminal () -> (RawFd, RawFd) {
let pid_primary = fork_pty_res.master;
let pid_secondary = match fork_pty_res.fork_result {
ForkResult::Parent { child } => {
fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::empty())).expect("could not fcntl");
// fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).expect("could not fcntl");
// fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::empty())).expect("could not fcntl");
fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).expect("could not fcntl");
child
},
ForkResult::Child => {
@ -87,13 +87,13 @@ pub struct OsInputOutput {}
pub trait OsApi: Send + Sync {
fn get_terminal_size_using_fd(&self, pid: RawFd) -> Winsize;
fn set_terminal_size_using_fd(&self, pid: RawFd, cols: u16, rows: u16);
fn into_raw_mode(&self, pid: RawFd);
fn spawn_terminal(&self) -> (RawFd, RawFd);
fn read(&self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
fn write(&self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
fn tcdrain(&self, pid: RawFd) -> Result<(), nix::Error>;
fn kill(&self, pid: RawFd) -> Result<(), nix::Error>;
fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16);
fn into_raw_mode(&mut self, pid: RawFd);
fn spawn_terminal(&mut self) -> (RawFd, RawFd);
fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error>;
fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error>;
fn get_stdin_reader(&self) -> Box<dyn Read>;
fn get_stdout_writer(&self) -> Box<dyn Write>;
fn box_clone(&self) -> Box<dyn OsApi>;
@ -103,22 +103,22 @@ impl OsApi for OsInputOutput {
fn get_terminal_size_using_fd(&self, pid: RawFd) -> Winsize {
get_terminal_size_using_fd(pid)
}
fn set_terminal_size_using_fd(&self, pid: RawFd, cols: u16, rows: u16) {
fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) {
set_terminal_size_using_fd(pid, cols, rows);
}
fn into_raw_mode(&self, pid: RawFd) {
fn into_raw_mode(&mut self, pid: RawFd) {
into_raw_mode(pid);
}
fn spawn_terminal(&self) -> (RawFd, RawFd) {
fn spawn_terminal(&mut self) -> (RawFd, RawFd) {
spawn_terminal()
}
fn read(&self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
read(pid, buf)
}
fn write(&self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
write(pid, buf)
}
fn tcdrain(&self, pid: RawFd) -> Result<(), nix::Error> {
fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> {
tcdrain(pid)
}
fn box_clone(&self) -> Box<dyn OsApi> {
@ -135,7 +135,7 @@ impl OsApi for OsInputOutput {
let stdout = ::std::io::stdout();
Box::new(stdout)
}
fn kill(&self, fd: RawFd) -> Result<(), nix::Error> {
fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> {
kill(Pid::from_raw(fd), None)
}
}

File diff suppressed because one or more lines are too long

168
src/tests/fakes.rs Normal file
View file

@ -0,0 +1,168 @@
use ::std::time::Duration;
use ::nix::pty::Winsize;
use ::std::os::unix::io::RawFd;
use ::std::io::{Read, Write};
use ::std::collections::HashMap;
use ::std::sync::{Arc, Mutex};
use crate::os_input_output::OsApi;
use crate::tests::possible_inputs::{Bytes, get_possible_inputs};
#[derive(Clone)]
pub enum IoEvent {
Kill(RawFd),
SetTerminalSizeUsingFd(RawFd, u16, u16),
IntoRawMode(RawFd),
TcDrain(RawFd),
}
pub struct FakeStdinReader {
pub input_chars: Bytes,
}
impl FakeStdinReader {
pub fn new(input_chars: Vec<u8>) -> Self {
let bytes = Bytes::new().content(input_chars);
FakeStdinReader {
input_chars: bytes,
}
}
}
impl Read for FakeStdinReader {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
::std::thread::sleep(Duration::from_millis(1000)); // TODO: adjust duration
let read_position = self.input_chars.read_position;
buf[0] = self.input_chars.content[read_position];
self.input_chars.set_read_position(read_position + 1);
Ok(1)
}
}
#[derive(Clone, Default)]
pub struct FakeStdoutWriter {
output_buffer: Arc<Mutex<Vec<u8>>>,
pub output_frames: Arc<Mutex<Vec<Vec<u8>>>>,
}
impl Write for FakeStdoutWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
let mut bytes_written = 0;
let mut output_buffer = self.output_buffer.lock().unwrap();
for byte in buf {
bytes_written += 1;
output_buffer.push(*byte);
}
Ok(bytes_written)
}
fn flush(&mut self) -> Result<(), std::io::Error> {
let mut output_buffer = self.output_buffer.lock().unwrap();
let mut output_frames = self.output_frames.lock().unwrap();
let new_frame = output_buffer.drain(..).collect();
output_frames.push(new_frame);
Ok(())
}
}
#[derive(Clone)]
pub struct FakeInputOutput {
read_buffers: Arc<Mutex<HashMap<RawFd, Bytes>>>,
input_to_add: Arc<Mutex<Option<Vec<u8>>>>,
stdin_writes: Arc<Mutex<HashMap<RawFd, Vec<u8>>>>,
pub stdout_writer: FakeStdoutWriter, // stdout_writer.output is already an arc/mutex
io_events: Arc<Mutex<Vec<IoEvent>>>,
win_sizes: Arc<Mutex<HashMap<RawFd, Winsize>>>,
possible_inputs: HashMap<u16, Bytes>,
}
impl FakeInputOutput {
pub fn new(winsize: Winsize) -> Self {
let mut win_sizes = HashMap::new();
win_sizes.insert(0 , winsize); // 0 is the current terminal
FakeInputOutput {
read_buffers: Arc::new(Mutex::new(HashMap::new())),
stdin_writes: Arc::new(Mutex::new(HashMap::new())),
input_to_add: Arc::new(Mutex::new(None)),
stdout_writer: FakeStdoutWriter::default(),
io_events: Arc::new(Mutex::new(vec![])),
win_sizes: Arc::new(Mutex::new(win_sizes)),
possible_inputs: get_possible_inputs(),
}
}
pub fn add_terminal_input(&mut self, input: &[u8]) {
self.input_to_add = Arc::new(Mutex::new(Some(input.to_vec())));
}
pub fn add_terminal(&mut self, fd: RawFd) {
self.stdin_writes.lock().unwrap().insert(fd, vec![]);
}
}
impl OsApi for FakeInputOutput {
fn get_terminal_size_using_fd(&self, pid: RawFd) -> Winsize {
let win_sizes = self.win_sizes.lock().unwrap();
let winsize = win_sizes.get(&pid).unwrap();
*winsize
}
fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) {
let terminal_input = self.possible_inputs.get(&cols).expect(&format!("could not find input for size {:?}", cols));
self.read_buffers.lock().unwrap().insert(pid, terminal_input.clone());
self.io_events.lock().unwrap().push(IoEvent::SetTerminalSizeUsingFd(pid, cols, rows));
}
fn into_raw_mode(&mut self, pid: RawFd) {
self.io_events.lock().unwrap().push(IoEvent::IntoRawMode(pid));
}
fn spawn_terminal(&mut self) -> (RawFd, RawFd) {
let next_terminal_id = { self.read_buffers.lock().unwrap().keys().len() as RawFd + 1 };
self.add_terminal(next_terminal_id);
(next_terminal_id as i32, next_terminal_id + 1000) // secondary number is arbitrary here
}
fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
let mut read_buffers = self.read_buffers.lock().unwrap();
let mut bytes_read = 0;
if let Some(bytes) = read_buffers.get_mut(&pid) {
for i in bytes.read_position..bytes.content.len() {
bytes_read += 1;
buf[i] = bytes.content[i];
}
if bytes_read > bytes.read_position {
bytes.set_read_position(bytes_read);
}
}
Ok(bytes_read)
}
fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
let mut stdin_writes = self.stdin_writes.lock().unwrap();
let write_buffer = stdin_writes.get_mut(&pid).unwrap();
let mut bytes_written = 0;
for byte in buf {
bytes_written += 1;
write_buffer.push(*byte);
}
Ok(bytes_written)
}
fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> {
self.io_events.lock().unwrap().push(IoEvent::TcDrain(pid));
Ok(())
}
fn box_clone(&self) -> Box<dyn OsApi> {
Box::new((*self).clone())
}
fn get_stdin_reader(&self) -> Box<dyn Read> {
let mut input_chars = vec![0, 1, 2];
if let Some(input_to_add) = self.input_to_add.lock().unwrap().as_ref() {
for byte in input_to_add {
input_chars.push(*byte);
}
}
input_chars.push(17); // ctrl-q (quit)
let reader = FakeStdinReader::new(input_chars);
Box::new(reader)
}
fn get_stdout_writer(&self) -> Box<dyn Write> {
Box::new(self.stdout_writer.clone())
}
fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> {
self.io_events.lock().unwrap().push(IoEvent::Kill(fd));
Ok(())
}
}

99
src/tests/integration.rs Normal file
View file

@ -0,0 +1,99 @@
use ::nix::pty::Winsize;
use ::insta::assert_snapshot;
use crate::{start, TerminalOutput};
use crate::tests::fakes::{FakeInputOutput};
pub fn get_fake_os_input () -> FakeInputOutput {
let fake_win_size = Winsize {
ws_col: 121,
ws_row: 20,
ws_xpixel: 0,
ws_ypixel: 0,
};
FakeInputOutput::new(fake_win_size)
}
#[test]
pub fn starts_with_one_terminal () {
let mut fake_input_output = get_fake_os_input();
fake_input_output.add_terminal_input(&[17]); // quit (ctrl-q)
start(Box::new(fake_input_output.clone()));
let output_frames = fake_input_output.stdout_writer.output_frames.lock().unwrap();
let mut vte_parser = vte::Parser::new();
let main_pid = 0;
let x = 0;
let fake_win_size = Winsize { // TODO: combine with above
ws_col: 121,
ws_row: 20,
ws_xpixel: 0,
ws_ypixel: 0,
};
let mut terminal_output = TerminalOutput::new(main_pid, fake_win_size, x);
for frame in output_frames.iter() {
for byte in frame.iter() {
vte_parser.advance(&mut terminal_output, *byte);
}
let output_lines = terminal_output.read_buffer_as_lines();
let cursor_position_in_last_line = terminal_output.cursor_position_in_last_line();
let mut snapshot = String::new();
for (line_index, line) in output_lines.iter().enumerate() {
for (character_index, terminal_character) in line.iter().enumerate() {
if line_index == output_lines.len() - 1 && character_index == cursor_position_in_last_line {
snapshot.push('█');
} else {
snapshot.push(terminal_character.character);
}
}
if line_index != output_lines.len() - 1 {
snapshot.push('\n');
}
}
assert_snapshot!(snapshot);
}
}
#[test]
pub fn split_terminals_vertically() {
// TODO: this test is a little flaky - there's some sort of race condition that makes the
// second terminal in the split appear blank sometimes - fix this
let mut fake_input_output = get_fake_os_input();
fake_input_output.add_terminal_input(&[14, 17]); // split-vertically and quit (ctrl-n + ctrl-q)
start(Box::new(fake_input_output.clone()));
let output_frames = fake_input_output.stdout_writer.output_frames.lock().unwrap();
let mut vte_parser = vte::Parser::new();
let main_pid = 0;
let x = 0;
let fake_win_size = Winsize { // TODO: combine with above
ws_col: 121,
ws_row: 20,
ws_xpixel: 0,
ws_ypixel: 0,
};
let mut terminal_output = TerminalOutput::new(main_pid, fake_win_size, x);
for frame in output_frames.iter() {
for byte in frame.iter() {
vte_parser.advance(&mut terminal_output, *byte);
}
let output_lines = terminal_output.read_buffer_as_lines();
let cursor_position_in_last_line = terminal_output.cursor_position_in_last_line();
let mut snapshot = String::new();
for (line_index, line) in output_lines.iter().enumerate() {
for (character_index, terminal_character) in line.iter().enumerate() {
if line_index == output_lines.len() - 1 && character_index == cursor_position_in_last_line {
snapshot.push('█');
} else {
snapshot.push(terminal_character.character);
}
}
if line_index != output_lines.len() - 1 {
snapshot.push('\n');
}
}
assert_snapshot!(snapshot);
}
}

4
src/tests/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod integration;
pub mod possible_inputs;
pub mod binary_inputs;
pub mod fakes;

View file

@ -0,0 +1,33 @@
use std::collections::HashMap;
use crate::tests::binary_inputs::{COL_121, COL_60};
#[derive(Clone)]
pub struct Bytes {
pub content: Vec<u8>,
pub read_position: usize,
}
impl Bytes {
pub fn new() -> Self {
Bytes {
content: vec![],
read_position: 0
}
}
pub fn content(mut self, content: Vec<u8>) -> Self {
self.content = content;
self
}
pub fn set_read_position(&mut self, read_position: usize) {
self.read_position = read_position;
}
}
pub fn get_possible_inputs () -> HashMap<u16, Bytes> { // the key is the column count for this terminal input
let mut possible_inputs = HashMap::new();
let col_60_bytes = Bytes::new().content(Vec::from(COL_60));
let col_121_bytes = Bytes::new().content(Vec::from(COL_121));
possible_inputs.insert(121, col_121_bytes);
possible_inputs.insert(60, col_60_bytes);
possible_inputs
}

View file

@ -0,0 +1,24 @@
---
source: src/tests/integration.rs
expression: snapshot
---
Welcome to fish, the friendly interactive shell │
⋊> ~/c/mosaic on main

View file

@ -0,0 +1,24 @@
---
source: src/tests/integration.rs
expression: snapshot
---
Welcome to fish, the friendly interactive shell │
│Welcome to fish, the friendly interactive shell
⋊> ~/c/mosaic on main │⋊> ~/c/mosaic on main █ 19:40:00

View file

@ -0,0 +1,24 @@
---
source: src/tests/integration.rs
expression: snapshot
---
Welcome to fish, the friendly interactive shell │
│Welcome to fish, the friendly interactive shell
⋊> ~/c/mosaic on main │⋊> ~/c/mosaic on main 19:40:00
Bye from Mosaic!

View file

@ -0,0 +1,24 @@
---
source: src/tests/integration.rs
expression: snapshot
---
Welcome to fish, the friendly interactive shell
⋊> ~/c/mosaic on main █ 10:47:03

View file

@ -0,0 +1,24 @@
---
source: src/tests/integration.rs
expression: snapshot
---
Welcome to fish, the friendly interactive shell
⋊> ~/c/mosaic on main 10:47:03
Bye from Mosaic!

View file

@ -0,0 +1,24 @@
---
source: src/tests/integration.rs
expression: snapshot
---
Welcome to fish, the friendly interactive shell
⋊> ~/c/mosaic on main █ 10:47:03