From decf1d3b82a282798fd7d05966d51fcbc9174276 Mon Sep 17 00:00:00 2001 From: stjet <49297268+stjet@users.noreply.github.com> Date: Mon, 21 Oct 2024 05:21:59 +0000 Subject: [PATCH] MUSIC PLAYERgit diff --cached src/window_likes/malvim.rs! and fixes --- Cargo.toml | 2 + bmps/times-new-roman/=4.bmp | Bin 0 -> 162 bytes bmps/times-new-romono/=4.bmp | Bin 0 -> 162 bytes src/fs.rs | 14 +++ src/messages.rs | 3 +- src/utils.rs | 35 ++++++ src/window_likes/audio_player.rs | 177 +++++++++++++++++++++++++++++++ src/window_likes/malvim.rs | 21 +++- src/window_likes/minesweeper.rs | 29 ++--- src/window_likes/mod.rs | 1 + src/window_likes/start_menu.rs | 4 +- src/window_likes/terminal.rs | 33 ++---- src/window_manager.rs | 23 ++-- 13 files changed, 288 insertions(+), 54 deletions(-) create mode 100644 bmps/times-new-roman/=4.bmp create mode 100644 bmps/times-new-romono/=4.bmp create mode 100644 src/window_likes/audio_player.rs diff --git a/Cargo.toml b/Cargo.toml index ecfcec6..79a1f12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ blake2 = { version = "0.10.6", default-features = false } linux_framebuffer = { package = "framebuffer", version = "0.3.1" } bmp-rust = "0.4.1" termion = "4.0.3" +rodio = "0.19.0" +rand = "0.8.5" diff --git a/bmps/times-new-roman/=4.bmp b/bmps/times-new-roman/=4.bmp new file mode 100644 index 0000000000000000000000000000000000000000..739feca2963184301cac2e3425698484ce414aac GIT binary patch literal 162 ycmZ?rUBmzZW Vec>> { bmp } +pub fn get_all_files(dir: PathBuf) -> Vec { + let mut files = Vec::new(); + for entry in read_dir(dir).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + files.extend(get_all_files(path)); + } else { + files.push(path); + } + } + files +} + diff --git a/src/messages.rs b/src/messages.rs index 6db678a..5451cff 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -11,7 +11,7 @@ pub enum WindowManagerMessage { // } -pub type WindowBox = Box; +pub type WindowBox = Box; /* impl PartialEq for WindowBox { @@ -63,6 +63,7 @@ pub enum ShortcutType { StartMenu, SwitchWorkspace(u8), MoveWindowToWorkspace(u8), + FocusPrevWindow, FocusNextWindow, QuitWindow, MoveWindow(Direction), diff --git a/src/utils.rs b/src/utils.rs index 8e9913d..4cf710c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; pub trait Substring { fn substring(&self, start: usize, end: usize) -> &str; @@ -67,4 +68,38 @@ pub fn calc_new_cursor_pos(cursor_pos: usize, new_length: usize) -> usize { } } +pub fn concat_paths(current_path: &str, add_path: &str) -> Result { + let mut new_path = PathBuf::from(current_path); + if add_path.starts_with("/") { + //absolute path + new_path = PathBuf::from(add_path); + } else { + //relative path + for part in add_path.split("/") { + if part == ".." { + if let Some(parent) = new_path.parent() { + new_path = parent.to_path_buf(); + } else { + return Err(()); + } + } else { + new_path.push(part); + } + } + } + Ok(new_path) +} + +//go from seconds to minutes:seconds +pub fn format_seconds(seconds: u64) -> String { + let mut m = (seconds / 60).to_string(); //automatically rounds down + if m.len() == 1 { + m = "0".to_string() + &m; + } + let mut s = (seconds % 60).to_string(); + if s.len() == 1 { + s = "0".to_string() + &s; + } + m + ":" + &s +} diff --git a/src/window_likes/audio_player.rs b/src/window_likes/audio_player.rs new file mode 100644 index 0000000..38203ce --- /dev/null +++ b/src/window_likes/audio_player.rs @@ -0,0 +1,177 @@ +use std::vec::Vec; +use std::vec; +use std::io::BufReader; +use std::path::PathBuf; +use std::fs::File; + +use rodio::{ Decoder, OutputStream, Sink, Source }; +use rand::prelude::*; + +use crate::window_manager::{ DrawInstructions, WindowLike, WindowLikeType }; +use crate::messages::{ WindowMessage, WindowMessageResponse }; +use crate::framebuffer::Dimensions; +use crate::themes::ThemeInfo; +use crate::utils::{ concat_paths, format_seconds }; +use crate::fs::get_all_files; + +const MONO_WIDTH: u8 = 10; +const LINE_HEIGHT: usize = 18; + +#[derive(Default)] +pub struct AudioPlayer { + dimensions: Dimensions, + base_directory: String, + queue: Vec<(PathBuf, u64)>, + stream: Option>, + sink: Option, + command: String, + response: String, +} + +impl WindowLike for AudioPlayer { + fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse { + match message { + WindowMessage::Init(dimensions) => { + self.dimensions = dimensions; + WindowMessageResponse::JustRerender + }, + WindowMessage::ChangeDimensions(dimensions) => { + self.dimensions = dimensions; + WindowMessageResponse::JustRerender + }, + WindowMessage::KeyPress(key_press) => { + if key_press.key == '𐘂' { //the enter key + self.response = self.process_command(); + self.command = String::new(); + } else if key_press.key == '𐘁' { //backspace + if self.command.len() > 0 { + self.command = self.command[..self.command.len() - 1].to_string(); + } + } else { + self.command += &key_press.key.to_string(); + } + WindowMessageResponse::JustRerender + }, + _ => { + WindowMessageResponse::DoNothing + }, + } + } + + fn draw(&self, theme_info: &ThemeInfo) -> Vec { + let mut instructions = vec![DrawInstructions::Text([2, self.dimensions[1] - LINE_HEIGHT], "times-new-roman", 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 { + 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], "times-new-romono", current_name.clone(), 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], "times-new-romono", time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH))); + } + // + instructions + } + + //properties + + fn title(&self) -> &'static str { + "Audio Player" + } + + fn subtype(&self) -> WindowLikeType { + WindowLikeType::Window + } + + fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions { + [500, 200] + } + + fn resizable(&self) -> bool { + true + } +} + +impl AudioPlayer { + pub fn new() -> Self { + let mut ap: Self = Default::default(); + ap.base_directory = "/".to_string(); + ap + } + + //t: toggle pause/play + //h: prev + //l: next/skip + //j: volume down + //k: volume up + //b : set base directory + //p /: play directory or playlist in random order + //just hit enter 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(); + } + } + } else { + let parts: Vec<&str> = self.command.split(" ").collect(); + if self.command.starts_with("p ") { + 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 new_path.ends_with(".playlist") { + Vec::new() //placeholder + } else { + get_all_files(PathBuf::from(new_path)) + }; + let mut rng = rand::thread_rng(); + 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()); + let decoded = Decoder::new(file).unwrap(); + self.queue.push((item.clone(), decoded.total_duration().unwrap().as_secs())); + sink.append(decoded); + } + self.stream = Some(Box::new(stream)); + self.sink = Some(sink); + return "Playing".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]) { + if new_path.exists() { + self.base_directory = new_path.to_str().unwrap().to_string(); + return "Set new base directory".to_string(); + } + } + } + } + } + String::new() + } +} + diff --git a/src/window_likes/malvim.rs b/src/window_likes/malvim.rs index 661bc28..77cfcce 100644 --- a/src/window_likes/malvim.rs +++ b/src/window_likes/malvim.rs @@ -264,6 +264,7 @@ impl WindowLike for Malvim { self.files[self.current_file_index].changed = true; } } + self.calc_top_line_pos(); WindowMessageResponse::JustRerender }, WindowMessage::ChangeDimensions(dimensions) => { @@ -380,6 +381,21 @@ impl Malvim { pub fn new() -> Self { Default::default() } + + fn calc_top_line_pos(&mut self) { + if self.files.len() == 0 { + return; + } + //now, see if the line_pos is still visible from the top_line_pos, + //if not, move top_line_pos down until it is + let current_file = &self.files[self.current_file_index]; + let actual_line_pos = self.current.actual_lines.iter().position(|l| l.1 == current_file.line_pos).unwrap(); + if current_file.top_line_pos + self.current.max_lines < actual_line_pos { + self.files[self.current_file_index].top_line_pos = actual_line_pos.checked_sub(self.current.max_lines - 1).unwrap_or(0); + } else if actual_line_pos < current_file.top_line_pos { + self.files[self.current_file_index].top_line_pos = actual_line_pos; + } + } fn calc_current(&mut self) { if self.files.len() == 0 { @@ -389,12 +405,7 @@ impl Malvim { let line_num_width = current_file.content.len().to_string().len() * MONO_WIDTH as usize; let max_chars_per_line = (self.dimensions[0] - line_num_width - PADDING * 2) / MONO_WIDTH as usize; let actual_lines = calc_actual_lines(current_file.content.iter(), max_chars_per_line, true); - //now, see if the line_pos is still visible from the top_line_pos, - //if not, move top_line_pos down until it is let max_lines = (self.dimensions[1] - BAND_HEIGHT * 3 - PADDING) / LINE_HEIGHT; - if current_file.top_line_pos + max_lines < current_file.line_pos { - self.files[self.current_file_index].top_line_pos = current_file.line_pos.checked_sub(max_lines).unwrap_or(0); - } self.current = Current { actual_lines, line_num_width, diff --git a/src/window_likes/minesweeper.rs b/src/window_likes/minesweeper.rs index 8e16048..a7c9f75 100644 --- a/src/window_likes/minesweeper.rs +++ b/src/window_likes/minesweeper.rs @@ -31,7 +31,7 @@ struct MineTile { } #[derive(Default, PartialEq)] -enum MinesweeperState { +enum State { #[default] Seed, BeforePlaying, @@ -43,7 +43,7 @@ enum MinesweeperState { #[derive(Default)] pub struct Minesweeper { dimensions: Dimensions, - state: MinesweeperState, + state: State, tiles: [[MineTile; 16]; 16], random_chars: String, random_seed: u32, //user types in random keyboard stuff at beginning @@ -58,19 +58,19 @@ impl WindowLike for Minesweeper { WindowMessageResponse::JustRerender }, WindowMessage::KeyPress(key_press) => { - if self.state == MinesweeperState::Seed { + if self.state == State::Seed { if self.random_chars.len() == 4 { let mut r_chars = self.random_chars.chars(); self.random_seed = ((r_chars.next().unwrap() as u8 as u32) << 24) | ((r_chars.next().unwrap() as u8 as u32) << 16) | ((r_chars.next().unwrap() as u8 as u32) << 8) | (r_chars.next().unwrap() as u8 as u32); self.random_chars = String::new(); - self.state = MinesweeperState::BeforePlaying; + self.state = State::BeforePlaying; } else { if u8::try_from(key_press.key).is_ok() { self.random_chars.push(key_press.key); } } WindowMessageResponse::JustRerender - } else if self.state == MinesweeperState::BeforePlaying || self.state == MinesweeperState::Playing { + } else if self.state == State::BeforePlaying || self.state == State::Playing { if key_press.key == '𐘁' { //backspace self.first_char = '\0'; WindowMessageResponse::DoNothing @@ -84,7 +84,7 @@ impl WindowLike for Minesweeper { let y = u / 16; let x = u % 16; if HEX_CHARS.iter().find(|c| c == &&key_press.key).is_some() { - if self.state == MinesweeperState::BeforePlaying { + if self.state == State::BeforePlaying { loop { self.new_tiles(); if self.tiles[y][x].touching == 0 && !self.tiles[y][x].mine { @@ -92,11 +92,11 @@ impl WindowLike for Minesweeper { } } } - self.state = MinesweeperState::Playing; + self.state = State::Playing; //if that tile not reveal it, reveal it and all adjacent zero touching squares, etc if self.tiles[y][x].mine { self.tiles[y][x].revealed = true; - self.state = MinesweeperState::Lost; + self.state = State::Lost; } else if self.tiles[y][x].touching == 0 { let mut queue = VecDeque::new(); queue.push_back([x, y]); @@ -121,7 +121,7 @@ impl WindowLike for Minesweeper { self.tiles[y][x].revealed = true; } self.first_char = '\0'; - if self.state != MinesweeperState::Lost { + if self.state != State::Lost { //check for win let mut won = true; for y in 0..16 { @@ -133,7 +133,7 @@ impl WindowLike for Minesweeper { } } if won { - self.state = MinesweeperState::Won; + self.state = State::Won; } } WindowMessageResponse::JustRerender @@ -145,7 +145,7 @@ impl WindowLike for Minesweeper { } } else { self.tiles = Default::default(); - self.state = MinesweeperState::Seed; + self.state = State::Seed; WindowMessageResponse::DoNothing } }, @@ -154,7 +154,7 @@ impl WindowLike for Minesweeper { } fn draw(&self, theme_info: &ThemeInfo) -> Vec { - if self.state == MinesweeperState::Seed { + if self.state == State::Seed { vec![ DrawInstructions::Text([4, 4], "times-new-roman", "Type in random characters to initalise the seed".to_string(), theme_info.text, theme_info.background, None, None), DrawInstructions::Text([4, 4 + 16], "times-new-roman", self.random_chars.clone(), theme_info.text, theme_info.background, None, None), @@ -230,9 +230,9 @@ impl WindowLike for Minesweeper { } } } - if self.state == MinesweeperState::Lost { + if self.state == State::Lost { instructions.extend(vec![DrawInstructions::Text([4, 4], "times-new-roman", "You LOST!!! Press a key to play again.".to_string(), theme_info.text, theme_info.background, None, None)]); - } else if self.state == MinesweeperState::Won { + } else if self.state == State::Won { instructions.extend(vec![DrawInstructions::Text([4, 4], "times-new-roman", "You WON!!! Press a key to play again.".to_string(), theme_info.text, theme_info.background, None, None)]); } instructions @@ -240,6 +240,7 @@ impl WindowLike for Minesweeper { } //properties + fn title(&self) -> &'static str { "Minesweeper" } diff --git a/src/window_likes/mod.rs b/src/window_likes/mod.rs index dea554b..39676c6 100644 --- a/src/window_likes/mod.rs +++ b/src/window_likes/mod.rs @@ -7,4 +7,5 @@ pub mod workspace_indicator; pub mod minesweeper; pub mod terminal; pub mod malvim; +pub mod audio_player; diff --git a/src/window_likes/start_menu.rs b/src/window_likes/start_menu.rs index 493457d..c7f5e14 100644 --- a/src/window_likes/start_menu.rs +++ b/src/window_likes/start_menu.rs @@ -143,8 +143,10 @@ impl StartMenu { to_add.push("Minesweeper"); } else if name == "Editing" { to_add.push("Malvim"); - } else if name == "Files" { + } else if name == "Utils" { to_add.push("Terminal"); + } else if name == "Files" { + to_add.push("Audio Player"); } // for a in 0..to_add.len() { diff --git a/src/window_likes/terminal.rs b/src/window_likes/terminal.rs index df3c774..7a27880 100644 --- a/src/window_likes/terminal.rs +++ b/src/window_likes/terminal.rs @@ -2,13 +2,13 @@ use std::vec::Vec; use std::vec; use std::process::{ Command, Output }; use std::str::from_utf8; -use std::path::PathBuf; use std::io; use crate::window_manager::{ DrawInstructions, WindowLike, WindowLikeType }; use crate::messages::{ WindowMessage, WindowMessageResponse }; use crate::framebuffer::Dimensions; use crate::themes::ThemeInfo; +use crate::utils::concat_paths; const MONO_WIDTH: u8 = 10; const LINE_HEIGHT: usize = 15; @@ -41,6 +41,10 @@ impl WindowLike for Terminal { self.calc_actual_lines(); WindowMessageResponse::JustRerender }, + WindowMessage::ChangeDimensions(dimensions) => { + self.dimensions = dimensions; + WindowMessageResponse::JustRerender + }, WindowMessage::KeyPress(key_press) => { if key_press.key == '𐘁' { //backspace if self.current_input.len() > 0 { @@ -72,11 +76,6 @@ impl WindowLike for Terminal { self.actual_line_num = self.actual_lines.len().checked_sub(self.get_max_lines()).unwrap_or(0); WindowMessageResponse::JustRerender }, - WindowMessage::ChangeDimensions(dimensions) => { - self.dimensions = dimensions; - WindowMessageResponse::JustRerender - }, - // _ => WindowMessageResponse::DoNothing, } } @@ -132,28 +131,8 @@ impl Terminal { } else if self.current_input.starts_with("cd ") { let mut cd_split = self.current_input.split(" "); cd_split.next().unwrap(); - let mut failed = false; let arg = cd_split.next().unwrap(); - let mut new_path = PathBuf::from(&self.current_path); - if arg.starts_with("/") { - //absolute path - new_path = PathBuf::from(arg); - } else { - //relative path - for part in arg.split("/") { - if part == ".." { - if let Some(parent) = new_path.parent() { - new_path = parent.to_path_buf(); - } else { - failed = true; - } - } else { - new_path.push(part); - } - } - } - if !failed { - //see if path exists + if let Ok(new_path) = concat_paths(&self.current_path, arg) { if new_path.exists() { self.current_path = new_path.to_str().unwrap().to_string(); } diff --git a/src/window_manager.rs b/src/window_manager.rs index db13576..b632c24 100644 --- a/src/window_manager.rs +++ b/src/window_manager.rs @@ -25,6 +25,7 @@ use crate::window_likes::start_menu::StartMenu; use crate::window_likes::minesweeper::Minesweeper; use crate::window_likes::terminal::Terminal; use crate::window_likes::malvim::Malvim; +use crate::window_likes::audio_player::AudioPlayer; //todo, better error handling for windows @@ -112,7 +113,7 @@ pub enum Workspace { pub struct WindowLikeInfo { id: usize, - window_like: Box, + window_like: WindowBox, top_left: Point, dimensions: Dimensions, workspace: Workspace, @@ -154,7 +155,7 @@ impl WindowManager { wm } - pub fn add_window_like(&mut self, mut window_like: Box, top_left: Point, dimensions: Option) { + pub fn add_window_like(&mut self, mut window_like: Box, top_left: Point, dimensions: Option) { let subtype = window_like.subtype(); let dimensions = dimensions.unwrap_or(window_like.ideal_dimensions(self.dimensions)); self.id_count = self.id_count + 1; @@ -250,6 +251,7 @@ impl WindowManager { let shortcuts = HashMap::from([ //alt+e is terminate program (ctrl+c) ('s', ShortcutType::StartMenu), + ('[', ShortcutType::FocusPrevWindow), (']', ShortcutType::FocusNextWindow), ('q', ShortcutType::QuitWindow), ('c', ShortcutType::CenterWindow), @@ -377,13 +379,21 @@ impl WindowManager { } } }, - &ShortcutType::FocusNextWindow => { + &ShortcutType::FocusPrevWindow | &ShortcutType::FocusNextWindow => { let current_index = self.get_focused_index().unwrap_or(0); let mut new_focus_index = current_index; loop { - new_focus_index += 1; - if new_focus_index == self.window_infos.len() { - new_focus_index = 0; + if shortcut == &ShortcutType::FocusPrevWindow { + if new_focus_index == 0 { + new_focus_index = self.window_infos.len() - 1; + } else { + new_focus_index -= 1; + } + } else { + new_focus_index += 1; + if new_focus_index == self.window_infos.len() { + new_focus_index = 0; + } } if self.window_infos[new_focus_index].window_like.subtype() == WindowLikeType::Window && self.window_infos[new_focus_index].workspace == Workspace::Workspace(self.current_workspace) { //switch focus to this @@ -494,6 +504,7 @@ impl WindowManager { "Minesweeper" => Box::new(Minesweeper::new()), "Malvim" => Box::new(Malvim::new()), "Terminal" => Box::new(Terminal::new()), + "Audio Player" => Box::new(AudioPlayer::new()), "StartMenu" => Box::new(StartMenu::new()), _ => panic!("no such window"), };