Compare commits

...

4 Commits

  1. 14
      Cargo.lock
  2. 8
      src/bin.rs
  3. 1
      ui/Cargo.toml
  4. 303
      ui/src/components/mail/compose.rs
  5. 7
      ui/src/components/utilities/widgets.rs
  6. 17
      ui/src/conf/composing.rs
  7. 38
      ui/src/state.rs
  8. 1
      ui/src/terminal.rs
  9. 318
      ui/src/terminal/embed.rs
  10. 890
      ui/src/terminal/embed/grid.rs
  11. 82
      ui/src/terminal/keys.rs
  12. 15
      ui/src/types.rs

14
Cargo.lock

@ -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,6 +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.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)",
@ -1358,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"

8
src/bin.rs

@ -197,6 +197,8 @@ fn main() -> std::result::Result<(), std::io::Error> {
let signals = &[
/* Catch SIGWINCH to handle terminal resizing */
signal_hook::SIGWINCH,
/* Catch SIGCHLD to handle embed applications status change */
signal_hook::SIGCHLD,
];
let signal_recvr = notify(signals, sender)?;
@ -303,11 +305,17 @@ fn main() -> std::result::Result<(), std::io::Error> {
},
}
},
UIMode::Embed => state.redraw(),
UIMode::Fork => {
break 'inner; // `goto` 'reap loop, and wait on child.
},
}
},
ThreadEvent::InputRaw(raw_input) => {
state.rcv_event(UIEvent::EmbedInput(raw_input));
state.redraw();
},
ThreadEvent::RefreshMailbox(event) => {
state.refresh_event(*event);
state.redraw();

1
ui/Cargo.toml

@ -25,6 +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 = "0.15.0"
[features]
default = []

303
ui/src/components/mail/compose.rs

@ -21,9 +21,12 @@
use super::*;
use crate::terminal::embed::EmbedGrid;
use melib::Draft;
use mime_apps::query_mime_info;
use nix::sys::wait::WaitStatus;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
#[derive(Debug, PartialEq)]
enum Cursor {
@ -32,6 +35,31 @@ enum Cursor {
//Attachments,
}
#[derive(Debug)]
enum EmbedStatus {
Stopped(Arc<Mutex<EmbedGrid>>, File),
Running(Arc<Mutex<EmbedGrid>>, File),
}
impl std::ops::Deref for EmbedStatus {
type Target = Arc<Mutex<EmbedGrid>>;
fn deref(&self) -> &Arc<Mutex<EmbedGrid>> {
use EmbedStatus::*;
match self {
Stopped(ref e, _) | Running(ref e, _) => e,
}
}
}
impl std::ops::DerefMut for EmbedStatus {
fn deref_mut(&mut self) -> &mut Arc<Mutex<EmbedGrid>> {
use EmbedStatus::*;
match self {
Stopped(ref mut e, _) | Running(ref mut e, _) => e,
}
}
}
#[derive(Debug)]
pub struct Composer {
reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, thread_node_index)
@ -44,6 +72,9 @@ pub struct Composer {
form: FormWidget,
mode: ViewMode,
body_area: Area, // Cache body_area in case we need to replace it with a pseudoterminal
embed: Option<EmbedStatus>,
sign_mail: ToggleFlag,
dirty: bool,
has_changes: bool,
@ -67,6 +98,8 @@ impl Default for Composer {
sign_mail: ToggleFlag::Unset,
dirty: true,
has_changes: false,
body_area: ((0, 0), (0, 0)),
embed: None,
initialized: false,
id: ComponentId::new_v4(),
}
@ -77,6 +110,7 @@ impl Default for Composer {
enum ViewMode {
Discard(Uuid, Selector<char>),
Edit,
Embed,
SelectRecipients(Selector<Address>),
ThreadView,
}
@ -97,6 +131,14 @@ impl ViewMode {
false
}
}
fn is_embed(&self) -> bool {
if let ViewMode::Embed = self {
true
} else {
false
}
}
}
impl fmt::Display for Composer {
@ -211,10 +253,9 @@ impl Composer {
fn update_draft(&mut self) {
let header_values = self.form.values_mut();
let draft_header_map = self.draft.headers_mut();
/* avoid extra allocations by updating values instead of inserting */
for (k, v) in draft_header_map.iter_mut() {
if let Some(vn) = header_values.remove(k) {
std::mem::swap(v, &mut vn.into_string());
if let Some(ref vn) = header_values.get(k) {
*v = vn.as_str().to_string();
}
}
}
@ -504,8 +545,44 @@ impl Component for Composer {
/* Regardless of view mode, do the following */
self.form.draw(grid, header_area, context);
self.pager.set_dirty();
self.pager.draw(grid, body_area, context);
if let Some(ref mut embed_pty) = self.embed {
clear_area(grid, body_area);
match embed_pty {
EmbedStatus::Running(_, _) => {
let mut guard = embed_pty.lock().unwrap();
copy_area(
grid,
&guard.grid,
body_area,
((0, 0), pos_dec(guard.terminal_size, (1, 1))),
);
guard.set_terminal_size((width!(body_area), height!(body_area)));
context.dirty_areas.push_back(body_area);
self.dirty = false;
return;
}
EmbedStatus::Stopped(_, _) => {
write_string_to_grid(
"process has stopped, press 'e' to re-activate",
grid,
Color::Default,
Color::Default,
Attr::Default,
body_area,
false,
);
context.dirty_areas.push_back(body_area);
self.dirty = false;
return;
}
}
} else {
self.pager.set_dirty();
self.pager.draw(grid, body_area, context);
}
self.body_area = body_area;
if self.cursor == Cursor::Body {
change_colors(
grid,
@ -529,7 +606,7 @@ impl Component for Composer {
}
match self.mode {
ViewMode::ThreadView | ViewMode::Edit => {}
ViewMode::ThreadView | ViewMode::Edit | ViewMode::Embed => {}
ViewMode::SelectRecipients(ref mut s) => {
s.draw(grid, center_area(area, s.content.size()), context);
}
@ -685,6 +762,15 @@ impl Component for Composer {
match *event {
UIEvent::Resize => {
self.set_dirty();
if let Some(ref mut embed_pty) = self.embed {
match embed_pty {
EmbedStatus::Running(_, _) => {
let mut guard = embed_pty.lock().unwrap();
guard.grid.clear(Cell::default());
}
_ => {}
}
}
}
/*
/* Switch e-mail From: field to the `left` configured account. */
@ -744,9 +830,163 @@ impl Component for Composer {
}
return true;
}
UIEvent::ChildStatusExited(ref pid, _)
if self.embed.is_some()
&& *pid
== self
.embed
.as_ref()
.map(|e| e.lock().unwrap().child_pid)
.unwrap() =>
{
self.embed = None;
self.set_dirty();
self.mode = ViewMode::Edit;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
}
UIEvent::ChildStatusStopped(ref pid)
if self.embed.is_some()
&& *pid
== self
.embed
.as_ref()
.map(|e| e.lock().unwrap().child_pid)
.unwrap() =>
{
match self.embed.take() {
Some(EmbedStatus::Running(e, f)) | Some(EmbedStatus::Stopped(e, f)) => {
self.embed = Some(EmbedStatus::Stopped(e, f));
}
_ => {}
}
self.set_dirty();
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
}
UIEvent::ChildStatusContinued(ref pid)
if self.embed.is_some()
&& *pid
== self
.embed
.as_ref()
.map(|e| e.lock().unwrap().child_pid)
.unwrap() =>
{
match self.embed.take() {
Some(EmbedStatus::Running(e, f)) | Some(EmbedStatus::Stopped(e, f)) => {
self.embed = Some(EmbedStatus::Running(e, f));
}
_ => {}
}
self.set_dirty();
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
}
UIEvent::EmbedInput((Key::Ctrl('z'), _)) => {
self.embed.as_ref().unwrap().lock().unwrap().stop();
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
self.dirty = true;
}
UIEvent::EmbedInput((ref k, ref b)) => {
use std::io::Write;
if let Some(ref mut embed) = self.embed {
let mut embed_guard = embed.lock().unwrap();
if embed_guard.stdin.write_all(b).is_err() {
match embed_guard.is_active() {
Ok(WaitStatus::Exited(_, exit_code)) => {
drop(embed_guard);
if exit_code != 0 {
context.replies.push_back(UIEvent::Notification(
None,
format!(
"Subprocess has exited with exit code {}",
exit_code
),
Some(NotificationType::ERROR),
));
} else if let EmbedStatus::Running(_, f) = embed {
let result = f.read_to_string();
match Draft::from_str(result.as_str()) {
Ok(mut new_draft) => {
std::mem::swap(
self.draft.attachments_mut(),
new_draft.attachments_mut(),
);
if self.draft != new_draft {
self.has_changes = true;
}
self.draft = new_draft;
}
Err(_) => {
context.replies.push_back(UIEvent::Notification(
None,
"Could not parse draft headers correctly. The invalid text has been set as the body of your draft".to_string(),
Some(NotificationType::ERROR),
));
}
}
self.initialized = false;
}
self.embed = None;
self.mode = ViewMode::Edit;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
}
Ok(WaitStatus::Stopped(_, _)) => {
drop(embed_guard);
match self.embed.take() {
Some(EmbedStatus::Running(e, f))
| Some(EmbedStatus::Stopped(e, f)) => {
self.embed = Some(EmbedStatus::Stopped(e, f));
}
_ => {}
}
self.dirty = true;
return true;
}
Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::StillAlive) => {
context
.replies
.push_back(UIEvent::EmbedInput((k.clone(), b.to_vec())));
return true;
}
e => {
context.replies.push_back(UIEvent::Notification(
None,
format!("Subprocess has exited with reason {:?}", e),
Some(NotificationType::ERROR),
));
drop(embed_guard);
self.embed = None;
self.mode = ViewMode::Edit;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
}
}
}
}
self.set_dirty();
return true;
}
UIEvent::Input(Key::Char('e')) if self.mode.is_embed() => {
self.embed.as_ref().unwrap().lock().unwrap().wake_up();
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
self.set_dirty();
return true;
}
UIEvent::Input(Key::Char('e')) => {
/* Edit draft in $EDITOR */
use std::process::{Command, Stdio};
let settings = &context.settings;
let editor = if let Some(editor_cmd) = settings.composing.editor_cmd.as_ref() {
editor_cmd.to_string()
@ -763,19 +1003,37 @@ impl Component for Composer {
Ok(v) => v,
}
};
/* Kill input thread so that spawned command can be sole receiver of stdin */
{
context.input_kill();
}
/* update Draft's headers based on form values */
self.update_draft();
let f = create_temp_file(
self.draft.to_string().unwrap().as_str().as_bytes(),
None,
None,
true,
false,
);
if settings.composing.embed {
self.embed = Some(EmbedStatus::Running(
crate::terminal::embed::create_pty(
self.body_area,
[editor, f.path().display().to_string()].join(" "),
)
.unwrap(),
f,
));
self.dirty = true;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
self.mode = ViewMode::Embed;
return true;
}
use std::process::{Command, Stdio};
/* Kill input thread so that spawned command can be sole receiver of stdin */
{
context.input_kill();
}
let parts = split_command!(editor);
let (cmd, args) = (parts[0], &parts[1..]);
if let Err(e) = Command::new(cmd)
@ -866,14 +1124,19 @@ impl Component for Composer {
}
fn is_dirty(&self) -> bool {
self.dirty
|| self.pager.is_dirty()
|| self
.reply_context
.as_ref()
.map(|(_, p)| p.is_dirty())
.unwrap_or(false)
|| self.form.is_dirty()
match self.mode {
ViewMode::Embed => true,
_ => {
self.dirty
|| self.pager.is_dirty()
|| self
.reply_context
.as_ref()
.map(|(_, p)| p.is_dirty())
.unwrap_or(false)
|| self.form.is_dirty()
}
}
}
fn set_dirty(&mut self) {

7
ui/src/components/utilities/widgets.rs

@ -57,6 +57,13 @@ impl Field {
self.as_str().is_empty()
}
pub fn to_string(&self) -> String {
match self {
Text(ref s, _) => s.as_str().to_string(),
Choice(ref v, ref cursor) => v[*cursor].clone(),
}
}
pub fn into_string(self) -> String {
match self {
Text(s, _) => s.into_string(),

17
ui/src/conf/composing.rs

@ -18,13 +18,28 @@
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::default_vals::{none, true_val};
/// Settings for writing and sending new e-mail
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ComposingSettings {
/// A command to pipe new emails to
/// Required
pub mailer_cmd: String,
/// Command to launch editor. Can have arguments. Draft filename is given as the last argument. If it's missing, the environment variable $EDITOR is looked up.
#[serde(default = "none")]
pub editor_cmd: Option<String>,
/// Embed editor (for terminal interfaces) instead of forking and waiting.
#[serde(default = "true_val")]
pub embed: bool,
}
impl Default for ComposingSettings {
fn default() -> Self {
ComposingSettings {
mailer_cmd: String::new(),
editor_cmd: None,
embed: true,
}
}
}

38
ui/src/state.rs

@ -44,33 +44,39 @@ use termion::{clear, cursor};
pub type StateStdout = termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>;
struct InputHandler {
rx: Receiver<bool>,
tx: Sender<bool>,
rx: Receiver<InputCommand>,
tx: Sender<InputCommand>,
}
impl InputHandler {
fn restore(&self, tx: Sender<ThreadEvent>) {
let stdin = std::io::stdin();
let rx = self.rx.clone();
thread::Builder::new()
.name("input-thread".to_string())
.spawn(move || {
get_events(
stdin,
|k| {
tx.send(ThreadEvent::Input(k)).unwrap();
},
|| {
tx.send(ThreadEvent::UIEvent(UIEvent::ChangeMode(UIMode::Fork)))
.unwrap();
|i| {
tx.send(ThreadEvent::InputRaw(i)).unwrap();
},
&rx,
)
})
.unwrap();
}
fn kill(&self) {
self.tx.send(false).unwrap();
self.tx.send(InputCommand::Kill).unwrap();
}
fn switch_to_raw(&self) {
self.tx.send(InputCommand::Raw).unwrap();
}
fn switch_from_raw(&self) {
self.tx.send(InputCommand::NoRaw).unwrap();
}
}
@ -98,9 +104,19 @@ impl Context {
pub fn replies(&mut self) -> Vec<UIEvent> {
self.replies.drain(0..).collect()
}
pub fn input_kill(&self) {
self.input.kill();
}
pub fn input_from_raw(&self) {
self.input.switch_from_raw();
}
pub fn input_to_raw(&self) {
self.input.switch_to_raw();
}
pub fn restore_input(&self) {
self.input.restore(self.sender.clone());
}
@ -584,10 +600,16 @@ impl State {
return;
}
UIEvent::ChangeMode(m) => {
if self.mode == UIMode::Embed {
self.context.input_from_raw();
}
self.context
.sender
.send(ThreadEvent::UIEvent(UIEvent::ChangeMode(m)))
.unwrap();
if m == UIMode::Embed {
self.context.input_to_raw();
}
}
_ => {}
}

1
ui/src/terminal.rs

@ -29,6 +29,7 @@ mod position;
mod cells;
#[macro_use]
mod keys;
pub mod embed;
mod text_editing;
pub use self::cells::*;
pub use self::keys::*;

318
ui/src/terminal/embed.rs

@ -0,0 +1,318 @@
use crate::split_command;
use crate::terminal::position::Area;
use crate::terminal::position::*;
use melib::log;
use melib::ERROR;
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, wait::waitpid};
use nix::unistd::{dup2, fork, ForkResult};
use std::ffi::CString;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
mod grid;
pub use grid::EmbedGrid;
// ioctl command to set window size of pty:
use libc::TIOCSWINSZ;
use std::path::Path;
use std::io::Read;
use std::io::Write;
use std::sync::{Arc, Mutex};
// Macro generated function that calls ioctl to set window size of slave pty end
ioctl_write_ptr_bad!(set_window_size, TIOCSWINSZ, Winsize);
pub fn create_pty(area: Area, command: String) -> nix::Result<Arc<Mutex<EmbedGrid>>> {
// Open a new PTY master
let master_fd = posix_openpt(OFlag::O_RDWR)?;
// Allow a slave to be generated for it
grantpt(&master_fd)?;
unlockpt(&master_fd)?;
// Get the name of the slave
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 winsize = Winsize {
ws_row: 20,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
let master_fd = master_fd.clone().into_raw_fd();
unsafe { set_window_size(master_fd, &winsize).unwrap() };
}
let child_pid = match fork() {
Ok(ForkResult::Child) => {
/* Open slave end for pseudoterminal */
let slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, stat::Mode::empty())?;
let child_pid = match fork() {
Ok(ForkResult::Child) => {
// assign stdin, stdout, stderr to the tty
dup2(slave_fd, STDIN_FILENO).unwrap();
dup2(slave_fd, STDOUT_FILENO).unwrap();
dup2(slave_fd, STDERR_FILENO).unwrap();
let parts = split_command!(command);
let (cmd, _) = (parts[0], &parts[1..]);
if let Err(e) = nix::unistd::execv(
&CString::new(cmd).unwrap(),
&parts
.iter()
.map(|&a| CString::new(a).unwrap())
.collect::<Vec<CString>>(),
) {
log(format!("Could not execute `{}`: {}", command, e,), ERROR);
std::process::exit(-1);
}
/* This path shouldn't be executed. */
std::process::exit(0);
}
Ok(ForkResult::Parent { child }) => child,
Err(e) => panic!(e),
};
waitpid(child_pid, None).unwrap();
std::process::exit(0);
}
Ok(ForkResult::Parent { child }) => child,
Err(e) => panic!(e),
};
let stdin = unsafe { std::fs::File::from_raw_fd(master_fd.clone().into_raw_fd()) };
let mut embed_grid = EmbedGrid::new(stdin, child_pid);
embed_grid.set_terminal_size((width!(area), height!(area)));
let grid = Arc::new(Mutex::new(embed_grid));
let grid_ = grid.clone();
std::thread::Builder::new()
.spawn(move || {
let master_fd = master_fd.into_raw_fd();
let master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
forward_pty_translate_escape_codes(master_file, grid_);
})
.unwrap();
Ok(grid)
}
fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, grid: Arc<Mutex<EmbedGrid>>) {
let mut bytes_iter = pty_fd.bytes();
debug!("waiting for bytes");
while let Some(Ok(byte)) = bytes_iter.next() {
debug!("got byte {}", byte as char);
grid.lock().unwrap().process_byte(byte);
}
}
#[derive(Debug)]
pub enum State {
ExpectingControlChar,
G0, // Designate G0 Character Set
Osc1(Vec<u8>), //ESC ] Operating System Command (OSC is 0x9d).
Osc2(Vec<u8>, Vec<u8>),
Csi, // ESC [ Control Sequence Introducer (CSI is 0x9b).
Csi1(Vec<u8>),
Csi2(Vec<u8>, Vec<u8>),
Csi3(Vec<u8>, Vec<u8>, Vec<u8>),
CsiQ(Vec<u8>),
Normal,
}
/* Used for debugging */
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;
EscCode(s, b)
}
}
impl std::fmt::Display for EscCode<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use State::*;
macro_rules! unsafestr {
($buf:ident) => {
unsafe { std::str::from_utf8_unchecked($buf) }
};
}
match self {
EscCode(G0, b'B') => write!(f, "ESC(B\t\tG0 USASCII charset set"),
EscCode(G0, c) => write!(f, "ESC({}\t\tG0 charset set", *c as char),
EscCode(Osc1(ref buf), ref c) => {
write!(f, "ESC]{}{}\t\tOSC", unsafestr!(buf), *c as char)
}
EscCode(Osc2(ref buf1, ref buf2), c) => write!(
f,
"ESC]{};{}{}\t\tOSC [UNKNOWN]",
unsafestr!(buf1),
unsafestr!(buf2),
*c as char
),
EscCode(Csi, b'm') => write!(
f,
"ESC[m\t\tCSI Character Attributes | Set Attr and Color to Normal (default)"
),
EscCode(Csi, b'K') => write!(
f,
"ESC[K\t\tCSI Erase from the cursor to the end of the line"
),
EscCode(Csi, b'J') => write!(
f,
"ESC[J\t\tCSI Erase from the cursor to the end of the screen"
),
EscCode(Csi, b'H') => write!(f, "ESC[H\t\tCSI Move the cursor to home position."),
EscCode(Csi, c) => write!(f, "ESC[{}\t\tCSI [UNKNOWN]", *c as char),
EscCode(Csi1(ref buf), b'm') => write!(
f,
"ESC[{}m\t\tCSI Character Attributes | Set fg, bg color",
unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'n') => write!(
f,
"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",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'C') => write!(
f,
"ESC[{buf}C\t\tCSI Cursor Forward {buf} Times",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'D') => write!(
f,
"ESC[{buf}D\t\tCSI Cursor Backward {buf} Times",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'E') => write!(
f,
"ESC[{buf}E\t\tCSI Cursor Next Line {buf} Times",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'F') => write!(
f,
"ESC[{buf}F\t\tCSI Cursor Preceding Line {buf} Times",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'G') => write!(
f,
"ESC[{buf}G\t\tCursor Character Absolute [column={buf}] (default = [row,1])",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'P') => write!(
f,
"ESC[{buf}P\t\tDelete P s Character(s) (default = 1) (DCH). ",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'S') => write!(
f,
"ESC[{buf}S\t\tCSI P s S Scroll up P s lines (default = 1) (SU), VT420, EC",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), b'J') => write!(
f,
"Erase in display {buf}",
buf = unsafestr!(buf)
),
EscCode(Csi1(ref buf), c) => {
write!(f, "ESC[{}{}\t\tCSI [UNKNOWN]", unsafestr!(buf), *c as char)
}
EscCode(Csi2(ref buf1, ref buf2), b'r') => write!(
f,
"ESC[{};{}r\t\tCSI Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100.",
unsafestr!(buf1),
unsafestr!(buf2),
),
EscCode(Csi2(ref buf1, ref buf2), c) => write!(
f,
"ESC[{};{}{}\t\tCSI",
unsafestr!(buf1),
unsafestr!(buf2),
*c as char
),
EscCode(Csi3(ref buf1, ref buf2, ref buf3), b'm') => write!(
f,
"ESC[{};{};{}m\t\tCSI Character Attributes | Set fg, bg color",
unsafestr!(buf1),
unsafestr!(buf2),
unsafestr!(buf3),
),
EscCode(Csi3(ref buf1, ref buf2, ref buf3), c) => write!(
f,
"ESC[{};{};{}{}\t\tCSI [UNKNOWN]",
unsafestr!(buf1),
unsafestr!(buf2),
unsafestr!(buf3),
*c as char
),
EscCode(CsiQ(ref buf), b's') => write!(
f,
"ESC[?{}r\t\tCSI Save DEC Private Mode Values",
unsafestr!(buf)
),
EscCode(CsiQ(ref buf), b'r') => write!(
f,
"ESC[?{}r\t\tCSI Restore DEC Private Mode Values",
unsafestr!(buf)
),
EscCode(CsiQ(ref buf), b'h') if buf == b"25" => write!(
f,
"ESC[?25h\t\tCSI DEC Private Mode Set (DECSET) show cursor",
),
EscCode(CsiQ(ref buf), b'h') if buf == b"12" => write!(
f,
"ESC[?12h\t\tCSI DEC Private Mode Set (DECSET) Start Blinking Cursor.",
),
EscCode(CsiQ(ref buf), b'h') => write!(
f,
"ESC[?{}h\t\tCSI DEC Private Mode Set (DECSET). [UNKNOWN]",
unsafestr!(buf)
),
EscCode(CsiQ(ref buf), b'l') if buf == b"12" => write!(
f,
"ESC[?12l\t\tCSI DEC Private Mode Set (DECSET) Stop Blinking Cursor",
),
EscCode(CsiQ(ref buf), b'l') if buf == b"25" => write!(
f,
"ESC[?25l\t\tCSI DEC Private Mode Set (DECSET) hide cursor",
),
EscCode(CsiQ(ref buf), c) => {
write!(f, "ESC[?{}{}\t\tCSI [UNKNOWN]", unsafestr!(buf), *c as char)
}
EscCode(unknown, c) => {
write!(f, "{:?}{} [UNKNOWN]", unknown, c)
}
}
}
}

890
ui/src/terminal/embed/grid.rs

@ -0,0 +1,890 @@
use super::*;
use crate::terminal::cells::*;
use melib::error::{MeliError, Result};
use nix::sys::wait::WaitStatus;
use nix::sys::wait::{waitpid, WaitPidFlag};
/**
* `EmbedGrid` manages the terminal grid state of the embed process.
*
* The embed process sends bytes to the master end (see super mod) and interprets them in a state
* machine stored in `State`. Escape codes are translated as changes to the grid, eg changes in a
* cell's colors.
*
* The main process copies the grid whenever the actual terminal is redrawn.
**/
/// In a scroll region up and down cursor movements shift the region vertically. The new lines are
/// empty.
#[derive(Debug)]
struct ScrollRegion {
top: usize,
bottom: usize,
}
#[derive(Debug)]
pub struct EmbedGrid {
cursor: (usize, usize),
/// [top;bottom]
scroll_region: ScrollRegion,
pub grid: CellBuffer,
pub state: State,
pub stdin: std::fs::File,
/// Pid of the embed process
pub child_pid: nix::unistd::Pid,
/// (width, height)
pub terminal_size: (usize, usize),
fg_color: Color,
bg_color: Color,
/// Store the fg/bg color when highlighting the cell where the cursor is so that it can be
/// restored afterwards
prev_fg_color: Option<Color>,
prev_bg_color: Option<Color>,
show_cursor: bool,
/// Store state in case a multi-byte character is encountered
codepoints: CodepointBuf,
}
#[derive(Debug, PartialEq)]
enum CodepointBuf {
None,
TwoCodepoints(Vec<u8>),
ThreeCodepoints(Vec<u8>),
FourCodepoints(Vec<u8>),
}
impl EmbedGrid {
pub fn new(stdin: std::fs::File, child_pid: nix::unistd::Pid) -> Self {
EmbedGrid {
cursor: (0, 0),
scroll_region: ScrollRegion { top: 0, bottom: 0 },
terminal_size: (0, 0),
grid: CellBuffer::default(),
state: State::Normal,
stdin,
child_pid,
fg_color: Color::Default,
bg_color: Color::Default,
prev_fg_color: None,
prev_bg_color: None,
show_cursor: true,
codepoints: CodepointBuf::None,
}
}
pub fn set_terminal_size(&mut self, new_val: (usize, usize)) {
if new_val == self.terminal_size {
return;
}
debug!("resizing to {:?}", new_val);
if self.scroll_region.top == 0 && self.scroll_region.bottom == self.terminal_size.1 {
self.scroll_region.bottom = new_val.1;
}
self.terminal_size = new_val;
self.grid.resize(new_val.0, new_val.1, Cell::default());
self.grid.clear(Cell::default());
self.cursor = (0, 0);
use std::convert::TryFrom;
let winsize = Winsize {
ws_row: <u16>::try_from(new_val.1).unwrap(),
ws_col: <u16>::try_from(new_val.0).unwrap(),
ws_xpixel: 0,
ws_ypixel: 0,
};
let master_fd = self.stdin.as_raw_fd();
unsafe { set_window_size(master_fd, &winsize).unwrap() };
nix::sys::signal::kill(self.child_pid, nix::sys::signal::SIGWINCH).unwrap();
}
pub fn wake_up(&self) {
nix::sys::signal::kill(self.child_pid, nix::sys::signal::SIGCONT).unwrap();
}
pub fn stop(&self) {
debug!("stopping");
nix::sys::signal::kill(debug!(self.child_pid), nix::sys::signal::SIGSTOP).unwrap();
}
pub fn is_active(&self) -> Result<WaitStatus> {
debug!(waitpid(self.child_pid, Some(WaitPidFlag::WNOHANG),))
.map_err(|e| MeliError::new(e.to_string()))
}
pub fn process_byte(&mut self, byte: u8) {
let EmbedGrid {
ref mut cursor,
ref mut scroll_region,
ref terminal_size,
ref mut grid,
ref mut state,
ref mut stdin,
ref mut fg_color,
ref mut bg_color,
ref mut prev_fg_color,
ref mut prev_bg_color,
ref mut codepoints,
ref mut show_cursor,
child_pid: _,
} = self;
macro_rules! increase_cursor_x {
() => {
if cursor.0 + 1 < terminal_size.0 {
cursor.0 += 1;
}
};
}
macro_rules! cursor_x {
() => {{
if cursor.0 >= terminal_size.0 {
cursor.0 = terminal_size.0.saturating_sub(1);
}
cursor.0
}};
}
macro_rules! cursor_y {
() => {
std::cmp::min(
cursor.1 + scroll_region.top,
terminal_size.1.saturating_sub(1),
)
};
}
macro_rules! cursor_val {
() => {
(cursor_x!(), cursor_y!())
};
}
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;
}
(b'J', State::ExpectingControlChar) => {
// ESCJ Erase from the cursor to the end of the screen
debug!("sending {}", EscCode::from((&(*state), byte)));
debug!("erasing from {:?} to {:?}", cursor, terminal_size);
for y in cursor.1..terminal_size.1 {
for x in cursor.0..terminal_size.0 {
grid[(x, y)] = Cell::default();
}
}
*state = State::Normal;
}
(b'K', State::ExpectingControlChar) => {
// ESCK Erase from the cursor to the end of the line
debug!("sending {}", EscCode::from((&(*state), byte)));
for x in cursor.0..terminal_size.0 {
grid[(x, cursor.1)] = Cell::default();
}
*state = State::Normal;
}
(_, 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);
}
(_, State::Osc1(_)) => {
debug!("ignoring unknown code {}", EscCode::from((&(*state), byte)));
*state = State::Normal;
}
(_, State::Osc2(_, _)) => {
debug!("ignoring unknown code {}", EscCode::from((&(*state), byte)));
*state = State::Normal;
}
/* Normal */
(b'\r', State::Normal) => {
debug!("carriage return x-> 0, cursor was: {:?}", cursor);
cursor.0 = 0;
debug!("cursor became: {:?}", cursor);
}
(b'\n', State::Normal) => {
//debug!("setting cell {:?} char '{}'", cursor, c as char);
debug!("newline y-> y+1, cursor was: {:?}", cursor);
if cursor.1 + 1 < terminal_size.1 {
cursor.1 += 1;
}
debug!("cursor became: {:?}", cursor);
}
(b'', State::Normal) => {
debug!("Visual bell ^G, ignoring {:?}", cursor);
}
(0x08, State::Normal) => {
/* Backspace */
debug!("backspace x-> x-1, cursor was: {:?}", cursor);
if cursor.0 > 0 {
cursor.0 -= 1;
}
debug!("cursor became: {:?}", cursor);
}
(c, State::Normal) => {
/* Character to be printed. */
if *codepoints == CodepointBuf::None && c & 0x80 == 0 {
/* This is a one byte char */
grid[cursor_val!()].set_ch(c as char);
} else {
match codepoints {
CodepointBuf::None if c & 0b1110_0000 == 0b1100_0000 => {
*codepoints = CodepointBuf::TwoCodepoints(vec![c]);
}
CodepointBuf::None if c & 0b1111_0000 == 0b1110_0000 => {
*codepoints = CodepointBuf::ThreeCodepoints(vec![c]);
}
CodepointBuf::None if c & 0b1111_1000 == 0b1111_0000 => {
*codepoints = CodepointBuf::FourCodepoints(vec![c]);
}
CodepointBuf::TwoCodepoints(buf) => {
grid[cursor_val!()].set_ch(
unsafe { std::str::from_utf8_unchecked(&[buf[0], c]) }
.chars()
.next()
.unwrap(),
);
*codepoints = CodepointBuf::None;
}
CodepointBuf::ThreeCodepoints(buf) if buf.len() == 2 => {
grid[cursor_val!()].set_ch(
unsafe { std::str::from_utf8_unchecked(&[buf[0], buf[1], c]) }
.chars()
.next()
.unwrap(),
);
*codepoints = CodepointBuf::None;
}
CodepointBuf::ThreeCodepoints(buf) => {
buf.push(c);
return;
}
CodepointBuf::FourCodepoints(buf) if buf.len() == 3 => {
grid[cursor_val!()].set_ch(
unsafe {
std::str::from_utf8_unchecked(&[buf[0], buf[1], buf[2], c])
}
.chars()
.next()
.unwrap(),
);
*codepoints = CodepointBuf::None;
}
CodepointBuf::FourCodepoints(buf) => {
buf.push(c);
return;
}
_ => {
debug!(
"invalid utf8 sequence: codepoints = {:?} and c={}",
codepoints, c
);
*codepoints = CodepointBuf::None;
}
}
}
grid[cursor_val!()].set_fg(*fg_color);
grid[cursor_val!()].set_bg(*bg_color);
increase_cursor_x!();
}
(b'u', State::Csi) => {
/* restore cursor */
debug!("restore cursor {}", EscCode::from((&(*state), byte)));
*show_cursor = true;
*state = State::Normal;
}
(b'm', State::Csi) => {
/* Reset character Attributes (SGR). Ps = 0 -> Normal (default), VT100 */
debug!("{}", EscCode::from((&(*state), byte)));
*fg_color = Color::Default;
*bg_color = Color::Default;
grid[cursor_val!()].set_fg(Color::Default);
grid[cursor_val!()].set_bg(Color::Default);
*state = State::Normal;
}
(b'H', State::Csi) => {
/* move cursor to (1,1) */
debug!("{}", EscCode::from((&(*state), byte)),);
debug!("move cursor to (1,1) cursor before: {:?}", *cursor);
*cursor = (0, 0);
debug!("cursor after: {:?}", *cursor);
*state = State::Normal;
}
(b'P', State::Csi) => {
/* delete one character */
debug!("{}", EscCode::from((&(*state), byte)),);
grid[cursor_val!()].set_ch(' ');
*state = State::Normal;
}
(b'C', State::Csi) => {
// ESC[C CSI Cursor Forward one Time
debug!("cursor forward one time, cursor was: {:?}", cursor);
cursor.0 = std::cmp::min(cursor.0 + 1, terminal_size.0.saturating_sub(1));
debug!("cursor became: {:?}", cursor);
*state = State::Normal;
}
/* CSI ? stuff */
(c, State::CsiQ(ref mut buf)) if c >= b'0' && c <= b'9' => {
buf.push(c);
}
(b'h', State::CsiQ(ref buf)) => {
match buf.as_slice() {
b"25" => {
*show_cursor = true;
*prev_fg_color = Some(grid[cursor_val!()].fg());
*prev_bg_color = Some(grid[cursor_val!()].bg());
grid[cursor_val!()].set_fg(Color::Black);
grid[cursor_val!()].set_bg(Color::White);
}
_ => {}
}
debug!("{}", EscCode::from((&(*state), byte)));
*state = State::Normal;
}
(b'l', State::CsiQ(ref mut buf)) => {
match buf.as_slice() {
b"25" => {
*show_cursor = false;
if let Some(fg_color) = prev_fg_color.take() {
grid[cursor_val!()].set_fg(fg_color);
} else {
grid[cursor_val!()].