├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── lib.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "term_mux" 3 | version = "0.1.1" 4 | authors = ["Matthias Devlamynck "] 5 | license = "MIT" 6 | 7 | [dependencies] 8 | libc = "0" 9 | termion = "1" 10 | chan-signal = "0" 11 | 12 | [[bin]] 13 | name = "term_mux" 14 | doc = false 15 | 16 | [lib] 17 | doc = true 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matthias Devlamynck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # term_mux_rs (working title, I don't like it, see [#1](https://github.com/mdevlamynck/term_mux_rs/issues/1)) 2 | 3 | [![Build Status](https://travis-ci.org/mdevlamynck/term_mux_rs.svg?branch=master)](https://travis-ci.org/mdevlamynck/term_mux_rs) 4 | 5 | Terminal multiplexer in rust 6 | 7 | This project only supports GNU/Linux at the moment. Redox, macOS and Windows support may happen in the future (in that order of priority). 8 | 9 | # Installation 10 | 11 | You can use `cargo install` to install this project. It will compile the binary `term_mux` and install it in the `~/.cargo/bin` folder. Make sure this folder is in your path if you want to be able to run it directly. 12 | 13 | ```sh 14 | cargo install --git https://github.com/mdevlamynck/term_mux_rs 15 | 16 | # or you can specify a branch with --branch 17 | cargo install --git https://github.com/mdevlamynck/term_mux_rs --branch dev 18 | ``` 19 | 20 | If you want the last stable version (i.e. release), use the master branch. 21 | If you want the last development version, use the dev branch. 22 | 23 | This project is not ready to be used yet. Once the project is ready, it will be published on crates.io and you will be able to install the latest release with a simple `cargo install term_mux_rs`. 24 | 25 | # Hacking 26 | 27 | As any rust project, use `cargo` to build, run the project, run the tests or build the docs. 28 | 29 | ```sh 30 | cargo build # compile 31 | cargo run # launch term_mux 32 | cargo test # run tests 33 | cargo doc # build the docs 34 | cargo doc --open # build the docs and open them in your browser 35 | ``` 36 | 37 | As a bonus, if you want to see the full documentation, including the docs of private elements, use : 38 | 39 | ```sh 40 | cargo rustdoc -- --no-defaults --passes collapse-docs --passes unindent-comments --passes strip-priv-imports 41 | 42 | # or the version with --open 43 | cargo rustdoc --open -- --no-defaults --passes collapse-docs --passes unindent-comments --passes strip-priv-imports 44 | ``` 45 | 46 | This doc also includes the documentation of libraries term_mux_rs depends on so it can be really usefull when working on the project. 47 | 48 | As for the documentation of rust itself, if you're using `rustup` you can use `rustup doc`. 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Terminal multiplexer 2 | 3 | extern crate libc; 4 | extern crate termion; 5 | 6 | pub use util::get_shell; 7 | 8 | pub mod pty { 9 | //! pty low level handling 10 | 11 | use std::fs::File; 12 | use std::os::unix::io::{FromRawFd, RawFd}; 13 | use std::ptr; 14 | use std::io::{self, Write, Read}; 15 | use std::process::{Command, Stdio}; 16 | use std::os::unix::process::CommandExt; 17 | use std::ops; 18 | use libc; 19 | use ::tui::Size; 20 | use ::util::FromLibcResult; 21 | 22 | /// Master side of a pty master / slave pair. 23 | /// 24 | /// Allows reading the slave output, writing to the slave input and controlling the slave. 25 | pub struct Pty { 26 | /// File descriptor of the master side of the pty 27 | fd: RawFd, 28 | /// File built from fd to access Read and Write traits 29 | file: File, 30 | } 31 | 32 | /// Errors that might happen durring operations on pty. 33 | #[derive(Debug)] 34 | pub enum PtyError { 35 | /// Failed to open pty 36 | OpenPty, 37 | /// Failed spawn the shell 38 | SpawnShell, 39 | /// Failed to resize the pty 40 | Resize, 41 | } 42 | 43 | impl Pty { 44 | /// Spawns a child process running the given shell executable with the 45 | /// given size in a newly created pty. 46 | /// Returns a Pty representing the master side controlling the pty. 47 | pub fn spawn(shell: &str, size: &Size) -> Result { 48 | let (master, slave) = openpty(&size)?; 49 | 50 | Command::new(&shell) 51 | .stdin(unsafe { Stdio::from_raw_fd(slave) }) 52 | .stdout(unsafe { Stdio::from_raw_fd(slave) }) 53 | .stderr(unsafe { Stdio::from_raw_fd(slave) }) 54 | .before_exec(before_exec) 55 | .spawn() 56 | .map_err(|_| PtyError::SpawnShell) 57 | .and_then(|_| { 58 | let pty = Pty { 59 | fd: master, 60 | file: unsafe { File::from_raw_fd(master) }, 61 | }; 62 | 63 | pty.resize(&size)?; 64 | 65 | Ok(pty) 66 | }) 67 | } 68 | 69 | /// Resizes the child pty. 70 | pub fn resize(&self, size: &Size) -> Result<(), PtyError> { 71 | unsafe { 72 | libc::ioctl(self.fd, libc::TIOCSWINSZ, &size.to_c_winsize()) 73 | .to_result() 74 | .map(|_| ()) 75 | .map_err(|_| PtyError::Resize) 76 | } 77 | } 78 | } 79 | 80 | /// Creates a pty with the given size and returns the (master, slave) 81 | /// pair of file descriptors attached to it. 82 | fn openpty(size: &Size) -> Result<(RawFd, RawFd), PtyError> { 83 | let mut master = 0; 84 | let mut slave = 0; 85 | 86 | unsafe { 87 | // Create the pty master / slave pair 88 | libc::openpty(&mut master, 89 | &mut slave, 90 | ptr::null_mut(), 91 | ptr::null(), 92 | &size.to_c_winsize()) 93 | .to_result() 94 | .map_err(|_| PtyError::OpenPty)?; 95 | 96 | // Configure master to be non blocking 97 | let current_config = libc::fcntl(master, libc::F_GETFL, 0) 98 | .to_result() 99 | .map_err(|_| PtyError::OpenPty)?; 100 | 101 | libc::fcntl(master, 102 | libc::F_SETFL, 103 | current_config) 104 | .to_result() 105 | .map_err(|_| PtyError::OpenPty)?; 106 | } 107 | 108 | Ok((master, slave)) 109 | } 110 | 111 | /// Run between the fork and exec calls. So it runs in the cild process 112 | /// before the process is replaced by the program we want to run. 113 | fn before_exec() -> io::Result<()> { 114 | unsafe { 115 | // Create a new process group, this process being the master 116 | libc::setsid() 117 | .to_result() 118 | .map_err(|_| io::Error::new(io::ErrorKind::Other, ""))?; 119 | 120 | // Set this process as the controling terminal 121 | libc::ioctl(0, libc::TIOCSCTTY, 1) 122 | .to_result() 123 | .map_err(|_| io::Error::new(io::ErrorKind::Other, ""))?; 124 | } 125 | 126 | Ok(()) 127 | } 128 | 129 | impl Size { 130 | fn to_c_winsize(&self) -> libc::winsize { 131 | libc::winsize { 132 | ws_row: self.height, 133 | ws_col: self.width, 134 | 135 | // Unused fields in libc::winsize 136 | ws_xpixel: 0, 137 | ws_ypixel: 0, 138 | } 139 | } 140 | } 141 | 142 | impl Read for Pty { 143 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 144 | self.file.read(buf) 145 | } 146 | } 147 | 148 | impl Write for Pty { 149 | fn write(&mut self, buf: &[u8]) -> io::Result { 150 | self.file.write(buf) 151 | } 152 | 153 | fn flush(&mut self) -> io::Result<()> { 154 | self.file.flush() 155 | } 156 | } 157 | 158 | impl ops::Deref for Pty { 159 | type Target = File; 160 | 161 | fn deref(&self) -> &File { 162 | &self.file 163 | } 164 | } 165 | 166 | impl ops::DerefMut for Pty { 167 | fn deref_mut(&mut self) -> &mut File { 168 | &mut self.file 169 | } 170 | } 171 | 172 | #[cfg(test)] 173 | mod tests { 174 | use super::*; 175 | use std::io::{Write, Read}; 176 | 177 | #[test] 178 | fn can_open_a_shell_with_its_own_pty_and_can_read_and_write_to_its_master_side() { 179 | // Opening shell and its pty 180 | let mut pty = Pty::spawn("/bin/sh", &Size { width: 100, height: 100 }).unwrap(); 181 | 182 | let mut packet = [0; 4096]; 183 | 184 | // Reading 185 | let count = pty.read(&mut packet).unwrap(); 186 | let output = String::from_utf8_lossy(&packet[..count]).to_string(); 187 | assert!(output.ends_with("$ ")); 188 | 189 | // Writing and reading effect 190 | pty.write_all("exit\n".as_bytes()).unwrap(); 191 | pty.flush().unwrap(); 192 | 193 | let count = pty.read(&mut packet).unwrap(); 194 | let output = String::from_utf8_lossy(&packet[..count]).to_string(); 195 | assert!(output.starts_with("exit")); 196 | } 197 | 198 | #[test] 199 | fn to_c_winsize_maps_width_to_col_height_to_row_and_sets_the_rest_to_0() { 200 | let expected = libc::winsize { 201 | ws_row: 42, 202 | ws_col: 314, 203 | ws_xpixel: 0, 204 | ws_ypixel: 0, 205 | }; 206 | 207 | let actual = Size { width: 314, height: 42 }.to_c_winsize(); 208 | 209 | assert_eq!(expected.ws_row, actual.ws_row); 210 | assert_eq!(expected.ws_col, actual.ws_col); 211 | assert_eq!(expected.ws_xpixel, actual.ws_xpixel); 212 | assert_eq!(expected.ws_ypixel, actual.ws_ypixel); 213 | } 214 | } 215 | } 216 | 217 | pub mod tui { 218 | //! Terminal UI library 219 | 220 | use termion; 221 | 222 | /// A rectangular size in number of columns and rows 223 | pub struct Size { 224 | /// Number of columns 225 | pub width: u16, 226 | /// Number of rows 227 | pub height: u16, 228 | } 229 | 230 | /// Returns the terminal current size 231 | pub fn get_terminal_size() -> Result { 232 | let (width, height) = termion::terminal_size().map_err(|_| ())?; 233 | Ok( Size { width, height } ) 234 | } 235 | } 236 | 237 | pub mod util { 238 | //! Utilities 239 | 240 | use std::env; 241 | use std::ptr; 242 | use std::ffi::CStr; 243 | use libc; 244 | 245 | /// The informations in /etc/passwd corresponding to the current user. 246 | struct Passwd { 247 | pub shell: String 248 | } 249 | 250 | /// Return the informations in /etc/passwd corresponding to the current user. 251 | fn get_passwd() -> Result { 252 | unsafe { 253 | let passwd = libc::getpwuid(libc::getuid()).to_result()?; 254 | 255 | let shell = CStr::from_ptr(passwd.pw_shell) 256 | .to_str() 257 | .map_err(|_| ())? 258 | .to_string(); 259 | 260 | Ok(Passwd { shell }) 261 | } 262 | } 263 | 264 | /// Returns the path to the shell executable. 265 | /// 266 | /// Tries in order: 267 | /// 268 | /// * to read the SHELL env var 269 | /// * to read the user's passwd 270 | /// * defaults to /bin/sh 271 | /// 272 | /// # Example 273 | /// 274 | /// ``` 275 | /// # use term_mux::get_shell; 276 | /// let shell = get_shell(); 277 | /// assert!(shell.contains("/bin/")); 278 | /// assert!(shell.contains("sh")); 279 | /// ``` 280 | pub fn get_shell() -> String { 281 | env::var("SHELL") 282 | .or_else(|_| get_passwd().map(|passwd| passwd.shell)) 283 | .unwrap_or_else(|_| "/bin/sh".to_string()) 284 | } 285 | 286 | /// Converts a value returned by a libc function to a rust result. 287 | pub trait FromLibcResult: Sized { 288 | type Target; 289 | 290 | /// The intented use is for the user to call map_err() after this function. 291 | /// 292 | /// # Examples 293 | /// 294 | /// ``` 295 | /// # use term_mux::util::FromLibcResult; 296 | /// let result = -1; 297 | /// assert_eq!(Err(()), result.to_result()); 298 | /// 299 | /// let result = 42; 300 | /// assert_eq!(Ok(42), result.to_result()); 301 | /// ``` 302 | fn to_result(self) -> Result; 303 | } 304 | 305 | impl FromLibcResult for libc::c_int { 306 | type Target = libc::c_int; 307 | 308 | fn to_result(self) -> Result { 309 | match self { 310 | -1 => Err(()), 311 | res => Ok(res), 312 | } 313 | } 314 | } 315 | 316 | impl FromLibcResult for *mut libc::passwd { 317 | type Target = libc::passwd; 318 | 319 | fn to_result(self) -> Result { 320 | if self == ptr::null_mut() { return Err(()) } 321 | else { unsafe { Ok(*self) } } 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate term_mux; 2 | extern crate termion; 3 | extern crate chan_signal; 4 | 5 | use std::io::{Read, Write, Result}; 6 | use std::fs::File; 7 | use std::thread; 8 | use std::time::Duration; 9 | use termion::get_tty; 10 | use termion::raw::IntoRawMode; 11 | use chan_signal::{notify, Signal}; 12 | use term_mux::pty::Pty; 13 | use term_mux::tui::{get_terminal_size, Size}; 14 | use term_mux::get_shell; 15 | 16 | fn main () { 17 | let signal = notify(&[Signal::WINCH]); 18 | 19 | let mut tty_output = get_tty().unwrap().into_raw_mode().unwrap(); 20 | let mut tty_input = tty_output.try_clone().unwrap(); 21 | 22 | let pty_resize = Pty::spawn(&get_shell(), &get_terminal_size().unwrap()).unwrap(); 23 | let mut pty_output = pty_resize.try_clone().unwrap(); 24 | let mut pty_input = pty_output.try_clone().unwrap(); 25 | 26 | let handle = thread::spawn(move || { 27 | loop { 28 | match pipe(&mut pty_input, &mut tty_output) { 29 | Err(_) => return, 30 | _ => (), 31 | } 32 | } 33 | }); 34 | 35 | thread::spawn(move || { 36 | loop { 37 | match pipe(&mut tty_input, &mut pty_output) { 38 | Err(_) => return, 39 | _ => (), 40 | } 41 | } 42 | }); 43 | 44 | thread::spawn(move || { 45 | loop { 46 | signal.recv().unwrap(); 47 | pty_resize.resize(&get_terminal_size().unwrap()); 48 | } 49 | }); 50 | 51 | handle.join(); 52 | } 53 | 54 | /// Sends the content of input into output 55 | fn pipe(input: &mut File, output: &mut File) -> Result<()> { 56 | let mut packet = [0; 4096]; 57 | 58 | let count = input.read(&mut packet)?; 59 | 60 | let read = &packet[..count]; 61 | output.write_all(&read)?; 62 | output.flush()?; 63 | 64 | Ok(()) 65 | } 66 | --------------------------------------------------------------------------------