major usability improvements
Terminal now modal, supports line buffering, stdin, because it uses pty (and threading). Also supports paste, again (?). Audio player queueing no longer blocks, supports appending. Now can change window size. Halfscreen window shortcut improved. Eliminate dirs dep, deconstruct audiotags dep, better feature flags. Remove .alpha files because they are built. Use /home/jondough for default dirs if possible. Themes config. Fix for rotated touchscreen. Write more docs
This commit is contained in:
@@ -1,36 +1,86 @@
|
||||
use std::vec::Vec;
|
||||
use std::vec;
|
||||
use std::process::{ Command, Child, Stdio };
|
||||
use std::io::Read;
|
||||
use std::sync::mpsc::{ channel, Receiver, Sender };
|
||||
use std::thread;
|
||||
use std::process::{ Child, Stdio };
|
||||
use std::io::{ BufReader, BufRead, Write };
|
||||
use std::time::Duration;
|
||||
use std::path::PathBuf;
|
||||
use std::fmt;
|
||||
|
||||
use pty_process::blocking;
|
||||
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::messages::{ WindowMessage, WindowMessageResponse };
|
||||
use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::utils::{ concat_paths, Substring };
|
||||
use ming_wm::dirs::home;
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
//todo: support copy and paste
|
||||
|
||||
const MONO_WIDTH: u8 = 10;
|
||||
const LINE_HEIGHT: usize = 15;
|
||||
const PADDING: usize = 4;
|
||||
|
||||
//at least the ones that starts with ESC[
|
||||
fn strip_ansi_escape_codes(line: String) -> String {
|
||||
let mut new_line = String::new();
|
||||
let mut in_ansi = false;
|
||||
let mut lc = line.chars().peekable();
|
||||
loop {
|
||||
let c = lc.next();
|
||||
if c.is_none() {
|
||||
break;
|
||||
}
|
||||
let c = c.unwrap();
|
||||
if c == '\x1B' && lc.peek() == Some(&'[') {
|
||||
in_ansi = true;
|
||||
} else if in_ansi {
|
||||
if c.is_alphabetic() {
|
||||
in_ansi = false;
|
||||
}
|
||||
} else {
|
||||
new_line += &c.to_string()
|
||||
}
|
||||
}
|
||||
new_line
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
enum State {
|
||||
enum Mode {
|
||||
#[default]
|
||||
Input, //typing in to run command
|
||||
Running, //running command
|
||||
Running, //running command, key presses trigger writing output
|
||||
Stdin, //key presses writing to stdin of a running command
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
let write_str = match self {
|
||||
Mode::Input=> "INPUT",
|
||||
Mode::Running => "RUNNING ('i' to stdin, else output)",
|
||||
Mode::Stdin => "STDIN ('esc' to return, 'enter' to send)",
|
||||
};
|
||||
fmt.write_str(write_str)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Terminal {
|
||||
dimensions: Dimensions,
|
||||
state: State,
|
||||
mode: Mode,
|
||||
lines: Vec<String>,
|
||||
actual_lines: Vec<String>, //wrapping
|
||||
actual_line_num: usize, //what line # is at the top, for scrolling
|
||||
current_input: String,
|
||||
current_stdin_input: String,
|
||||
current_path: String,
|
||||
running_process: Option<Child>,
|
||||
pty_outerr_rx: Option<Receiver<String>>,
|
||||
pty_in_tx: Option<Sender<String>>,
|
||||
last_command: Option<String>,
|
||||
}
|
||||
|
||||
@@ -41,7 +91,7 @@ impl WindowLike for Terminal {
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
self.current_path = "/".to_string();
|
||||
self.current_path = home().unwrap_or(PathBuf::from("/")).to_string_lossy().to_string();
|
||||
self.lines = vec!["Mingde Terminal".to_string(), "".to_string()];
|
||||
self.calc_actual_lines();
|
||||
WindowMessageResponse::JustRedraw
|
||||
@@ -51,60 +101,94 @@ impl WindowLike for Terminal {
|
||||
WindowMessageResponse::JustRedraw
|
||||
},
|
||||
WindowMessage::KeyPress(key_press) => {
|
||||
if self.state == State::Input {
|
||||
if key_press.key == '𐘁' { //backspace
|
||||
if self.current_input.len() > 0 {
|
||||
self.current_input = self.current_input.remove_last();
|
||||
match self.mode {
|
||||
Mode::Input => {
|
||||
if key_press.key == '𐘁' { //backspace
|
||||
if self.current_input.len() > 0 {
|
||||
self.current_input = self.current_input.remove_last();
|
||||
} else {
|
||||
return WindowMessageResponse::DoNothing;
|
||||
}
|
||||
} else if key_press.key == '𐘂' { //the enter key
|
||||
self.lines.push("$ ".to_string() + &self.current_input);
|
||||
self.last_command = Some(self.current_input.clone());
|
||||
self.mode = self.process_command();
|
||||
self.current_input = String::new();
|
||||
} else {
|
||||
return WindowMessageResponse::DoNothing;
|
||||
self.current_input += &key_press.key.to_string();
|
||||
}
|
||||
} else if key_press.key == '𐘂' { //the enter key
|
||||
self.lines.push("$ ".to_string() + &self.current_input);
|
||||
self.last_command = Some(self.current_input.clone());
|
||||
self.state = self.process_command();
|
||||
self.current_input = String::new();
|
||||
} else {
|
||||
self.current_input += &key_press.key.to_string();
|
||||
}
|
||||
self.calc_actual_lines();
|
||||
self.actual_line_num = self.actual_lines.len().checked_sub(self.get_max_lines()).unwrap_or(0);
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else {
|
||||
//update
|
||||
let running_process = self.running_process.as_mut().unwrap();
|
||||
if let Some(status) = running_process.try_wait().unwrap() {
|
||||
//process exited
|
||||
let mut output = String::new();
|
||||
if status.success() {
|
||||
let _ = running_process.stdout.as_mut().unwrap().read_to_string(&mut output);
|
||||
self.calc_actual_lines();
|
||||
self.actual_line_num = self.actual_lines.len().checked_sub(self.get_max_lines()).unwrap_or(0);
|
||||
WindowMessageResponse::JustRedraw
|
||||
},
|
||||
Mode::Running => {
|
||||
//update
|
||||
let mut changed = false;
|
||||
loop {
|
||||
if let Ok(line) = self.pty_outerr_rx.as_mut().unwrap().recv_timeout(Duration::from_millis(5)) {
|
||||
self.lines.push(strip_ansi_escape_codes(line));
|
||||
changed = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let running_process = self.running_process.as_mut().unwrap();
|
||||
if let Some(_status) = running_process.try_wait().unwrap() {
|
||||
//process exited
|
||||
self.mode = Mode::Input;
|
||||
changed = true;
|
||||
} else {
|
||||
let _ = running_process.stderr.as_mut().unwrap().read_to_string(&mut output);
|
||||
if key_press.key == 'i' {
|
||||
self.mode = Mode::Stdin;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for line in output.split("\n") {
|
||||
self.lines.push(line.to_string());
|
||||
if changed {
|
||||
self.calc_actual_lines();
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
},
|
||||
Mode::Stdin => {
|
||||
if key_press.key == '𐘃' {
|
||||
//esc
|
||||
self.mode = Mode::Running;
|
||||
} else if key_press.key == '𐘂' {
|
||||
//enter
|
||||
let _ = self.pty_in_tx.as_mut().unwrap().send(self.current_stdin_input.clone());
|
||||
self.mode = Mode::Running;
|
||||
self.current_stdin_input = String::new();
|
||||
} else if key_press.key == '𐘁' {
|
||||
//backspace
|
||||
if self.current_stdin_input.len() > 0 {
|
||||
self.current_stdin_input = self.current_stdin_input.remove_last();
|
||||
} else {
|
||||
return WindowMessageResponse::DoNothing;
|
||||
}
|
||||
} else {
|
||||
self.current_stdin_input += &key_press.key.to_string();
|
||||
}
|
||||
self.state = State::Input;
|
||||
self.calc_actual_lines();
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else {
|
||||
//still running
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
WindowMessage::CtrlKeyPress(key_press) => {
|
||||
if self.state == State::Running && key_press.key == 'c' {
|
||||
if self.mode == Mode::Running && key_press.key == 'c' {
|
||||
//kills and running_process is now None
|
||||
let _ = self.running_process.take().unwrap().kill();
|
||||
self.state = State::Input;
|
||||
self.mode = Mode::Input;
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else if self.state == State::Input && (key_press.key == 'p' || key_press.key == 'n') {
|
||||
} else if self.mode == Mode::Input && (key_press.key == 'p' || key_press.key == 'n') {
|
||||
//only the last command is saved unlike other terminals. good enough for me
|
||||
if key_press.key == 'p' && self.last_command.is_some() {
|
||||
self.current_input = self.last_command.clone().unwrap();
|
||||
self.calc_actual_lines();
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else if key_press.key == 'n' {
|
||||
self.current_input = String::new();
|
||||
self.calc_actual_lines();
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
@@ -113,6 +197,24 @@ impl WindowLike for Terminal {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
},
|
||||
WindowMessage::Shortcut(shortcut) => {
|
||||
match shortcut {
|
||||
ShortcutType::ClipboardPaste(copy_string) => {
|
||||
if self.mode == Mode::Input || self.mode == Mode::Stdin {
|
||||
if self.mode == Mode::Input {
|
||||
self.current_input += copy_string;
|
||||
} else {
|
||||
self.current_stdin_input += copy_string;
|
||||
}
|
||||
self.calc_actual_lines();
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
}
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
}
|
||||
}
|
||||
@@ -122,7 +224,7 @@ impl WindowLike for Terminal {
|
||||
DrawInstructions::Rect([0, 0], self.dimensions, theme_info.alt_background),
|
||||
];
|
||||
//add the visible lines of text
|
||||
let end_line = self.actual_line_num + self.get_max_lines();
|
||||
let end_line = self.actual_line_num + self.get_max_lines() - 1;
|
||||
let mut text_y = PADDING;
|
||||
for line_num in self.actual_line_num..end_line {
|
||||
if line_num == self.actual_lines.len() {
|
||||
@@ -132,6 +234,7 @@ impl WindowLike for Terminal {
|
||||
instructions.push(DrawInstructions::Text([PADDING, text_y], vec!["nimbus-romono".to_string()], line, theme_info.alt_text, theme_info.alt_background, Some(0), Some(MONO_WIDTH)));
|
||||
text_y += LINE_HEIGHT;
|
||||
}
|
||||
instructions.push(DrawInstructions::Text([PADDING, self.dimensions[1] - LINE_HEIGHT], vec!["nimbus-romono".to_string()], self.mode.to_string(), theme_info.alt_text, theme_info.alt_background, Some(0), Some(MONO_WIDTH)));
|
||||
instructions
|
||||
}
|
||||
|
||||
@@ -161,37 +264,64 @@ impl Terminal {
|
||||
(self.dimensions[1] - PADDING * 2) / LINE_HEIGHT
|
||||
}
|
||||
|
||||
fn process_command(&mut self) -> State {
|
||||
fn process_command(&mut self) -> Mode {
|
||||
if self.current_input.starts_with("clear ") || self.current_input == "clear" {
|
||||
self.lines = Vec::new();
|
||||
State::Input
|
||||
Mode::Input
|
||||
} else if self.current_input.starts_with("cd ") {
|
||||
let mut cd_split = self.current_input.split(" ");
|
||||
cd_split.next().unwrap();
|
||||
let arg = cd_split.next().unwrap();
|
||||
if let Ok(new_path) = concat_paths(&self.current_path, arg) {
|
||||
if new_path.exists() {
|
||||
if new_path.is_dir() {
|
||||
self.current_path = new_path.to_str().unwrap().to_string();
|
||||
}
|
||||
}
|
||||
State::Input
|
||||
Mode::Input
|
||||
} else {
|
||||
self.running_process = Some(Command::new("sh").arg("-c").arg(&self.current_input).current_dir(&self.current_path).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap());
|
||||
State::Running
|
||||
let (pty, pts) = blocking::open().unwrap();
|
||||
self.running_process = Some(blocking::Command::new("sh").arg("-c").arg(&self.current_input).current_dir(&self.current_path).stdin(Stdio::piped()).spawn(pts).unwrap());
|
||||
let (tx1, rx1) = channel();
|
||||
thread::spawn(move || {
|
||||
let reader = BufReader::new(pty);
|
||||
for line in reader.lines() {
|
||||
tx1.send(line.unwrap().to_string()).unwrap();
|
||||
}
|
||||
});
|
||||
let mut stdin = self.running_process.as_mut().unwrap().stdin.take().unwrap();
|
||||
let (tx2, rx2) = channel();
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
if let Ok(write_line) = rx2.recv() {
|
||||
let write_line: String = write_line + "\n";
|
||||
stdin.write(write_line.as_bytes()).unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
self.pty_outerr_rx = Some(rx1);
|
||||
self.pty_in_tx = Some(tx2);
|
||||
Mode::Running
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_actual_lines(&mut self) {
|
||||
self.actual_lines = Vec::new();
|
||||
let max_chars_per_line = (self.dimensions[0] - PADDING * 2) / MONO_WIDTH as usize;
|
||||
let end = if self.state == State::Input {
|
||||
let end = if self.mode != Mode::Running {
|
||||
self.lines.len()
|
||||
} else {
|
||||
self.lines.len() - 1
|
||||
};
|
||||
for line_num in 0..=end {
|
||||
let mut working_line = if line_num == self.lines.len() {
|
||||
"$ ".to_string() + &self.current_input + "█"
|
||||
if self.mode == Mode::Input {
|
||||
"$ ".to_string() + &self.current_input + "█"
|
||||
} else {
|
||||
//Mode::Stdin
|
||||
self.current_stdin_input.clone() + "█"
|
||||
}
|
||||
} else {
|
||||
self.lines[line_num].clone()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user