use std::{
env, fs, io,
mem::ManuallyDrop,
os::unix::{
ffi::OsStrExt,
io::{AsRawFd, FromRawFd, RawFd},
net::{UnixListener, UnixStream},
},
path::Path,
process::Child,
time::Duration,
};
use crate::{
application::{ApplicationConfig, ClientApplication},
editor_utils::hash_bytes,
platform::{BufPool, Key, PooledBuf, ProcessTag},
Args,
};
pub(crate) fn run(
config: ApplicationConfig,
server_fn: fn(ApplicationConfig, UnixListener),
client_fn: fn(Args, UnixStream),
) {
let mut session_path = String::new();
session_path.push_str("/tmp/");
session_path.push_str(env!("CARGO_PKG_NAME"));
session_path.push('/');
match config.args.session {
Some(ref name) => session_path.push_str(name),
None => {
use io::Write;
let current_dir = env::current_dir().expect("could not retrieve the current directory");
let current_dir_bytes = current_dir.as_os_str().as_bytes();
let current_directory_hash = hash_bytes(current_dir_bytes);
let mut hash_buf = [0u8; 16];
let mut cursor = io::Cursor::new(&mut hash_buf[..]);
write!(&mut cursor, "{:x}", current_directory_hash).unwrap();
let len = cursor.position() as usize;
let name = std::str::from_utf8(&hash_buf[..len]).unwrap();
session_path.push_str(name);
}
}
if config.args.print_session {
print!("{}", session_path);
return;
}
let session_path = Path::new(&session_path);
fn start_server(session_path: &Path) -> UnixListener {
if let Some(dir) = session_path.parent() {
if !dir.exists() {
let _ = fs::create_dir(dir);
}
}
let _ = fs::remove_file(session_path);
UnixListener::bind(session_path).expect("could not start unix domain socket server")
}
if config.args.server {
server_fn(config, start_server(session_path));
let _ = fs::remove_file(session_path);
} else {
match UnixStream::connect(session_path) {
Ok(stream) => client_fn(config.args, stream),
Err(_) => match unsafe { libc::fork() } {
-1 => panic!("could not start server"),
0 => {
server_fn(config, start_server(session_path));
let _ = fs::remove_file(session_path);
}
_ => loop {
match UnixStream::connect(session_path) {
Ok(stream) => {
client_fn(config.args, stream);
break;
}
Err(_) => std::thread::sleep(Duration::from_millis(100)),
}
},
},
}
}
}
pub(crate) fn is_pipped(fd: RawFd) -> bool {
unsafe { libc::isatty(fd) != true as _ }
}
pub(crate) struct Terminal {
fd: RawFd,
original_state: libc::termios,
}
impl Terminal {
pub fn new() -> Self {
let flags = libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC;
let fd = unsafe { libc::open("/dev/tty\0".as_ptr() as _, flags) };
if fd < 0 {
panic!("could not open terminal");
}
let original_state = unsafe {
let mut original_state = std::mem::zeroed();
libc::tcgetattr(fd, &mut original_state);
original_state
};
Self { fd, original_state }
}
pub fn to_file(&self) -> ManuallyDrop<fs::File> {
unsafe { ManuallyDrop::new(fs::File::from_raw_fd(self.fd)) }
}
pub fn enter_raw_mode(&self) {
let mut next_state = self.original_state.clone();
next_state.c_iflag &= !(libc::IGNBRK
| libc::BRKINT
| libc::PARMRK
| libc::ISTRIP
| libc::INLCR
| libc::IGNCR
| libc::ICRNL
| libc::IXON);
next_state.c_oflag &= !libc::OPOST;
next_state.c_cflag &= !(libc::CSIZE | libc::PARENB);
next_state.c_cflag |= libc::CS8;
next_state.c_lflag &= !(libc::ECHO | libc::ICANON | libc::ISIG | libc::IEXTEN);
next_state.c_lflag |= libc::NOFLSH;
next_state.c_cc[libc::VMIN] = 0;
next_state.c_cc[libc::VTIME] = 0;
unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &next_state) };
}
pub fn leave_raw_mode(&self) {
unsafe { libc::tcsetattr(self.fd, libc::TCSAFLUSH, &self.original_state) };
}
pub fn get_size(&self) -> (usize, usize) {
let mut size: libc::winsize = unsafe { std::mem::zeroed() };
let result = unsafe {
libc::ioctl(
self.fd,
libc::TIOCGWINSZ as _,
&mut size as *mut libc::winsize,
)
};
if result == -1 || size.ws_col == 0 || size.ws_row == 0 {
panic!("could not get terminal size");
}
(size.ws_col as _, size.ws_row as _)
}
pub fn parse_keys(&self, mut buf: &[u8], keys: &mut Vec<Key>) {
let backspace_code = self.original_state.c_cc[libc::VERASE];
loop {
let (key, rest) = match buf {
&[] => break,
&[b, ref rest @ ..] if b == backspace_code => (Key::Backspace, rest),
&[0x1b, b'[', b'5', b'~', ref rest @ ..] => (Key::PageUp, rest),
&[0x1b, b'[', b'6', b'~', ref rest @ ..] => (Key::PageDown, rest),
&[0x1b, b'[', b'A', ref rest @ ..] => (Key::Up, rest),
&[0x1b, b'[', b'B', ref rest @ ..] => (Key::Down, rest),
&[0x1b, b'[', b'C', ref rest @ ..] => (Key::Right, rest),
&[0x1b, b'[', b'D', ref rest @ ..] => (Key::Left, rest),
&[0x1b, b'[', b'1', b'~', ref rest @ ..]
| &[0x1b, b'[', b'7', b'~', ref rest @ ..]
| &[0x1b, b'[', b'H', ref rest @ ..]
| &[0x1b, b'O', b'H', ref rest @ ..] => (Key::Home, rest),
&[0x1b, b'[', b'4', b'~', ref rest @ ..]
| &[0x1b, b'[', b'8', b'~', ref rest @ ..]
| &[0x1b, b'[', b'F', ref rest @ ..]
| &[0x1b, b'O', b'F', ref rest @ ..] => (Key::End, rest),
&[0x1b, b'[', b'3', b'~', ref rest @ ..] => (Key::Delete, rest),
&[0x1b, ref rest @ ..] => (Key::Esc, rest),
&[0x8, ref rest @ ..] => (Key::Backspace, rest),
&[b'\r', ref rest @ ..] => (Key::Enter, rest),
&[b'\t', ref rest @ ..] => (Key::Tab, rest),
&[0x7f, ref rest @ ..] => (Key::Delete, rest),
&[b @ 0b0..=0b11111, ref rest @ ..] => {
let byte = b | 0b01100000;
(Key::Ctrl(byte as _), rest)
}
_ => match buf.iter().position(|b| b.is_ascii()).unwrap_or(buf.len()) {
0 => (Key::Char(buf[0] as _), &buf[1..]),
len => {
let (c, rest) = buf.split_at(len);
match std::str::from_utf8(c) {
Ok(s) => match s.chars().next() {
Some(c) => (Key::Char(c), rest),
None => (Key::None, rest),
},
Err(_) => (Key::None, rest),
}
}
},
};
buf = rest;
keys.push(key);
}
}
}
impl AsRawFd for Terminal {
fn as_raw_fd(&self) -> RawFd {
self.fd
}
}
impl Drop for Terminal {
fn drop(&mut self) {
self.leave_raw_mode()
}
}
pub(crate) fn read(fd: RawFd, buf: &mut [u8]) -> Result<usize, ()> {
let len = unsafe { libc::read(fd, buf.as_mut_ptr() as _, buf.len()) };
if len >= 0 {
Ok(len as _)
} else {
Err(())
}
}
pub(crate) fn write_all_bytes(fd: RawFd, mut buf: &[u8]) -> bool {
while !buf.is_empty() {
let len = unsafe { libc::write(fd, buf.as_ptr() as _, buf.len()) };
if len > 0 {
buf = &buf[len as usize..];
} else {
return false;
}
}
true
}
pub(crate) fn read_from_connection(
connection: &mut UnixStream,
buf_pool: &mut BufPool,
len: usize,
) -> Result<PooledBuf, ()> {
use io::Read;
let mut buf = buf_pool.acquire();
let write = buf.write_with_len(len);
match connection.read(write) {
Ok(0) | Err(_) => {
buf_pool.release(buf);
Err(())
}
Ok(len) => {
write.truncate(len);
Ok(buf)
}
}
}
pub(crate) struct Process {
alive: bool,
child: Child,
tag: ProcessTag,
buf_len: usize,
}
impl Process {
pub fn new(child: Child, tag: ProcessTag, buf_len: usize) -> Self {
Self {
alive: true,
child,
tag,
buf_len,
}
}
pub fn tag(&self) -> ProcessTag {
self.tag
}
pub fn try_as_raw_fd(&self) -> Option<RawFd> {
self.child.stdout.as_ref().map(|s| s.as_raw_fd())
}
pub fn read(&mut self, buf_pool: &mut BufPool) -> Result<Option<PooledBuf>, ()> {
use io::Read;
match self.child.stdout {
Some(ref mut stdout) => {
let mut buf = buf_pool.acquire();
let write = buf.write_with_len(self.buf_len);
match stdout.read(write) {
Ok(0) | Err(_) => {
buf_pool.release(buf);
Err(())
}
Ok(len) => {
write.truncate(len);
Ok(Some(buf))
}
}
}
None => Ok(None),
}
}
pub fn write(&mut self, buf: &[u8]) -> bool {
use io::Write;
match self.child.stdin {
Some(ref mut stdin) => stdin.write_all(buf).is_ok(),
None => true,
}
}
pub fn close_input(&mut self) {
self.child.stdin = None;
}
pub fn kill(&mut self) {
if !self.alive {
return;
}
self.alive = false;
let _ = self.child.kill();
let _ = self.child.wait();
}
}
impl Drop for Process {
fn drop(&mut self) {
self.kill();
self.alive = false;
}
}
pub(crate) fn suspend_process(application: &mut ClientApplication, terminal: &Option<Terminal>) {
application.restore_screen();
if let Some(terminal) = terminal {
terminal.leave_raw_mode();
}
unsafe { libc::raise(libc::SIGTSTP) };
if let Some(terminal) = terminal {
terminal.enter_raw_mode();
}
application.reinit_screen();
}