embed test #2
parent
7ea6593a1f
commit
ec35b9fa0a
|
@ -645,6 +645,18 @@ dependencies = [
|
|||
"void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.13"
|
||||
|
@ -1181,7 +1193,7 @@ dependencies = [
|
|||
"linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"melib 0.3.2",
|
||||
"mime_apps 0.2.0 (git+https://git.meli.delivery/meli/mime_apps)",
|
||||
"nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nix 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify-rust 3.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -1359,6 +1371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
"checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e"
|
||||
"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
|
||||
"checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce"
|
||||
"checksum nix 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229"
|
||||
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
|
||||
"checksum nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b"
|
||||
"checksum notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "3572d71f13ea8ed41867accd971fd564aa75934cf7a1fae03ddb8c74a8a49943"
|
||||
|
|
|
@ -303,10 +303,13 @@ fn main() -> std::result::Result<(), std::io::Error> {
|
|||
},
|
||||
}
|
||||
},
|
||||
UIMode::Embed => {
|
||||
state.rcv_event(UIEvent::EmbedInput(k));
|
||||
state.redraw();
|
||||
},
|
||||
UIMode::Fork => {
|
||||
break 'inner; // `goto` 'reap loop, and wait on child.
|
||||
},
|
||||
UIMode::Embed => {}
|
||||
}
|
||||
},
|
||||
ThreadEvent::RefreshMailbox(event) => {
|
||||
|
|
|
@ -25,7 +25,7 @@ uuid = { version = "0.7.4", features = ["serde", "v4"] }
|
|||
unicode-segmentation = "1.2.1" # >:c
|
||||
text_processing = { path = "../text_processing", version = "*" }
|
||||
libc = {version = "0.2.59", features = ["extra_traits",]}
|
||||
nix = "*"
|
||||
nix = "0.15.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
use super::*;
|
||||
|
||||
use crate::terminal::embed::EmbedPty;
|
||||
use melib::Draft;
|
||||
use mime_apps::query_mime_info;
|
||||
use std::str::FromStr;
|
||||
|
@ -46,6 +47,7 @@ pub struct Composer {
|
|||
mode: ViewMode,
|
||||
|
||||
body_area: Area, // Cache body_area in case we need to replace it with a pseudoterminal
|
||||
embed: Option<EmbedPty>,
|
||||
sign_mail: ToggleFlag,
|
||||
dirty: bool,
|
||||
has_changes: bool,
|
||||
|
@ -70,6 +72,7 @@ impl Default for Composer {
|
|||
dirty: true,
|
||||
has_changes: false,
|
||||
body_area: ((0, 0), (0, 0)),
|
||||
embed: None,
|
||||
initialized: false,
|
||||
id: ComponentId::new_v4(),
|
||||
}
|
||||
|
@ -530,6 +533,25 @@ impl Component for Composer {
|
|||
Color::Default,
|
||||
);
|
||||
}
|
||||
self.body_area = body_area;
|
||||
if let Some(ref mut embed_pty) = self.embed {
|
||||
let lock = embed_pty.grid.lock().unwrap();
|
||||
copy_area(
|
||||
grid,
|
||||
&lock,
|
||||
area,
|
||||
((0, 0), pos_dec(embed_pty.terminal_size, (1, 1))),
|
||||
);
|
||||
for y in 0..embed_pty.terminal_size.1 {
|
||||
for x in 0..embed_pty.terminal_size.0 {
|
||||
if lock[(x, y)].ch() != ' ' {
|
||||
debug!("coors {:?} char = {}", (x, y), lock[(x, y)].ch());
|
||||
}
|
||||
}
|
||||
}
|
||||
context.dirty_areas.push_back(area);
|
||||
debug!("copied grid");
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
ViewMode::ThreadView | ViewMode::Edit => {}
|
||||
|
@ -747,12 +769,27 @@ impl Component for Composer {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::EmbedInput(Key::Char(c)) => {
|
||||
debug!("got embed input {:?}", event);
|
||||
let mut buf: [u8; 4] = [0; 4];
|
||||
let s = c.encode_utf8(&mut buf);
|
||||
|
||||
use std::io::Write;
|
||||
self.embed
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.stdin
|
||||
.write_all(s.as_bytes())
|
||||
.unwrap();
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Char('e')) => {
|
||||
/* Edit draft in $EDITOR */
|
||||
use std::process::{Command, Stdio};
|
||||
context.input_kill();
|
||||
crate::terminal::embed::create_pty(self.body_area).unwrap();
|
||||
|
||||
self.embed = Some(crate::terminal::embed::create_pty(self.body_area).unwrap());
|
||||
self.dirty = true;
|
||||
debug!("returned");
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::ChangeMode(UIMode::Embed));
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
use crate::terminal::position::Area;
|
||||
use nix::fcntl::{open, OFlag};
|
||||
use nix::ioctl_write_ptr_bad;
|
||||
use nix::libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
|
||||
use nix::pty::{grantpt, posix_openpt, ptsname, unlockpt, Winsize};
|
||||
use nix::sys::stat::Mode;
|
||||
use nix::sys::{stat, wait};
|
||||
use nix::unistd::{dup2, fork, setsid, ForkResult};
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
|
||||
|
||||
mod grid;
|
||||
|
||||
use crate::terminal::cells::{Cell, CellBuffer};
|
||||
pub use grid::*;
|
||||
|
||||
// ioctl command to set window size of pty:
|
||||
use libc::TIOCSWINSZ;
|
||||
|
@ -11,13 +20,20 @@ use std::process::{Command, Stdio};
|
|||
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::os::unix::io::{FromRawFd, IntoRawFd};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
ioctl_write_ptr_bad!(set_window_size, TIOCSWINSZ, Winsize);
|
||||
|
||||
static SWITCHALTERNATIVE_1049: &'static [u8] = &[b'1', b'0', b'4', b'9'];
|
||||
|
||||
pub fn create_pty(area: Area) -> nix::Result<()> {
|
||||
#[derive(Debug)]
|
||||
pub struct EmbedPty {
|
||||
pub grid: Arc<Mutex<CellBuffer>>,
|
||||
pub stdin: std::fs::File,
|
||||
pub terminal_size: (usize, usize),
|
||||
}
|
||||
|
||||
pub fn create_pty(area: Area) -> nix::Result<EmbedPty> {
|
||||
// Open a new PTY master
|
||||
let master_fd = posix_openpt(OFlag::O_RDWR)?;
|
||||
|
||||
|
@ -29,34 +45,55 @@ pub fn create_pty(area: Area) -> nix::Result<()> {
|
|||
let slave_name = unsafe { ptsname(&master_fd) }?;
|
||||
|
||||
// Try to open the slave
|
||||
let _slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?;
|
||||
//let _slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?;
|
||||
{
|
||||
let winsize = Winsize {
|
||||
ws_row: 40,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
Command::new("vim")
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(unsafe { Stdio::from_raw_fd(_slave_fd) })
|
||||
.stderr(unsafe { Stdio::from_raw_fd(_slave_fd) })
|
||||
.spawn();
|
||||
let master_fd = master_fd.clone().into_raw_fd();
|
||||
unsafe { set_window_size(master_fd, &winsize).unwrap() };
|
||||
}
|
||||
match fork() {
|
||||
Ok(ForkResult::Child) => {
|
||||
setsid().unwrap(); // create new session with child as session leader
|
||||
let slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, stat::Mode::empty())?;
|
||||
|
||||
// assign stdin, stdout, stderr to the tty, just like a terminal does
|
||||
dup2(slave_fd, STDIN_FILENO).unwrap();
|
||||
dup2(slave_fd, STDOUT_FILENO).unwrap();
|
||||
dup2(slave_fd, STDERR_FILENO).unwrap();
|
||||
std::process::Command::new("vim").status().unwrap();
|
||||
}
|
||||
Ok(ForkResult::Parent { child: _ }) => {}
|
||||
Err(e) => panic!(e),
|
||||
};
|
||||
|
||||
let stdin = unsafe { std::fs::File::from_raw_fd(master_fd.clone().into_raw_fd()) };
|
||||
let stdin_ = unsafe { std::fs::File::from_raw_fd(master_fd.clone().into_raw_fd()) };
|
||||
let grid = Arc::new(Mutex::new(CellBuffer::new(80, 40, Cell::default())));
|
||||
let grid_ = grid.clone();
|
||||
let terminal_size = (80, 40);
|
||||
|
||||
std::thread::Builder::new()
|
||||
.spawn(move || {
|
||||
let winsize = Winsize {
|
||||
ws_row: 20,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
//lock.write(b"\x1b3g").unwrap(); //clear all
|
||||
let master_fd = master_fd.into_raw_fd();
|
||||
unsafe { set_window_size(master_fd, &winsize).unwrap() };
|
||||
let master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
|
||||
forward_pty_translate_escape_codes(master_file, area);
|
||||
forward_pty_translate_escape_codes(master_file, area, grid_, stdin_);
|
||||
})
|
||||
.unwrap();
|
||||
Ok(())
|
||||
Ok(EmbedPty {
|
||||
grid,
|
||||
stdin,
|
||||
terminal_size,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
pub enum State {
|
||||
ExpectingControlChar,
|
||||
G0, // Designate G0 Character Set
|
||||
Osc1(Vec<u8>), //ESC ] Operating System Command (OSC is 0x9d).
|
||||
|
@ -71,6 +108,13 @@ enum State {
|
|||
|
||||
struct EscCode<'a>(&'a State, u8);
|
||||
|
||||
impl<'a> From<(&'a mut State, u8)> for EscCode<'a> {
|
||||
fn from(val: (&mut State, u8)) -> EscCode {
|
||||
let (s, b) = val;
|
||||
EscCode(s, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<(&'a State, u8)> for EscCode<'a> {
|
||||
fn from(val: (&State, u8)) -> EscCode {
|
||||
let (s, b) = val;
|
||||
|
@ -122,6 +166,15 @@ impl std::fmt::Display for EscCode<'_> {
|
|||
"ESC[{}n\t\tCSI Device Status Report (DSR)| Report Cursor Position",
|
||||
unsafestr!(buf)
|
||||
),
|
||||
EscCode(Csi1(ref buf), b't') if buf == b"18" => write!(
|
||||
f,
|
||||
"ESC[18t\t\tReport the size of the text area in characters",
|
||||
),
|
||||
EscCode(Csi1(ref buf), b't') => write!(
|
||||
f,
|
||||
"ESC[{buf}t\t\tWindow manipulation, skipped",
|
||||
buf = unsafestr!(buf)
|
||||
),
|
||||
EscCode(Csi1(ref buf), b'B') => write!(
|
||||
f,
|
||||
"ESC[{buf}B\t\tCSI Cursor Down {buf} Times",
|
||||
|
@ -208,7 +261,12 @@ impl std::fmt::Display for EscCode<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, area: Area) {
|
||||
fn forward_pty_translate_escape_codes(
|
||||
pty_fd: std::fs::File,
|
||||
area: Area,
|
||||
grid: Arc<Mutex<CellBuffer>>,
|
||||
stdin: std::fs::File,
|
||||
) {
|
||||
let (upper_left, bottom_right) = area;
|
||||
let (upper_x, upper_y) = upper_left;
|
||||
let (bottom_x, bottom_y) = bottom_right;
|
||||
|
@ -222,322 +280,18 @@ fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, area: Area) {
|
|||
debug!(&upper_y_str);
|
||||
debug!(&bottom_x_str);
|
||||
debug!(&bottom_y_str);
|
||||
let stdout = std::io::stdout();
|
||||
let mut buf1: Vec<u8> = Vec::with_capacity(8);
|
||||
let mut buf2: Vec<u8> = Vec::with_capacity(8);
|
||||
let mut buf3: Vec<u8> = Vec::with_capacity(8);
|
||||
let mut lock = stdout.lock();
|
||||
|
||||
let mut state = State::Normal;
|
||||
let mut embed_grid = EmbedGrid::new(grid, stdin);
|
||||
embed_grid.set_terminal_size((79, 39));
|
||||
let mut bytes_iter = pty_fd.bytes();
|
||||
macro_rules! cleanup {
|
||||
(CSIQ) => {
|
||||
if let State::CsiQ(ref mut buf1_p) = state {
|
||||
std::mem::swap(buf1_p, &mut buf1);
|
||||
}
|
||||
};
|
||||
(CSI1) => {
|
||||
if let State::Csi1(ref mut buf1_p) = state {
|
||||
std::mem::swap(buf1_p, &mut buf1);
|
||||
}
|
||||
};
|
||||
(CSI2) => {
|
||||
if let State::Csi2(ref mut buf1_p, ref mut buf2_p) = state {
|
||||
std::mem::swap(buf1_p, &mut buf1);
|
||||
std::mem::swap(buf2_p, &mut buf2);
|
||||
}
|
||||
};
|
||||
(CSI3) => {
|
||||
if let State::Csi3(ref mut buf1_p, ref mut buf2_p, ref mut buf3_p) = state {
|
||||
std::mem::swap(buf1_p, &mut buf1);
|
||||
std::mem::swap(buf2_p, &mut buf2);
|
||||
std::mem::swap(buf3_p, &mut buf3);
|
||||
}
|
||||
};
|
||||
(OSC1) => {
|
||||
if let State::Osc1(ref mut buf1_p) = state {
|
||||
std::mem::swap(buf1_p, &mut buf1);
|
||||
}
|
||||
};
|
||||
(OSC2) => {
|
||||
if let State::Osc2(ref mut buf1_p, ref mut buf2_p) = state {
|
||||
std::mem::swap(buf1_p, &mut buf1);
|
||||
std::mem::swap(buf2_p, &mut buf2);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! restore_global_buf {
|
||||
($b:ident) => {
|
||||
let mut $b = std::mem::replace(&mut $b, Vec::new());
|
||||
$b.clear();
|
||||
};
|
||||
}
|
||||
|
||||
let mut prev_char = b'\0';
|
||||
debug!("waiting for bytes");
|
||||
while let Some(Ok(byte)) = bytes_iter.next() {
|
||||
debug!("got byte {}", byte as char);
|
||||
debug!(
|
||||
"{}{} byte is {} and state is {:?}",
|
||||
prev_char as char, byte as char, byte as char, &state
|
||||
prev_char as char, byte as char, byte as char, &embed_grid.state
|
||||
);
|
||||
prev_char = byte;
|
||||
match (byte, &mut state) {
|
||||
(b'\x1b', State::Normal) => {
|
||||
state = State::ExpectingControlChar;
|
||||
}
|
||||
(b']', State::ExpectingControlChar) => {
|
||||
restore_global_buf!(buf1);
|
||||
state = State::Osc1(buf1);
|
||||
}
|
||||
(b'[', State::ExpectingControlChar) => {
|
||||
state = State::Csi;
|
||||
}
|
||||
(b'(', State::ExpectingControlChar) => {
|
||||
state = State::G0;
|
||||
}
|
||||
(c, State::ExpectingControlChar) => {
|
||||
debug!(
|
||||
"unrecognised: byte is {} and state is {:?}",
|
||||
byte as char, &state
|
||||
);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b'?', State::Csi) => {
|
||||
restore_global_buf!(buf1);
|
||||
state = State::CsiQ(buf1);
|
||||
}
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* OSC stuff */
|
||||
(c, State::Osc1(ref mut buf)) if (c >= b'0' && c <= b'9') || c == b'?' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(b';', State::Osc1(ref mut buf1_p)) => {
|
||||
let buf1 = std::mem::replace(buf1_p, Vec::new());
|
||||
let mut buf2 = std::mem::replace(&mut buf2, Vec::new());
|
||||
buf2.clear();
|
||||
state = State::Osc2(buf1, buf2);
|
||||
}
|
||||
(c, State::Osc2(_, ref mut buf)) if (c >= b'0' && c <= b'9') || c == b'?' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Osc1(ref buf1)) => {
|
||||
lock.write_all(&[b'\x1b', b']']).unwrap();
|
||||
lock.write_all(buf1).unwrap();
|
||||
lock.write_all(&[c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
cleanup!(OSC1);
|
||||
state = State::Normal;
|
||||
}
|
||||
(c, State::Osc2(ref buf1, ref buf2)) => {
|
||||
lock.write_all(&[b'\x1b', b']']).unwrap();
|
||||
lock.write_all(buf1).unwrap();
|
||||
lock.write_all(&[b';']).unwrap();
|
||||
lock.write_all(buf2).unwrap();
|
||||
lock.write_all(&[c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
cleanup!(OSC2);
|
||||
state = State::Normal;
|
||||
}
|
||||
/* END OF OSC */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
(c, State::Normal) => {
|
||||
lock.write(&[byte]).unwrap();
|
||||
lock.flush().unwrap();
|
||||
}
|
||||
(b'u', State::Csi) => {
|
||||
/* restore cursor */
|
||||
lock.write_all(&[b'\x1b', b'[', b'u']).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
lock.flush().unwrap();
|
||||
state = State::Normal;
|
||||
}
|
||||
(b'm', State::Csi) => {
|
||||
/* Character Attributes (SGR). Ps = 0 -> Normal (default), VT100 */
|
||||
lock.write_all(&[b'\x1b', b'[', b'm']).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
lock.flush().unwrap();
|
||||
state = State::Normal;
|
||||
}
|
||||
(b'H', State::Csi) => {
|
||||
/* move cursor to (1,1) */
|
||||
lock.write_all(&[b'\x1b', b'[']).unwrap();
|
||||
lock.write_all(upper_x_str.as_bytes()).unwrap();
|
||||
lock.write_all(&[b';']).unwrap();
|
||||
lock.write_all(upper_y_str.as_bytes()).unwrap();
|
||||
lock.write_all(&[b'H']).unwrap();
|
||||
debug!(
|
||||
"sending translating {} to ESC[{};{}H",
|
||||
EscCode::from((&state, byte)),
|
||||
upper_x_str,
|
||||
upper_y_str,
|
||||
);
|
||||
lock.flush().unwrap();
|
||||
state = State::Normal;
|
||||
}
|
||||
/* CSI ? stuff */
|
||||
(c, State::CsiQ(ref mut buf)) if c >= b'0' && c <= b'9' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::CsiQ(ref mut buf)) => {
|
||||
// we are already in AlternativeScreen so do not forward this
|
||||
if &buf.as_slice() != &SWITCHALTERNATIVE_1049 {
|
||||
lock.write_all(&[b'\x1b', b'[', b'?']).unwrap();
|
||||
lock.write_all(buf).unwrap();
|
||||
lock.write_all(&[c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
}
|
||||
cleanup!(CSIQ);
|
||||
state = State::Normal;
|
||||
}
|
||||
/* END OF CSI ? stuff */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
(c, State::Csi) if c >= b'0' && c <= b'9' => {
|
||||
let mut buf1 = std::mem::replace(&mut buf1, Vec::new());
|
||||
buf1.clear();
|
||||
buf1.push(c);
|
||||
state = State::Csi1(buf1);
|
||||
}
|
||||
(c, State::Csi) => {
|
||||
lock.write_all(&[b'\x1b', b'[', c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
lock.flush().unwrap();
|
||||
state = State::Normal;
|
||||
}
|
||||
(b'K', State::Csi1(_)) => {
|
||||
/* Erase in Display (ED), VT100.*/
|
||||
cleanup!(CSI1);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b'J', State::Csi1(_)) => {
|
||||
/* Erase in Display (ED), VT100.*/
|
||||
cleanup!(CSI1);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b't', State::Csi1(_)) => {
|
||||
/* Window manipulation, skip it */
|
||||
cleanup!(CSI1);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b';', State::Csi1(ref mut buf1_p)) => {
|
||||
let buf1 = std::mem::replace(buf1_p, Vec::new());
|
||||
let mut buf2 = std::mem::replace(&mut buf2, Vec::new());
|
||||
buf2.clear();
|
||||
state = State::Csi2(buf1, buf2);
|
||||
}
|
||||
(c, State::Csi1(ref mut buf)) if (c >= b'0' && c <= b'9') || c == b' ' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Csi1(ref buf)) => {
|
||||
lock.write_all(&[b'\x1b', b'[']).unwrap();
|
||||
lock.write_all(buf).unwrap();
|
||||
lock.write_all(&[c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
cleanup!(CSI1);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b';', State::Csi2(ref mut buf1_p, ref mut buf2_p)) => {
|
||||
let buf1 = std::mem::replace(buf1_p, Vec::new());
|
||||
let buf2 = std::mem::replace(buf2_p, Vec::new());
|
||||
let mut buf3 = std::mem::replace(&mut buf3, Vec::new());
|
||||
buf3.clear();
|
||||
state = State::Csi3(buf1, buf2, buf3);
|
||||
}
|
||||
(b'n', State::Csi2(_, _)) => {
|
||||
// Report Cursor Position, skip it
|
||||
cleanup!(CSI2);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b't', State::Csi2(_, _)) => {
|
||||
// Window manipulation, skip it
|
||||
cleanup!(CSI2);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b'H', State::Csi2(ref x, ref y)) => {
|
||||
//Cursor Position [row;column] (default = [1,1]) (CUP).
|
||||
let orig_x = unsafe { std::str::from_utf8_unchecked(x) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
let orig_y = unsafe { std::str::from_utf8_unchecked(y) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
if orig_x + upper_x + 1 > bottom_x || orig_y + upper_y + 1 > bottom_y {
|
||||
debug!(orig_x);
|
||||
debug!(orig_y);
|
||||
debug!(area);
|
||||
} else {
|
||||
debug!("orig_x + upper_x = {}", orig_x + upper_x);
|
||||
debug!("orig_y + upper_y = {}", orig_y + upper_y);
|
||||
lock.write_all(&[b'\x1b', b'[']).unwrap();
|
||||
lock.write_all((orig_x + upper_x).to_string().as_bytes())
|
||||
.unwrap();
|
||||
lock.write_all(&[b';']).unwrap();
|
||||
lock.write_all((orig_y + upper_y).to_string().as_bytes())
|
||||
.unwrap();
|
||||
lock.write_all(&[b'H']).unwrap();
|
||||
debug!(
|
||||
"sending translating {} to ESC[{};{}H ",
|
||||
EscCode::from((&state, byte)),
|
||||
orig_x + upper_x,
|
||||
orig_y + upper_y
|
||||
);
|
||||
}
|
||||
cleanup!(CSI2);
|
||||
state = State::Normal;
|
||||
}
|
||||
(c, State::Csi2(_, ref mut buf)) if c >= b'0' && c <= b'9' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Csi2(ref buf1, ref buf2)) => {
|
||||
lock.write_all(&[b'\x1b', b'[']).unwrap();
|
||||
lock.write_all(buf1).unwrap();
|
||||
lock.write_all(&[b';']).unwrap();
|
||||
lock.write_all(buf2).unwrap();
|
||||
lock.write_all(&[c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
cleanup!(CSI2);
|
||||
state = State::Normal;
|
||||
}
|
||||
(b't', State::Csi3(_, _, _)) => {
|
||||
// Window manipulation, skip it
|
||||
cleanup!(CSI3);
|
||||
state = State::Normal;
|
||||
}
|
||||
|
||||
(c, State::Csi3(_, _, ref mut buf)) if c >= b'0' && c <= b'9' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Csi3(ref buf1, ref buf2, ref buf3)) => {
|
||||
lock.write_all(&[b'\x1b', b'[']).unwrap();
|
||||
lock.write_all(buf1).unwrap();
|
||||
lock.write_all(&[b';']).unwrap();
|
||||
lock.write_all(buf2).unwrap();
|
||||
lock.write_all(&[b';']).unwrap();
|
||||
lock.write_all(buf3).unwrap();
|
||||
lock.write_all(&[c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
cleanup!(CSI3);
|
||||
state = State::Normal;
|
||||
}
|
||||
/* other stuff */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
(c, State::G0) => {
|
||||
lock.write_all(&[b'\x1b', b'(']).unwrap();
|
||||
lock.write_all(&[c]).unwrap();
|
||||
debug!("sending {}", EscCode::from((&state, byte)));
|
||||
state = State::Normal;
|
||||
}
|
||||
(b, s) => {
|
||||
debug!("unrecognised: byte is {} and state is {:?}", b as char, s);
|
||||
}
|
||||
}
|
||||
embed_grid.process_byte(byte);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,376 @@
|
|||
use super::*;
|
||||
use crate::terminal::cells::{Cell, CellBuffer};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct EmbedGrid {
|
||||
cursor: (usize, usize),
|
||||
terminal_size: (usize, usize),
|
||||
grid: Arc<Mutex<CellBuffer>>,
|
||||
pub state: State,
|
||||
stdin: std::fs::File,
|
||||
}
|
||||
|
||||
impl EmbedGrid {
|
||||
pub fn new(grid: Arc<Mutex<CellBuffer>>, stdin: std::fs::File) -> Self {
|
||||
EmbedGrid {
|
||||
cursor: (1, 1),
|
||||
terminal_size: (0, 0),
|
||||
grid,
|
||||
state: State::Normal,
|
||||
stdin,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_terminal_size(&mut self, new_val: (usize, usize)) {
|
||||
self.terminal_size = new_val;
|
||||
}
|
||||
|
||||
pub fn process_byte(&mut self, byte: u8) {
|
||||
let EmbedGrid {
|
||||
ref mut cursor,
|
||||
ref terminal_size,
|
||||
ref mut grid,
|
||||
ref mut state,
|
||||
ref mut stdin,
|
||||
} = self;
|
||||
|
||||
macro_rules! increase_cursor_x {
|
||||
() => {
|
||||
if *cursor == *terminal_size {
|
||||
/* do nothing */
|
||||
} else if cursor.0 == terminal_size.0 {
|
||||
cursor.0 = 0;
|
||||
cursor.1 += 1;
|
||||
} else {
|
||||
cursor.0 += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut state = state;
|
||||
match (byte, &mut state) {
|
||||
(b'\x1b', State::Normal) => {
|
||||
*state = State::ExpectingControlChar;
|
||||
}
|
||||
(b']', State::ExpectingControlChar) => {
|
||||
let buf1 = Vec::new();
|
||||
*state = State::Osc1(buf1);
|
||||
}
|
||||
(b'[', State::ExpectingControlChar) => {
|
||||
*state = State::Csi;
|
||||
}
|
||||
(b'(', State::ExpectingControlChar) => {
|
||||
*state = State::G0;
|
||||
}
|
||||
(c, State::ExpectingControlChar) => {
|
||||
debug!(
|
||||
"unrecognised: byte is {} and state is {:?}",
|
||||
byte as char, state
|
||||
);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'?', State::Csi) => {
|
||||
let buf1 = Vec::new();
|
||||
*state = State::CsiQ(buf1);
|
||||
}
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* OSC stuff */
|
||||
(c, State::Osc1(ref mut buf)) if (c >= b'0' && c <= b'9') || c == b'?' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(b';', State::Osc1(ref mut buf1_p)) => {
|
||||
let buf1 = std::mem::replace(buf1_p, Vec::new());
|
||||
let buf2 = Vec::new();
|
||||
*state = State::Osc2(buf1, buf2);
|
||||
}
|
||||
(c, State::Osc2(_, ref mut buf)) if (c >= b'0' && c <= b'9') || c == b'?' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Osc1(_)) => {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(c, State::Osc2(_, _)) => {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
/* END OF OSC */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
/* ********** */
|
||||
(c, State::Normal) => {
|
||||
grid.lock().unwrap()[*cursor].set_ch(c as char);
|
||||
debug!("setting cell {:?} char '{}'", cursor, c as char);
|
||||
increase_cursor_x!();
|
||||
}
|
||||
(b'u', State::Csi) => {
|
||||
/* restore cursor */
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'm', State::Csi) => {
|
||||
/* Character Attributes (SGR). Ps = 0 -> Normal (default), VT100 */
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'H', State::Csi) => {
|
||||
/* move cursor to (1,1) */
|
||||
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)),);
|
||||
debug!("move cursor to (1,1) cursor before: {:?}", *cursor);
|
||||
*cursor = (0, 0);
|
||||
debug!("cursor after: {:?}", *cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
/* CSI ? stuff */
|
||||
(c, State::CsiQ(ref mut buf)) if c >= b'0' && c <= b'9' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::CsiQ(ref mut buf)) => {
|
||||
// we are already in AlternativeScreen so do not forward this
|
||||
if &buf.as_slice() != &SWITCHALTERNATIVE_1049 {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
}
|
||||
*state = State::Normal;
|
||||
}
|
||||
/* END OF CSI ? stuff */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
(c, State::Csi) if c >= b'0' && c <= b'9' => {
|
||||
let mut buf1 = Vec::new();
|
||||
buf1.push(c);
|
||||
*state = State::Csi1(buf1);
|
||||
}
|
||||
(b'J', State::Csi) => {
|
||||
// "ESC[J\t\tCSI Erase from the cursor to the end of the screen [BAD]"
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
let mut grid = grid.lock().unwrap();
|
||||
debug!("erasing from {:?} to {:?}", cursor, terminal_size);
|
||||
for y in cursor.1..terminal_size.1 {
|
||||
for x in cursor.0..terminal_size.0 {
|
||||
cursor.0 = x;
|
||||
grid[(x, y)] = Cell::default();
|
||||
}
|
||||
cursor.1 = y;
|
||||
}
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'K', State::Csi) => {
|
||||
// "ESC[K\t\tCSI Erase from the cursor to the end of the line [BAD]"
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
let mut grid = grid.lock().unwrap();
|
||||
for x in cursor.0..terminal_size.0 {
|
||||
grid[(x, terminal_size.1)] = Cell::default();
|
||||
}
|
||||
*state = State::Normal;
|
||||
}
|
||||
(c, State::Csi) => {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'K', State::Csi1(_)) => {
|
||||
/* Erase in Display (ED), VT100.*/
|
||||
debug!("not sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'J', State::Csi1(_)) => {
|
||||
/* Erase in Display (ED), VT100.*/
|
||||
debug!("not sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b't', State::Csi1(buf)) => {
|
||||
/* Window manipulation, skip it */
|
||||
if buf == b"18" {
|
||||
// P s = 1 8 → Report the size of the text area in characters as CSI 8 ; height ; width t
|
||||
stdin.write_all(&[b'\x1b', b'[', b'8', b';']).unwrap();
|
||||
stdin
|
||||
.write_all((terminal_size.0 + 1).to_string().as_bytes())
|
||||
.unwrap();
|
||||
stdin.write_all(&[b';']).unwrap();
|
||||
stdin
|
||||
.write_all((terminal_size.1 + 1).to_string().as_bytes())
|
||||
.unwrap();
|
||||
stdin.write_all(&[b't']).unwrap();
|
||||
}
|
||||
debug!("not sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'n', State::Csi1(_)) => {
|
||||
/* report cursor position */
|
||||
debug!("got {}", EscCode::from((&(*state), byte)));
|
||||
stdin.write_all(&[b'\x1b', b'[']).unwrap();
|
||||
// Ps = 6 ⇒ Report Cursor Position (CPR) [row;column].
|
||||
//Result is CSI r ; c R
|
||||
stdin
|
||||
.write_all((cursor.0 + 1).to_string().as_bytes())
|
||||
.unwrap();
|
||||
stdin.write_all(&[b';']).unwrap();
|
||||
stdin
|
||||
.write_all((cursor.1 + 1).to_string().as_bytes())
|
||||
.unwrap();
|
||||
stdin.write_all(&[b'R']).unwrap();
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'B', State::Csi1(buf)) => {
|
||||
//"ESC[{buf}B\t\tCSI Cursor Down {buf} Times",
|
||||
let offset = unsafe { std::str::from_utf8_unchecked(buf) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
debug!("cursor down {} times, cursor was: {:?}", offset, cursor);
|
||||
if offset + cursor.1 < terminal_size.1 {
|
||||
cursor.1 += offset;
|
||||
}
|
||||
debug!("cursor became: {:?}", cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'C', State::Csi1(buf)) => {
|
||||
// "ESC[{buf}C\t\tCSI Cursor Forward {buf} Times",
|
||||
let offset = unsafe { std::str::from_utf8_unchecked(buf) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
debug!("cursor forward {} times, cursor was: {:?}", offset, cursor);
|
||||
if offset + cursor.0 < terminal_size.0 {
|
||||
cursor.0 += offset;
|
||||
}
|
||||
debug!("cursor became: {:?}", cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'D', State::Csi1(buf)) => {
|
||||
// "ESC[{buf}D\t\tCSI Cursor Backward {buf} Times",
|
||||
let offset = unsafe { std::str::from_utf8_unchecked(buf) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
debug!("cursor backward {} times, cursor was: {:?}", offset, cursor);
|
||||
if offset + cursor.0 < terminal_size.0 {
|
||||
cursor.0 += offset;
|
||||
}
|
||||
debug!("cursor became: {:?}", cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'E', State::Csi1(buf)) => {
|
||||
//"ESC[{buf}E\t\tCSI Cursor Next Line {buf} Times",
|
||||
let offset = unsafe { std::str::from_utf8_unchecked(buf) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
debug!(
|
||||
"cursor next line {} times, cursor was: {:?}",
|
||||
offset, cursor
|
||||
);
|
||||
if offset + cursor.1 < terminal_size.1 {
|
||||
cursor.1 += offset;
|
||||
cursor.0 = 0;
|
||||
}
|
||||
debug!("cursor became: {:?}", cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'G', State::Csi1(buf)) => {
|
||||
// "ESC[{buf}G\t\tCursor Character Absolute [column={buf}] (default = [row,1])",
|
||||
let new_col = unsafe { std::str::from_utf8_unchecked(buf) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
debug!("cursor absolute {}, cursor was: {:?}", new_col, cursor);
|
||||
if new_col < terminal_size.0 {
|
||||
cursor.0 = new_col;
|
||||
}
|
||||
debug!("cursor became: {:?}", cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'C', State::Csi1(buf)) => {
|
||||
// "ESC[{buf}F\t\tCSI Cursor Preceding Line {buf} Times",
|
||||
let offset = unsafe { std::str::from_utf8_unchecked(buf) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
debug!(
|
||||
"cursor preceding {} times, cursor was: {:?}",
|
||||
offset, cursor
|
||||
);
|
||||
if cursor.1 < offset + terminal_size.1 {
|
||||
cursor.1 -= offset;
|
||||
cursor.0 = 0;
|
||||
}
|
||||
debug!("cursor became: {:?}", cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b';', State::Csi1(ref mut buf1_p)) => {
|
||||
let buf1 = std::mem::replace(buf1_p, Vec::new());
|
||||
let buf2 = Vec::new();
|
||||
*state = State::Csi2(buf1, buf2);
|
||||
}
|
||||
(c, State::Csi1(ref mut buf)) if (c >= b'0' && c <= b'9') || c == b' ' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Csi1(ref buf)) => {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b';', State::Csi2(ref mut buf1_p, ref mut buf2_p)) => {
|
||||
let buf1 = std::mem::replace(buf1_p, Vec::new());
|
||||
let buf2 = std::mem::replace(buf2_p, Vec::new());
|
||||
let buf3 = Vec::new();
|
||||
*state = State::Csi3(buf1, buf2, buf3);
|
||||
}
|
||||
(b'n', State::Csi2(_, _)) => {
|
||||
// Report Cursor Position, skip it
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b't', State::Csi2(_, _)) => {
|
||||
// Window manipulation, skip it
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b'H', State::Csi2(ref x, ref y)) => {
|
||||
//Cursor Position [row;column] (default = [1,1]) (CUP).
|
||||
let orig_x = unsafe { std::str::from_utf8_unchecked(x) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
let orig_y = unsafe { std::str::from_utf8_unchecked(y) }
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)),);
|
||||
debug!(
|
||||
"cursor set to ({},{}), cursor was: {:?}",
|
||||
orig_x, orig_y, cursor
|
||||
);
|
||||
if orig_x - 1 <= terminal_size.0 && orig_y - 1 <= terminal_size.1 {
|
||||
cursor.0 = orig_x - 1;
|
||||
cursor.1 = orig_y - 1;
|
||||
}
|
||||
debug!("cursor became: {:?}", cursor);
|
||||
*state = State::Normal;
|
||||
}
|
||||
(c, State::Csi2(_, ref mut buf)) if c >= b'0' && c <= b'9' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Csi2(ref buf1, ref buf2)) => {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b't', State::Csi3(_, _, _)) => {
|
||||
// Window manipulation, skip it
|
||||
*state = State::Normal;
|
||||
}
|
||||
|
||||
(c, State::Csi3(_, _, ref mut buf)) if c >= b'0' && c <= b'9' => {
|
||||
buf.push(c);
|
||||
}
|
||||
(c, State::Csi3(ref buf1, ref buf2, ref buf3)) => {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
/* other stuff */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
/* ******************* */
|
||||
(c, State::G0) => {
|
||||
debug!("sending {}", EscCode::from((&(*state), byte)));
|
||||
*state = State::Normal;
|
||||
}
|
||||
(b, s) => {
|
||||
debug!("unrecognised: byte is {} and state is {:?}", b as char, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -64,7 +64,10 @@ impl From<RefreshEvent> for ThreadEvent {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum ForkType {
|
||||
Finished, // Already finished fork, we only want to restore input/output
|
||||
/// Already finished fork, we only want to restore input/output
|
||||
Finished,
|
||||
/// Embed pty
|
||||
Embed,
|
||||
Generic(std::process::Child),
|
||||
NewDraft(File, std::process::Child),
|
||||
}
|
||||
|
@ -81,6 +84,7 @@ pub enum UIEvent {
|
|||
Input(Key),
|
||||
ExInput(Key),
|
||||
InsertInput(Key),
|
||||
EmbedInput(Key),
|
||||
RefreshMailbox((usize, FolderHash)), //view has changed to FolderHash mailbox
|
||||
//Quit?
|
||||
Resize,
|
||||
|
@ -111,9 +115,10 @@ impl From<RefreshEvent> for UIEvent {
|
|||
pub enum UIMode {
|
||||
Normal,
|
||||
Insert,
|
||||
/// Forward input to an embed pseudoterminal.
|
||||
Embed,
|
||||
Execute,
|
||||
Fork,
|
||||
Embed,
|
||||
}
|
||||
|
||||
impl fmt::Display for UIMode {
|
||||
|
|
Loading…
Reference in New Issue