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:
@@ -3,10 +3,15 @@ use std::vec;
|
||||
use std::io::BufReader;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::{ read_to_string, File };
|
||||
use std::time::{ Duration, SystemTime, UNIX_EPOCH };
|
||||
use std::thread;
|
||||
use std::sync::{ Arc, Mutex };
|
||||
|
||||
use rodio::{ Decoder, OutputStream, Sink, Source };
|
||||
use rand::prelude::*;
|
||||
use audiotags::Tag;
|
||||
use rand::{ SeedableRng, prelude::SliceRandom, rngs::SmallRng };
|
||||
use id3::TagLike;
|
||||
use mp4ameta;
|
||||
use metaflac;
|
||||
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::messages::{ WindowMessage, WindowMessageResponse };
|
||||
@@ -14,24 +19,67 @@ use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::utils::{ concat_paths, format_seconds, Substring };
|
||||
use ming_wm::fs::get_all_files;
|
||||
use ming_wm::dirs::home;
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
fn get_artist(path: &PathBuf) -> Option<String> {
|
||||
let ext = path.extension().unwrap();
|
||||
if ext == "mp4" {
|
||||
let tag = mp4ameta::Tag::read_from_path(path).unwrap();
|
||||
tag.artist().map(|s| s.to_string())
|
||||
} else if ext == "flac" {
|
||||
let tag = metaflac::Tag::read_from_path(path).unwrap();
|
||||
if let Some(mut artists) = tag.get_vorbis("Artist") {
|
||||
Some(artists.next().unwrap().to_string()) //get the first one
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if ext == "mp3" {
|
||||
let tag = id3::Tag::read_from_path(path).unwrap();
|
||||
tag.artist().map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
const MONO_WIDTH: u8 = 10;
|
||||
const LINE_HEIGHT: usize = 18;
|
||||
|
||||
#[derive(Default)]
|
||||
struct AudioPlayer {
|
||||
type QueueItem = (PathBuf, u64, Option<String>);
|
||||
|
||||
struct InternalPlayer {
|
||||
pub queue: Vec<QueueItem>,
|
||||
pub sink: Sink,
|
||||
}
|
||||
|
||||
impl InternalPlayer {
|
||||
fn add(internal: Arc<Mutex<InternalPlayer>>, queue: Vec<PathBuf>) {
|
||||
thread::spawn(move || {
|
||||
for item in queue {
|
||||
let file = BufReader::new(File::open(&item).unwrap());
|
||||
//slightly faster for mp3s? since it doesn't need to check if it is .wav, etc. but maybe not
|
||||
let decoded = if item.ends_with(".mp3") { Decoder::new_mp3(file) } else { Decoder::new(file) }.unwrap();
|
||||
let mut internal_locked = internal.lock().unwrap();
|
||||
(*internal_locked).queue.push((item.clone(), decoded.total_duration().unwrap().as_secs(), get_artist(&item)));
|
||||
(*internal_locked).sink.append(decoded);
|
||||
(*internal_locked).sink.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioPlayer {
|
||||
dimensions: Dimensions,
|
||||
base_directory: String,
|
||||
queue: Vec<(PathBuf, u64, Option<String>)>,
|
||||
stream: Option<Box<OutputStream>>,
|
||||
sink: Option<Sink>,
|
||||
_stream: Box<OutputStream>,
|
||||
internal: Arc<Mutex<InternalPlayer>>,
|
||||
command: String,
|
||||
response: String,
|
||||
}
|
||||
|
||||
impl WindowLike for AudioPlayer {
|
||||
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
||||
//
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
@@ -62,18 +110,19 @@ impl WindowLike for AudioPlayer {
|
||||
|
||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||
let mut instructions = vec![DrawInstructions::Text([2, self.dimensions[1] - LINE_HEIGHT], vec!["nimbus-roman".to_string()], if self.command.len() > 0 { self.command.clone() } else { self.response.clone() }, theme_info.text, theme_info.background, None, None)];
|
||||
if let Some(sink) = &self.sink {
|
||||
if sink.len() > 0 {
|
||||
let current = &self.queue[self.queue.len() - sink.len()];
|
||||
let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned();
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - current_name.len() * MONO_WIDTH as usize / 2, 2], vec!["nimbus-romono".to_string(), "shippori-mincho".to_string()], current_name.clone(), theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
if let Some(artist) = ¤t.2 {
|
||||
let artist_string = "by ".to_string() + &artist;
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - artist_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT + 2], vec!["nimbus-romono".to_string()], artist_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
}
|
||||
let time_string = format!("{}/{}", format_seconds(sink.get_pos().as_secs()), format_seconds(current.1));
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - time_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT * 2 + 2], vec!["nimbus-romono".to_string()], time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
let internal_locked = self.internal.lock().unwrap();
|
||||
let sink_len = internal_locked.sink.len();
|
||||
if sink_len > 0 {
|
||||
let queue = &internal_locked.queue;
|
||||
let current = &queue[queue.len() - sink_len];
|
||||
let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned();
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - current_name.len() * MONO_WIDTH as usize / 2, 2], vec!["nimbus-romono".to_string(), "shippori-mincho".to_string()], current_name.clone(), theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
if let Some(artist) = ¤t.2 {
|
||||
let artist_string = "by ".to_string() + &artist;
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - artist_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT + 2], vec!["nimbus-romono".to_string()], artist_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
}
|
||||
let time_string = format!("{}/{}", format_seconds(internal_locked.sink.get_pos().as_secs()), format_seconds(current.1));
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - time_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT * 2 + 2], vec!["nimbus-romono".to_string()], time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
} else {
|
||||
instructions.push(DrawInstructions::Text([2, 2], vec!["nimbus-roman".to_string()], "type to write commands, enter to execute.".to_string(), theme_info.text, theme_info.background, None, None));
|
||||
instructions.push(DrawInstructions::Text([2, 2 + LINE_HEIGHT], vec!["nimbus-roman".to_string()], "See help in start menu for commands.".to_string(), theme_info.text, theme_info.background, None, None));
|
||||
@@ -103,9 +152,18 @@ impl WindowLike for AudioPlayer {
|
||||
|
||||
impl AudioPlayer {
|
||||
pub fn new() -> Self {
|
||||
let mut ap: Self = Default::default();
|
||||
ap.base_directory = "/".to_string();
|
||||
ap
|
||||
let (stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
Self {
|
||||
dimensions: Default::default(),
|
||||
base_directory: home().unwrap_or(PathBuf::from("/")).to_string_lossy().to_string(),
|
||||
_stream: Box::new(stream),
|
||||
internal: Arc::new(Mutex::new(InternalPlayer {
|
||||
queue: Vec::new(),
|
||||
sink: Sink::try_new(&stream_handle).unwrap(),
|
||||
})),
|
||||
command: Default::default(),
|
||||
response: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
//t: toggle pause/play
|
||||
@@ -114,41 +172,35 @@ impl AudioPlayer {
|
||||
//k: volume up
|
||||
//b <dir>: set base directory
|
||||
//p <dir>/<playlist file>: play directory or playlist in random order
|
||||
//todo: h for help?
|
||||
//a <dir>/<playlist file>: same as p but appends to queue instead of clearing
|
||||
//just hit enter or any key to refresh
|
||||
fn process_command(&mut self) -> String {
|
||||
if self.command.len() == 1 {
|
||||
if let Some(sink) = &mut self.sink {
|
||||
if self.command == "t" {
|
||||
if sink.is_paused() {
|
||||
sink.play();
|
||||
return "Resumed".to_string();
|
||||
} else {
|
||||
sink.pause();
|
||||
return "Paused".to_string();
|
||||
}
|
||||
} else if self.command == "h" {
|
||||
//
|
||||
} else if self.command == "l" {
|
||||
sink.skip_one();
|
||||
return "Skipped".to_string();
|
||||
} else if self.command == "j" {
|
||||
sink.set_volume(sink.volume() - 0.1);
|
||||
return "Volume decreased".to_string();
|
||||
} else if self.command == "k" {
|
||||
sink.set_volume(sink.volume() + 0.1);
|
||||
return "Volume increased".to_string();
|
||||
let sink = &(*self.internal.lock().unwrap()).sink;
|
||||
if self.command == "t" {
|
||||
if sink.is_paused() {
|
||||
sink.play();
|
||||
return "Resumed".to_string();
|
||||
} else {
|
||||
sink.pause();
|
||||
return "Paused".to_string();
|
||||
}
|
||||
} else if self.command == "l" {
|
||||
sink.skip_one();
|
||||
return "Skipped".to_string();
|
||||
} else if self.command == "j" {
|
||||
sink.set_volume(sink.volume() - 0.1);
|
||||
return "Volume decreased".to_string();
|
||||
} else if self.command == "k" {
|
||||
sink.set_volume(sink.volume() + 0.1);
|
||||
return "Volume increased".to_string();
|
||||
}
|
||||
} else {
|
||||
let parts: Vec<&str> = self.command.split(" ").collect();
|
||||
if self.command.starts_with("p ") {
|
||||
if self.command.starts_with("p ") || self.command.starts_with("a ") {
|
||||
if parts.len() == 2 {
|
||||
if let Ok(new_path) = concat_paths(&self.base_directory, parts[1]) {
|
||||
if new_path.exists() {
|
||||
if let Some(sink) = &mut self.sink {
|
||||
sink.clear();
|
||||
}
|
||||
let mut queue = if parts[1].ends_with(".playlist") {
|
||||
let mut queue = Vec::new();
|
||||
let contents = read_to_string(new_path).unwrap();
|
||||
@@ -157,34 +209,36 @@ impl AudioPlayer {
|
||||
if line.ends_with("/*") {
|
||||
queue.extend(get_all_files(concat_paths(&self.base_directory, &line[..line.len() - 2]).unwrap()));
|
||||
} else if line.len() > 0 {
|
||||
queue.push(concat_paths(&self.base_directory, &(line.to_owned() + if line.ends_with(".mp3") { "" } else { ".mp3" })).unwrap());
|
||||
//if no file ext, assumes mp3
|
||||
queue.push(concat_paths(&self.base_directory, &(line.to_owned() + if line.contains(".") { "" } else { ".mp3" })).unwrap());
|
||||
}
|
||||
}
|
||||
queue
|
||||
} else {
|
||||
get_all_files(PathBuf::from(new_path))
|
||||
};
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = SmallRng::seed_from_u64(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs());
|
||||
queue.shuffle(&mut rng);
|
||||
let (stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||
self.queue = Vec::new();
|
||||
for item in &queue {
|
||||
let file = BufReader::new(File::open(item).unwrap());
|
||||
//slightly faster for mp3s? since it doesn't need to check if it is .wav, etc. but maybe not
|
||||
let decoded = if item.ends_with(".mp3") { Decoder::new_mp3(file) } else { Decoder::new(file) }.unwrap();
|
||||
self.queue.push((item.clone(), decoded.total_duration().unwrap().as_secs(), Tag::new().read_from_path(item.clone()).unwrap().artist().map(|s| s.to_string())));
|
||||
sink.append(decoded);
|
||||
if self.command.starts_with("p ") {
|
||||
let mut locked_internal = self.internal.lock().unwrap();
|
||||
(*locked_internal).sink.clear();
|
||||
(*locked_internal).queue = Vec::new();
|
||||
}
|
||||
self.stream = Some(Box::new(stream));
|
||||
self.sink = Some(sink);
|
||||
return "Playing".to_string();
|
||||
InternalPlayer::add(Arc::clone(&self.internal), queue);
|
||||
//to hopefully allow the first file to be loaded so info displays
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
return if self.command.starts_with("p ") {
|
||||
"Playing".to_string()
|
||||
} else {
|
||||
"Appended".to_string()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.command.starts_with("b ") {
|
||||
if parts.len() == 2 {
|
||||
if let Ok(new_path) = concat_paths(&self.base_directory, parts[1]) {
|
||||
let new_path = if parts[1].starts_with("/") { Ok(PathBuf::from(parts[1])) } else { concat_paths(&self.base_directory, parts[1]) };
|
||||
if let Ok(new_path) = new_path {
|
||||
if new_path.exists() {
|
||||
self.base_directory = new_path.to_str().unwrap().to_string();
|
||||
return "Set new base directory".to_string();
|
||||
|
||||
@@ -139,6 +139,7 @@ impl WindowLike for FileExplorer {
|
||||
instructions.push(DrawInstructions::Text([5, start_y], vec!["nimbus-roman".to_string()], format!("Size: {} mb ({} b)", bytes_len / (1024_u64).pow(2), bytes_len), theme_info.text, theme_info.background, None, None));
|
||||
start_y += HEIGHT;
|
||||
//todo: other stuff
|
||||
//
|
||||
}
|
||||
instructions
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ fn init(framebuffer: Framebuffer, framebuffer_info: FramebufferInfo) {
|
||||
}
|
||||
if x.is_some() && y.is_some() {
|
||||
let (x2, y2) = if rotate {
|
||||
(y.unwrap(), dimensions[0] - x.unwrap())
|
||||
(dimensions[0] - y.unwrap(), x.unwrap())
|
||||
} else {
|
||||
(x.unwrap(), y.unwrap())
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::utils::{ calc_actual_lines, calc_new_cursor_pos, Substring };
|
||||
use ming_wm::dirs::home;
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
const MONO_WIDTH: u8 = 10;
|
||||
@@ -496,8 +497,10 @@ impl Malvim {
|
||||
let mut failed = false;
|
||||
let mut new_path = if self.files.len() > 0 && !arg.starts_with("/") {
|
||||
PathBuf::from(self.files[self.current_file_index].path.clone()).parent().unwrap().to_path_buf()
|
||||
} else {
|
||||
} else if arg.starts_with("/") {
|
||||
PathBuf::from("/")
|
||||
} else {
|
||||
home().unwrap_or(PathBuf::from("/"))
|
||||
};
|
||||
for part in arg.split("/") {
|
||||
if part == ".." {
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
40
src/dirs.rs
Normal file
40
src/dirs.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn home() -> Option<PathBuf> {
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
Some(PathBuf::from(home))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_dir() -> Option<PathBuf> {
|
||||
//$XDG_DATA_HOME or $HOME/.local/share
|
||||
if let Ok(data_home) = env::var("XDG_DATA_HOME") {
|
||||
Some(PathBuf::from(data_home))
|
||||
} else {
|
||||
if let Some(mut data_home) = home() {
|
||||
data_home.push(".local");
|
||||
data_home.push("share");
|
||||
Some(data_home)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
//$XDG_CONFIG_HOME or $HOME/.config
|
||||
if let Ok(config_home) = env::var("XDG_CONFIG_HOME") {
|
||||
Some(PathBuf::from(config_home))
|
||||
} else {
|
||||
if let Some(mut config_home) = home() {
|
||||
config_home.push(".config");
|
||||
Some(config_home)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@ use std::vec::Vec;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
use dirs::config_dir;
|
||||
|
||||
use crate::window_manager::{ DrawInstructions, WindowLike, WindowLikeType, TASKBAR_HEIGHT, INDICATOR_HEIGHT };
|
||||
use crate::messages::{ WindowMessage, WindowMessageResponse, ShortcutType };
|
||||
use crate::framebuffer::Dimensions;
|
||||
use crate::themes::ThemeInfo;
|
||||
use crate::utils::{ hex_to_u8, is_hex };
|
||||
use crate::dirs::config_dir;
|
||||
|
||||
pub struct DesktopBackground {
|
||||
dimensions: Dimensions,
|
||||
|
||||
@@ -56,7 +56,7 @@ impl WindowLike for LockScreen {
|
||||
DrawInstructions::Text([4, 4 + 16], vec!["nimbus-roman".to_string()], "\"Yellow,\" he thought, and stomped off back to his bedroom to get dressed.".to_string(), [255, 255, 255], [0, 0, 0], None, None),
|
||||
DrawInstructions::Text([4, 4 + 16 * 2], vec!["nimbus-roman".to_string()], "He stared at it.".to_string(), [255, 255, 255], [0, 0, 0], None, None),
|
||||
DrawInstructions::Text([4, 4 + 16 * 3], vec!["nimbus-roman".to_string()], "Password: ".to_string(), [255, 255, 255], [0, 0, 0], None, None),
|
||||
DrawInstructions::Text([85, 4 + 16 * 3], vec!["nimbus-roman".to_string()], "*".repeat(self.input_password.len()), [255, 255, 255], [0, 0, 0], None, None),
|
||||
DrawInstructions::Text([80, 4 + 16 * 3], vec!["nimbus-roman".to_string()], "*".repeat(self.input_password.len()), [255, 255, 255], [0, 0, 0], None, None),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::themes::ThemeInfo;
|
||||
use crate::components::Component;
|
||||
use crate::components::highlight_button::HighlightButton;
|
||||
|
||||
static CATEGORIES: [&'static str; 9] = ["About", "Utils", "Games", "Editing", "Files", "System", "Misc", "Help", "Logout"];
|
||||
static CATEGORIES: [&'static str; 9] = ["About", "Utils", "Games", "Editing", "Files", "Internet", "Misc", "Help", "Logout"];
|
||||
|
||||
#[derive(Clone)]
|
||||
enum StartMenuMessage {
|
||||
|
||||
@@ -48,13 +48,12 @@ impl WindowLike for Taskbar {
|
||||
}
|
||||
},
|
||||
WindowMessage::Info(info) => {
|
||||
match info {
|
||||
InfoType::WindowsInWorkspace(windows, focused_id) => {
|
||||
self.windows_in_workspace = windows;
|
||||
self.focused_id = focused_id;
|
||||
WindowMessageResponse::JustRedraw
|
||||
}
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
if let InfoType::WindowsInWorkspace(windows, focused_id) = info {
|
||||
self.windows_in_workspace = windows;
|
||||
self.focused_id = focused_id;
|
||||
WindowMessageResponse::JustRedraw
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod utils;
|
||||
pub mod logging;
|
||||
pub mod ipc;
|
||||
pub mod serialize;
|
||||
pub mod dirs;
|
||||
mod proxy_window_like;
|
||||
mod essential;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fs::{ OpenOptions, create_dir };
|
||||
use std::io::Write;
|
||||
|
||||
use dirs::data_dir;
|
||||
use crate::dirs::data_dir;
|
||||
|
||||
pub fn log(message: &str) {
|
||||
let data = data_dir().unwrap().into_os_string().into_string().unwrap();
|
||||
|
||||
@@ -73,6 +73,7 @@ pub enum ShortcutType {
|
||||
QuitWindow,
|
||||
MoveWindow(Direction),
|
||||
MoveWindowToEdge(Direction),
|
||||
ChangeWindowSize(Direction),
|
||||
CenterWindow,
|
||||
FullscreenWindow,
|
||||
HalfWidthWindow, //half width, full height
|
||||
|
||||
@@ -538,6 +538,12 @@ impl Serializable for WindowMessage {
|
||||
Direction::Up => "Up",
|
||||
Direction::Right => "Right",
|
||||
}),
|
||||
ShortcutType::ChangeWindowSize(d) => format!("ChangeWindowSize/{}", match d {
|
||||
Direction::Left => "Left",
|
||||
Direction::Down => "Down",
|
||||
Direction::Up => "Up",
|
||||
Direction::Right => "Right",
|
||||
}),
|
||||
ShortcutType::CenterWindow => "CenterWindow".to_string(),
|
||||
ShortcutType::FullscreenWindow => "FullscreenWindow".to_string(),
|
||||
ShortcutType::HalfWidthWindow => "HalfWidthWindow".to_string(),
|
||||
@@ -617,7 +623,7 @@ impl Serializable for WindowMessage {
|
||||
"FocusPrevWindow" => Some(ShortcutType::FocusPrevWindow),
|
||||
"FocusNextWindow" => Some(ShortcutType::FocusNextWindow),
|
||||
"QuitWindow" => Some(ShortcutType::QuitWindow),
|
||||
"MoveWindow" | "MoveWindowToEdge" => {
|
||||
"MoveWindow" | "MoveWindowToEdge" | "ChangeWindowSize" => {
|
||||
let darg = parts.next();
|
||||
if let Some(darg) = darg {
|
||||
let direction = match darg {
|
||||
@@ -630,8 +636,10 @@ impl Serializable for WindowMessage {
|
||||
if let Some(direction) = direction {
|
||||
if arg == "MoveWindow" {
|
||||
Some(ShortcutType::MoveWindow(direction))
|
||||
} else {
|
||||
} else if arg == "MoveWindowToEdge" {
|
||||
Some(ShortcutType::MoveWindowToEdge(direction))
|
||||
} else {
|
||||
Some(ShortcutType::ChangeWindowSize(direction))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -8,12 +8,12 @@ use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
use linux_framebuffer::Framebuffer;
|
||||
use dirs::config_dir;
|
||||
|
||||
use crate::framebuffer::{ FramebufferWriter, Point, Dimensions, RGBColor };
|
||||
use crate::themes::{ ThemeInfo, Themes, get_theme_info };
|
||||
use crate::utils::{ min, point_inside };
|
||||
use crate::messages::*;
|
||||
use crate::dirs::config_dir;
|
||||
use crate::proxy_window_like::ProxyWindowLike;
|
||||
use crate::essential::desktop_background::DesktopBackground;
|
||||
use crate::essential::taskbar::Taskbar;
|
||||
@@ -251,7 +251,7 @@ impl WindowManager {
|
||||
if !self.locked {
|
||||
//keyboard shortcut
|
||||
let shortcuts = HashMap::from([
|
||||
//alt+e is terminate program (ctrl+c)
|
||||
//alt+E kills ming-wm when it is unlocked, but that is handled at a higher level
|
||||
('s', ShortcutType::StartMenu),
|
||||
('[', ShortcutType::FocusPrevWindow),
|
||||
(']', ShortcutType::FocusNextWindow),
|
||||
@@ -271,7 +271,12 @@ impl WindowManager {
|
||||
('J', ShortcutType::MoveWindowToEdge(Direction::Down)),
|
||||
('K', ShortcutType::MoveWindowToEdge(Direction::Up)),
|
||||
('L', ShortcutType::MoveWindowToEdge(Direction::Right)),
|
||||
//
|
||||
//expand window size
|
||||
('n', ShortcutType::ChangeWindowSize(Direction::Right)),
|
||||
('m', ShortcutType::ChangeWindowSize(Direction::Down)),
|
||||
//shrink window size
|
||||
('N', ShortcutType::ChangeWindowSize(Direction::Left)),
|
||||
('M', ShortcutType::ChangeWindowSize(Direction::Up)),
|
||||
//no 10th workspace
|
||||
('1', ShortcutType::SwitchWorkspace(0)),
|
||||
('2', ShortcutType::SwitchWorkspace(1)),
|
||||
@@ -359,6 +364,64 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
},
|
||||
&ShortcutType::ChangeWindowSize(direction) => {
|
||||
if let Some(focused_index) = self.get_focused_index() {
|
||||
let focused_info = &self.window_infos[focused_index];
|
||||
if focused_info.window_like.subtype() == WindowLikeType::Window && focused_info.window_like.resizable() && !focused_info.fullscreen {
|
||||
//mostly arbitrary
|
||||
let min_window_size = [100, WINDOW_TOP_HEIGHT + 5];
|
||||
let mut changed = false;
|
||||
let delta = 15;
|
||||
let window = &mut self.window_infos[focused_index];
|
||||
if direction == Direction::Right {
|
||||
//expand x
|
||||
if window.dimensions[0] + delta != self.dimensions[0] {
|
||||
window.dimensions[0] += delta;
|
||||
let max_width = self.dimensions[0] - window.top_left[0];
|
||||
if window.dimensions[0] > max_width {
|
||||
window.dimensions[0] = max_width;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
} else if direction == Direction::Down {
|
||||
//expand y
|
||||
let max_height = self.dimensions[1] - window.top_left[1] - INDICATOR_HEIGHT - TASKBAR_HEIGHT;
|
||||
if window.dimensions[1] + delta != max_height {
|
||||
window.dimensions[1] += delta;
|
||||
if window.dimensions[1] > max_height {
|
||||
window.dimensions[1] = max_height;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
} else if direction == Direction::Left {
|
||||
//shrink x
|
||||
if window.dimensions[0] - delta != min_window_size[0] {
|
||||
window.dimensions[0] -= delta;
|
||||
if window.dimensions[0] < min_window_size[0] {
|
||||
window.dimensions[0] = min_window_size[0];
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
} else if direction == Direction::Up {
|
||||
//shrink y
|
||||
if window.dimensions[1] - delta != min_window_size[1] {
|
||||
window.dimensions[1] -= delta;
|
||||
if window.dimensions[1] < min_window_size[1] {
|
||||
window.dimensions[1] = min_window_size[1];
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
let new_dimensions = [window.dimensions[0], window.dimensions[1] - WINDOW_TOP_HEIGHT];
|
||||
self.window_infos[focused_index].window_like.handle_message(WindowMessage::ChangeDimensions(new_dimensions));
|
||||
press_response = WindowMessageResponse::JustRedraw;
|
||||
use_saved_buffer = true;
|
||||
redraw_ids = Some(vec![self.focused_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
&ShortcutType::SwitchWorkspace(workspace) => {
|
||||
if self.current_workspace != workspace {
|
||||
//close start menu if open
|
||||
@@ -459,8 +522,14 @@ impl WindowManager {
|
||||
let window_like = &self.window_infos[focused_index].window_like;
|
||||
if window_like.subtype() == WindowLikeType::Window && window_like.resizable() {
|
||||
self.window_infos[focused_index].fullscreen = false;
|
||||
let top_left = &mut self.window_infos[focused_index].top_left;
|
||||
if top_left[0] > self.dimensions[0] / 2 {
|
||||
top_left[0] = self.dimensions[0] / 2;
|
||||
} else {
|
||||
top_left[0] = 0;
|
||||
}
|
||||
top_left[1] = INDICATOR_HEIGHT;
|
||||
//full height, half width
|
||||
self.window_infos[focused_index].top_left = [0, INDICATOR_HEIGHT];
|
||||
let new_dimensions = [self.dimensions[0] / 2, self.dimensions[1] - INDICATOR_HEIGHT - TASKBAR_HEIGHT];
|
||||
self.window_infos[focused_index].dimensions = new_dimensions;
|
||||
self.window_infos[focused_index].window_like.handle_message(WindowMessage::ChangeDimensions([new_dimensions[0], new_dimensions[1] - WINDOW_TOP_HEIGHT]));
|
||||
|
||||
Reference in New Issue
Block a user