Compare commits

...

60 Commits

Author SHA1 Message Date
krolxon 5e61e0fad5 fix #10: remove exec perms from LICENSE and README 2024-06-20 11:44:46 +05:30
krolxon 9941e1e42b v0.1.6 2024-06-19 15:23:10 +05:30
krolxon df10c2cf63 fix #9 2024-06-19 10:42:52 +05:30
krolxon f2b9051489 change event to event_handler 2024-06-15 00:06:35 +05:30
krolxon 5795d00831 add CHANGELOG.md 2024-06-15 00:06:15 +05:30
krolxon e1dc925ecc comply with clippy 2024-06-13 15:31:40 +05:30
krolxon 3862d5d324 update append_list when new playlist is created 2024-06-13 14:17:30 +05:30
krolxon 65f76181d3 new feature: add to new playlist 2024-06-12 18:13:05 +05:30
krolxon 8a8176e97b v0.1.5 2024-06-12 15:06:06 +05:30
krolxon a63845bae8 ui: volume mute state 2024-06-11 23:09:08 +05:30
krolxon a6fa8add79 add toggle mute keymap 2024-06-11 21:49:35 +05:30
krolxon 3e3ae64c72 fix crash when using playlist append 2024-06-08 00:00:53 +05:30
krolxon 8f41c6e1d0 seperate UI states into App 2024-06-07 23:50:05 +05:30
krolxon faaddc2bd7 add mouse scrolling 2024-06-07 23:49:51 +05:30
krolxon 53522c936d change String to &str in get_full_path() 2024-06-04 15:24:36 +05:30
krolxon a2f44eb6e6 v0.1.4 2024-06-01 20:52:44 +05:30
krolxon 15be9357da move FileExtension to utils.rs 2024-06-01 20:25:35 +05:30
krolxon 7ae0a2cc19 remove unnecessary variables from Connection 2024-06-01 16:27:52 +05:30
krolxon dc3f561de3 fix #8 2024-06-01 15:58:53 +05:30
krolxon 311cbc2631 workaround for #7, fix error when using 'Space' 2024-05-31 12:51:06 +05:30
krolxon def91deabe pl_append remove get_full_path() usage of Queue #7 2024-05-31 12:36:41 +05:30
krolxon 7b14b68164 workaround for #7, fix error when using 'Space' 2024-05-30 16:36:04 +05:30
krolxon 6ae0aca868 queue: highlight current playing song initially 2024-05-30 16:18:54 +05:30
krolxon b31e16554c workaround for #7 2024-05-25 11:09:54 +05:30
krolxon 48fd5a7508 fix #6 2024-05-24 15:15:52 +05:30
krolxon e3ba0169ea fix #5 2024-05-24 15:04:26 +05:30
krolxon 230c75790c update_status() on pause 2024-05-22 20:00:20 +05:30
krolxon 380a4193c9 fix sorting 2024-05-22 19:36:49 +05:30
krolxon c3dc9931d5 sort contents in filetree 2024-05-22 19:28:10 +05:30
krolxon 6a9e3d9801 v0.1.3 2024-05-21 12:04:35 +05:30
krolxon 6cc0b4eb9f v0.1.3 2024-05-19 20:04:39 +05:30
krolxon 2735afa943 <Space> for adding entire directory to queue 2024-05-19 19:55:27 +05:30
krolxon 1da0b85fa9 remove backspace keymap 2024-05-16 20:25:59 +05:30
krolyxon c51d9cc12e
Update README.md 2024-05-16 19:15:46 +05:30
krolxon 5d4e428f97 add '+' to increase volume 2024-05-16 19:14:59 +05:30
krolxon 0b184ec714 update screenshot 2024-05-16 19:09:35 +05:30
krolxon d48e57a3c7 convert keys into table in README.md 2024-05-16 19:07:58 +05:30
krolxon bcec798632 remove use of contains() 2024-05-16 18:24:18 +05:30
krolxon 35db5fb07d syntactical suger 2024-05-16 18:08:41 +05:30
krolxon 1d70e28c4e assert filenames instead of seeing if it contains 2024-05-16 18:08:34 +05:30
krolyxon ce06f50e56
Update README.md 2024-05-15 15:08:19 +05:30
krolxon f6a375fc46 Cargo.toml changes 2024-05-15 15:05:08 +05:30
krolxon 169ea38138 handle if mpd server is not running 2024-05-14 12:00:13 +05:30
krolxon a08c3a6e78 v0.1.2 2024-05-14 11:36:31 +05:30
krolxon 074021c5aa change highlighting of current song in queue 2024-05-14 01:38:48 +05:30
krolxon 420389664b sort pl_list 2024-05-14 01:38:20 +05:30
krolxon bba509fabf make dmenu optional, dont crash 2024-05-13 17:32:52 +05:30
krolxon bc7ab75f0c remove unnecessary comments 2024-05-13 17:28:56 +05:30
krolxon 2025f348f7 Revert "add demo"
This reverts commit ddf7423229.
2024-05-13 16:03:39 +05:30
krolxon be4505e5e5 wtf 2024-05-13 15:57:16 +05:30
krolxon ddf7423229 add demo 2024-05-13 15:42:42 +05:30
krolxon 4015a7e2a4 update Cargo.toml > v0.1.1 2024-05-13 15:27:51 +05:30
krolxon aca208e0d8 pl_append functionality for playlists 2024-05-13 14:33:24 +05:30
krolxon 9bdc76b6a9 ui: volume from bold to normal 2024-05-13 14:04:13 +05:30
krolxon f2783c00d2 instantly update status on playback controls 2024-05-13 14:04:00 +05:30
krolxon bb5bcea3ff fix #4 2024-05-13 13:41:26 +05:30
krolxon 1e02da68cc update songs list on db update 2024-05-12 21:29:51 +05:30
krolxon 51a3094d87 exclude hidden directories in browser 2024-05-12 21:10:28 +05:30
krolxon 98cfe4eb26 fix #3 2024-05-12 20:37:32 +05:30
krolxon 8c4d19f849 fix #2 2024-05-12 20:18:00 +05:30
23 changed files with 697 additions and 363 deletions

4
CHANGELOG.md Normal file
View File

@ -0,0 +1,4 @@
## rmptui-v0.1.6
- Add songs to new playlist feature
- Code refactoring
- fix #9

2
Cargo.lock generated
View File

@ -282,7 +282,7 @@ dependencies = [
[[package]] [[package]]
name = "rmptui" name = "rmptui"
version = "0.1.0" version = "0.1.6"
dependencies = [ dependencies = [
"crossterm", "crossterm",
"mpd", "mpd",

View File

@ -1,13 +1,22 @@
[package] [package]
name = "rmptui" name = "rmptui"
version = "0.1.0" authors = ["krolyxon"]
description = """
a fast and minimal tui mpd client
"""
readme = "README.md"
version = "0.1.6"
edition = "2021" edition = "2021"
repository = "https://github.com/krolyxon/rmptui"
keywords = ["rmptui", "mpd", "music", "cli", "tui", "client"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
mpd = "0.1.0" mpd = "0.1.0"
simple-dmenu = "0.1.0" simple-dmenu = "0.1.0"
ratatui = "0.26.2" ratatui = { version = "0.26.2", default-features = false, features = [
'crossterm',
] }
crossterm = "0.27.0" crossterm = "0.27.0"
rust-fuzzy-search = "0.1.1" rust-fuzzy-search = "0.1.1"

0
LICENSE Executable file → Normal file
View File

73
README.md Executable file → Normal file
View File

@ -1,38 +1,50 @@
## rmptui (Rust Music Player TUI(💀)) ## rmptui - A MPD client in Rust
A MPD client in Rust ![LOC](https://tokei.rs/b1/github/krolyxon/rmptui?category=code)
![Release](https://img.shields.io/github/v/release/krolyxon/rmptui?color=%23c694ff)
[![GitHub Downloads](https://img.shields.io/github/downloads/krolyxon/rmptui/total.svg?label=GitHub%20downloads)](https://github.com/krolyxon/rmptui/releases)
rmptui is a minimal tui mpd client made with rust.
## rmptui in action ## rmptui in action
![](https://raw.githubusercontent.com/krolyxon/rmptui/master/assets/ss.png) ![](https://raw.githubusercontent.com/krolyxon/rmptui/master/assets/ss.png)
### Keys ### Keys
- `q` OR `Ctr+C` to quit | Key | Action |
- `p` to toggle pause | --- | --- |
- `+` to increase volume | `q`/`Ctr+C` | Quit |
- `-` to decrease volume | `p` | Toggle pause |
- `D` to get dmenu prompt | `+`/`=` | Increase volume |
- `j` OR `Down` to scroll down | `-` | Decrease volume |
- `k` OR `Up` to scroll up | `m` | Toggle Mute |
- `J` to swap highlighted song with next one | `D` | Get dmenu prompt |
- `K` to swap highlighted song with previous one | `j`/`Down` | Scroll down |
- `l` OR `Right` add song to playlist or go inside the directory | `k`/`Up` | Scroll up |
- `h` OR `Left` to go back to previous directory | `J` | Swap highlighted song with next one |
- `Tab` to cycle through tabs | `K` | Swap highlighted song with previous one |
- `1` to go to queue | `l`/`Right` | Add song to playlist or go inside the directory |
- `2` to go to directory browser | `h`/`Left` | Go back to previous directory |
- `3` to go to playlists view | `Tab` | Cycle through tabs |
- `Enter` OR `l` OR `Right` to add song/playlist to current playlist | `1` | Go to queue |
- `a` to append the song to current playing queue | `2` | Go to directory browser |
- `Space`/`BackSpace` to delete the highlighted song from queue | `3` | Go to playlists view |
- `f` to go forwards | `Enter`/`l`/`Right` | Add song/playlist to current playlist |
- `b` to go backwards | `a` | Append the song to current playing queue |
- `>` to play next song from queue | `Space` | Delete the highlighted song from queue |
- `<` to play previous song from queue | `f` | Go forwards |
- `U` to update the MPD database | `b` | Go backwards |
- `r` to toggle repeat | `>` | Play next song from queue |
- `z` to toggle random | `<` | Play previous song from queue |
- `/` to search | `U` | Update the MPD database |
- `g` to go to top of list | `r` | Toggle repeat |
- `G` to go to bottom of list | `z` | Toggle random |
| `/` | Search |
| `R` | Rename Playlist |
| `g` | Go to top of list |
| `G` | Go to bottom of list |
### Prerequisites
- [MPD](https://wiki.archlinux.org/title/Music_Player_Daemon) installed and configured.
- [dmenu](https://tools.suckless.org/dmenu/) (optional)
### TODO ### TODO
- [x] fix performance issues - [x] fix performance issues
@ -43,4 +55,5 @@ A MPD client in Rust
- [x] metadata based tree view - [x] metadata based tree view
- [x] view playlist - [x] view playlist
- [x] change playlist name - [x] change playlist name
- [x] add to new playlist
- [ ] add lyrics fetcher - [ ] add lyrics fetcher

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -1,10 +1,12 @@
use std::time::Duration; use std::{path::Path, time::Duration};
use crate::browser::FileBrowser; use crate::browser::FileBrowser;
use crate::connection::Connection; use crate::connection::Connection;
use crate::list::ContentList; use crate::list::ContentList;
use crate::ui::InputMode; use crate::ui::InputMode;
use crate::utils::FileExtension;
use mpd::{Client, Song}; use mpd::{Client, Song};
use ratatui::widgets::{ListState, TableState};
// Application result type // Application result type
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>; pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
@ -12,24 +14,37 @@ pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
/// Application /// Application
#[derive(Debug)] #[derive(Debug)]
pub struct App { pub struct App {
pub running: bool, // Check if app is running pub running: bool, // Check if app is running
pub conn: Connection, // Connection pub conn: Connection, // Connection
pub browser: FileBrowser, // Directory browser pub browser: FileBrowser, // Directory browser
pub queue_list: ContentList<Song>, // Stores the current playing queue pub queue_list: ContentList<Song>, // Stores the current playing queue
pub pl_list: ContentList<String>, // Stores list of playlists pub pl_list: ContentList<String>, // Stores list of playlists
pub selected_tab: SelectedTab, // Used to switch between tabs pub selected_tab: SelectedTab, // Used to switch between tabs
// Search // Search
pub inputmode: InputMode, // Defines input mode, Normal or Search pub inputmode: InputMode, // Defines input mode, Normal or Search
pub search_input: String, // Stores the userinput to be searched pub search_input: String, // Stores the userinput to be searched
pub search_cursor_pos: usize, // Stores the cursor position for searching pub search_cursor_pos: usize, // Stores the cursor position for searching
pub pl_newname_input: String, // Stores the new name of the playlist pub pl_newname_input: String, // Stores the new name of the playlist
pub pl_cursor_pos: usize, // Stores the cursor position for renaming playlist pub pl_cursor_pos: usize, // Stores the cursor position for renaming playlist
pub pl_new_pl_input: String, // Stores the name of new playlist to be created
pub pl_new_pl_cursor_pos: usize, // Stores the cursor position of new playlist to be created
pub pl_new_pl_songs_buffer: Vec<Song>, // Buffer for songs that need to be added to the newly created playlist
// playlist variables // playlist variables
// used to show playlist popup // used to show playlist popup
pub playlist_popup: bool, pub playlist_popup: bool,
pub append_list: ContentList<String>, pub append_list: ContentList<String>,
// Determines if the database should be updated or not
pub should_update_song_list: bool,
// States
pub queue_state: TableState,
pub browser_state: TableState,
pub playlists_state: ListState,
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@ -41,16 +56,22 @@ pub enum SelectedTab {
impl App { impl App {
pub fn builder(addrs: &str) -> AppResult<Self> { pub fn builder(addrs: &str) -> AppResult<Self> {
let mut conn = Connection::new(addrs).unwrap(); let mut conn = Connection::builder(addrs)?;
let mut queue_list = ContentList::new(); let mut queue_list = ContentList::new();
let mut pl_list = ContentList::new(); let mut pl_list = ContentList::new();
pl_list.list = Self::get_playlist(&mut conn.conn)?; pl_list.list = Self::get_playlist(&mut conn.conn)?;
pl_list.list.sort();
let append_list = Self::get_append_list(&mut conn.conn)?; let append_list = Self::get_append_list(&mut conn.conn)?;
Self::get_queue(&mut conn, &mut queue_list.list); Self::get_queue(&mut conn, &mut queue_list.list);
let browser = FileBrowser::new(); let browser = FileBrowser::new();
let queue_state = TableState::new();
let browser_state = TableState::new();
let playlists_state = ListState::default();
Ok(Self { Ok(Self {
running: true, running: true,
conn, conn,
@ -64,13 +85,38 @@ impl App {
search_cursor_pos: 0, search_cursor_pos: 0,
pl_cursor_pos: 0, pl_cursor_pos: 0,
playlist_popup: false, playlist_popup: false,
pl_new_pl_input: String::new(),
pl_new_pl_cursor_pos: 0,
pl_new_pl_songs_buffer: Vec::new(),
append_list, append_list,
should_update_song_list: false,
queue_state,
browser_state,
playlists_state,
}) })
} }
pub fn tick(&mut self) { pub fn tick(&mut self) -> AppResult<()> {
self.conn.update_status(); self.conn.update_status();
self.update_queue(); self.update_queue();
// Deals with database update
if self.should_update_song_list && self.conn.status.updating_db.is_none() {
// Update the songs list
self.conn.songs_filenames = self
.conn
.conn
.listall()?
.into_iter()
.map(|x| x.file)
.collect();
self.browser.update_directory(&mut self.conn)?;
self.should_update_song_list = false;
}
Ok(())
} }
pub fn quit(&mut self) { pub fn quit(&mut self) {
@ -78,17 +124,6 @@ impl App {
} }
pub fn get_queue(conn: &mut Connection, vec: &mut Vec<Song>) { pub fn get_queue(conn: &mut Connection, vec: &mut Vec<Song>) {
// conn.conn.queue().unwrap().into_iter().for_each(|x| {
// if let Some(title) = x.title {
// if let Some(artist) = x.artist {
// vec.push(format!("{} - {}", artist, title));
// } else {
// vec.push(title)
// }
// } else {
// vec.push(x.file)
// }
// });
conn.conn.queue().unwrap().into_iter().for_each(|x| { conn.conn.queue().unwrap().into_iter().for_each(|x| {
vec.push(x); vec.push(x);
}); });
@ -108,6 +143,7 @@ impl App {
pub fn get_append_list(conn: &mut Client) -> AppResult<ContentList<String>> { pub fn get_append_list(conn: &mut Client) -> AppResult<ContentList<String>> {
let mut list = ContentList::new(); let mut list = ContentList::new();
list.list.push("Current Playlist".to_string()); list.list.push("Current Playlist".to_string());
list.list.push("New Playlist".to_string());
for item in Self::get_playlist(conn)? { for item in Self::get_playlist(conn)? {
list.list.push(item.to_string()); list.list.push(item.to_string());
} }
@ -119,23 +155,50 @@ impl App {
pub fn handle_add_or_remove_from_current_playlist(&mut self) -> AppResult<()> { pub fn handle_add_or_remove_from_current_playlist(&mut self) -> AppResult<()> {
match self.selected_tab { match self.selected_tab {
SelectedTab::DirectoryBrowser => { SelectedTab::DirectoryBrowser => {
let (_, file) = self.browser.filetree.get(self.browser.selected).unwrap(); let (content_type, content) =
self.browser.filetree.get(self.browser.selected).unwrap();
let mut status = false; if content_type == "directory" {
for (i, song) in self.queue_list.list.clone().iter().enumerate() { let file = format!("{}/{}", self.browser.path, content);
if song.file.contains(file) { let songs = self.conn.conn.listfiles(&file).unwrap_or_default();
self.conn.conn.delete(i as u32).unwrap(); for (t, f) in songs.iter() {
status = true; if t == "file"
&& Path::new(&f).has_extension(&[
"mp3", "ogg", "flac", "m4a", "wav", "aac", "opus", "ape", "wma",
"mpc", "aiff", "dff", "mp2", "mka",
])
{
let path = file.clone() + "/" + f;
let full_path = path.strip_prefix("./").unwrap_or("");
let song = self.conn.get_song_with_only_filename(full_path);
self.conn.conn.push(&song)?;
}
}
} else if content_type == "file" {
let mut status = false;
for (i, song) in self.queue_list.list.clone().iter().enumerate() {
let song_path = song.file.split('/').last().unwrap_or_default();
if song_path.eq(content) {
self.conn.conn.delete(i as u32).unwrap();
status = true;
}
} }
}
if !status { if !status {
if let Some(full_path) = &self.conn.get_full_path(file) { let mut filename = format!("{}/{}", self.browser.path, content);
let song = self.conn.get_song_with_only_filename(full_path);
// Remove "./" from the beginning of filename
filename.remove(0);
filename.remove(0);
let song = self.conn.get_song_with_only_filename(&filename);
self.conn.conn.push(&song)?; self.conn.conn.push(&song)?;
// updating queue, to avoid multiple pushes of the same songs if we enter multiple times before the queue gets updated
self.update_queue();
} }
} }
// Highlight next row if possible
if self.browser.selected != self.browser.filetree.len() - 1 { if self.browser.selected != self.browser.filetree.len() - 1 {
self.browser.selected += 1; self.browser.selected += 1;
} }
@ -154,7 +217,7 @@ impl App {
.to_string(); .to_string();
for (i, song) in self.queue_list.list.clone().iter().enumerate() { for (i, song) in self.queue_list.list.clone().iter().enumerate() {
if song.file.contains(&file) { if song.file.eq(&file) {
self.conn.conn.delete(i as u32).unwrap(); self.conn.conn.delete(i as u32).unwrap();
if self.queue_list.index == self.queue_list.list.len() - 1 if self.queue_list.index == self.queue_list.list.len() - 1
&& self.queue_list.index != 0 && self.queue_list.index != 0
@ -169,6 +232,7 @@ impl App {
} }
self.update_queue(); self.update_queue();
self.conn.update_status();
Ok(()) Ok(())
} }
@ -187,37 +251,29 @@ impl App {
let (t, path) = browser.filetree.get(browser.selected).unwrap(); let (t, path) = browser.filetree.get(browser.selected).unwrap();
if t == "directory" { if t == "directory" {
if path != "." { if path != "." {
browser.prev_path = browser.path.clone(); browser.prev_path.clone_from(&browser.path);
browser.path = browser.prev_path.clone() + "/" + path; browser.path = browser.prev_path.clone() + "/" + path;
browser.update_directory(&mut self.conn)?; browser.update_directory(&mut self.conn)?;
browser.prev_selected = browser.selected; browser.prev_selected = browser.selected;
browser.selected = 0; browser.selected = 0;
} }
} else { } else {
// let list = conn let index = self.queue_list.list.iter().position(|x| {
// .songs_filenames let file = x.file.split('/').last().unwrap_or_default();
// .iter() file.eq(path)
// .map(|f| f.as_str()) });
// .collect::<Vec<&str>>();
// let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&path, &list)
// .get(0)
// .unwrap()
// .clone();
let index = self
.queue_list
.list
.iter()
.position(|x| x.file.contains(path));
if index.is_some() { if index.is_some() {
self.conn.conn.switch(index.unwrap() as u32)?; self.conn.conn.switch(index.unwrap() as u32)?;
} else { } else {
for filename in self.conn.songs_filenames.clone().iter() { let mut filename = format!("{}/{}", browser.path, path);
if filename.contains(path) {
let song = self.conn.get_song_with_only_filename(filename); // Remove "./" from the beginning of filename
self.conn.push(&song)?; filename.remove(0);
} filename.remove(0);
}
let song = self.conn.get_song_with_only_filename(&filename);
self.conn.push(&song)?;
// updating queue, to avoid multiple pushes of the same songs if we enter multiple times before the queue gets updated // updating queue, to avoid multiple pushes of the same songs if we enter multiple times before the queue gets updated
self.update_queue(); self.update_queue();
@ -237,6 +293,10 @@ impl App {
let cursor_moved_left = self.search_cursor_pos.saturating_sub(1); let cursor_moved_left = self.search_cursor_pos.saturating_sub(1);
self.search_cursor_pos = self.clamp_cursor(cursor_moved_left); self.search_cursor_pos = self.clamp_cursor(cursor_moved_left);
} }
InputMode::NewPlaylist => {
let cursor_moved_left = self.pl_new_pl_cursor_pos.saturating_sub(1);
self.pl_new_pl_cursor_pos = self.clamp_cursor(cursor_moved_left);
}
_ => {} _ => {}
} }
} }
@ -251,6 +311,12 @@ impl App {
let cursor_moved_right = self.search_cursor_pos.saturating_add(1); let cursor_moved_right = self.search_cursor_pos.saturating_add(1);
self.search_cursor_pos = self.clamp_cursor(cursor_moved_right); self.search_cursor_pos = self.clamp_cursor(cursor_moved_right);
} }
InputMode::NewPlaylist => {
let cursor_moved_right = self.pl_new_pl_cursor_pos.saturating_add(1);
self.pl_new_pl_cursor_pos = self.clamp_cursor(cursor_moved_right);
}
_ => {} _ => {}
} }
} }
@ -259,19 +325,24 @@ impl App {
match self.inputmode { match self.inputmode {
InputMode::PlaylistRename => { InputMode::PlaylistRename => {
self.pl_newname_input.insert(self.pl_cursor_pos, new_char); self.pl_newname_input.insert(self.pl_cursor_pos, new_char);
self.move_cursor_right(); }
InputMode::NewPlaylist => {
self.pl_new_pl_input
.insert(self.pl_new_pl_cursor_pos, new_char);
} }
InputMode::Editing => { InputMode::Editing => {
self.search_input.insert(self.search_cursor_pos, new_char); self.search_input.insert(self.search_cursor_pos, new_char);
self.move_cursor_right();
} }
_ => {} _ => {}
} }
self.move_cursor_right();
} }
pub fn delete_char(&mut self) { pub fn delete_char(&mut self) {
let is_not_cursor_leftmost = match self.inputmode { let is_not_cursor_leftmost = match self.inputmode {
InputMode::PlaylistRename => self.pl_cursor_pos != 0, InputMode::PlaylistRename => self.pl_cursor_pos != 0,
InputMode::NewPlaylist => self.pl_new_pl_cursor_pos != 0,
InputMode::Editing => self.search_cursor_pos != 0, InputMode::Editing => self.search_cursor_pos != 0,
_ => false, _ => false,
}; };
@ -284,31 +355,35 @@ impl App {
let current_index = match self.inputmode { let current_index = match self.inputmode {
InputMode::Editing => self.search_cursor_pos, InputMode::Editing => self.search_cursor_pos,
InputMode::PlaylistRename => self.pl_cursor_pos, InputMode::PlaylistRename => self.pl_cursor_pos,
InputMode::NewPlaylist => self.pl_new_pl_cursor_pos,
_ => 0, _ => 0,
}; };
let from_left_to_current_index = current_index - 1; let from_left_to_current_index = current_index - 1;
if self.inputmode == InputMode::PlaylistRename { if self.inputmode == InputMode::PlaylistRename {
// Getting all characters before the selected character.
let before_char_to_delete = self let before_char_to_delete = self
.pl_newname_input .pl_newname_input
.chars() .chars()
.take(from_left_to_current_index); .take(from_left_to_current_index);
// Getting all characters after selected character.
let after_char_to_delete = self.pl_newname_input.chars().skip(current_index); let after_char_to_delete = self.pl_newname_input.chars().skip(current_index);
// Put all characters together except the selected one.
// By leaving the selected one out, it is forgotten and therefore deleted.
self.pl_newname_input = before_char_to_delete.chain(after_char_to_delete).collect(); self.pl_newname_input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left(); self.move_cursor_left();
} else if self.inputmode == InputMode::NewPlaylist {
let before_char_to_delete = self
.pl_new_pl_input
.chars()
.take(from_left_to_current_index);
let after_char_to_delete = self.pl_new_pl_input.chars().skip(current_index);
self.pl_new_pl_input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left();
} else if self.inputmode == InputMode::Editing { } else if self.inputmode == InputMode::Editing {
// Getting all characters before the selected character.
let before_char_to_delete = let before_char_to_delete =
self.search_input.chars().take(from_left_to_current_index); self.search_input.chars().take(from_left_to_current_index);
// Getting all characters after selected character.
let after_char_to_delete = self.search_input.chars().skip(current_index); let after_char_to_delete = self.search_input.chars().skip(current_index);
// Put all characters together except the selected one.
// By leaving the selected one out, it is forgotten and therefore deleted.
self.search_input = before_char_to_delete.chain(after_char_to_delete).collect(); self.search_input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left(); self.move_cursor_left();
} }
@ -318,6 +393,7 @@ impl App {
pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
match self.inputmode { match self.inputmode {
InputMode::PlaylistRename => new_cursor_pos.clamp(0, self.pl_newname_input.len()), InputMode::PlaylistRename => new_cursor_pos.clamp(0, self.pl_newname_input.len()),
InputMode::NewPlaylist => new_cursor_pos.clamp(0, self.pl_new_pl_input.len()),
InputMode::Editing => new_cursor_pos.clamp(0, self.search_input.len()), InputMode::Editing => new_cursor_pos.clamp(0, self.search_input.len()),
_ => 0, _ => 0,
} }
@ -331,6 +407,9 @@ impl App {
InputMode::PlaylistRename => { InputMode::PlaylistRename => {
self.pl_cursor_pos = 0; self.pl_cursor_pos = 0;
} }
InputMode::NewPlaylist => {
self.pl_new_pl_cursor_pos = 0;
}
_ => {} _ => {}
} }
} }
@ -348,10 +427,36 @@ impl App {
} }
} }
pub fn change_playlist_name(&mut self) -> AppResult<()> { // Mouse event handlers
if self.selected_tab == SelectedTab::Playlists { pub fn handle_scroll_up(&mut self) {
self.inputmode = InputMode::PlaylistRename; match self.selected_tab {
SelectedTab::Queue => {
self.queue_list.prev();
}
SelectedTab::DirectoryBrowser => {
self.browser.prev();
}
SelectedTab::Playlists => {
self.pl_list.prev();
}
} }
}
pub fn handle_scroll_down(&mut self) {
match self.selected_tab {
SelectedTab::Queue => {
self.queue_list.next();
}
SelectedTab::DirectoryBrowser => {
self.browser.next();
}
SelectedTab::Playlists => {
self.pl_list.next();
}
}
}
pub fn handle_mouse_left_click(&mut self, _x: u16, _y: u16) -> AppResult<()> {
Ok(()) Ok(())
} }
} }

View File

@ -1,9 +1,8 @@
use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
use mpd::Song; use mpd::Song;
use crate::{app::AppResult, connection::Connection}; use crate::{app::AppResult, connection::Connection, utils::FileExtension};
#[derive(Debug)] #[derive(Debug)]
/// struct for working with directory browser tab in rmptui /// struct for working with directory browser tab in rmptui
@ -16,23 +15,6 @@ pub struct FileBrowser {
pub songs: Vec<Song>, pub songs: Vec<Song>,
} }
// https://stackoverflow.com/questions/72392835/check-if-a-file-is-of-a-given-type
pub trait FileExtension {
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool;
}
impl<P: AsRef<Path>> FileExtension for P {
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool {
if let Some(extension) = self.as_ref().extension().and_then(OsStr::to_str) {
return extensions
.iter()
.any(|x| x.as_ref().eq_ignore_ascii_case(extension));
}
false
}
}
impl FileBrowser { impl FileBrowser {
pub fn new() -> FileBrowser { pub fn new() -> FileBrowser {
FileBrowser { FileBrowser {
@ -50,7 +32,7 @@ impl FileBrowser {
let mut file_vec: Vec<(String, String)> = vec![]; let mut file_vec: Vec<(String, String)> = vec![];
let mut dir_vec: Vec<(String, String)> = vec![]; let mut dir_vec: Vec<(String, String)> = vec![];
for (t, f) in conn.conn.listfiles(self.path.as_str())?.into_iter() { for (t, f) in conn.conn.listfiles(self.path.as_str())?.into_iter() {
if t == "directory" { if t == "directory" && !f.starts_with('.') {
dir_vec.push((t, f)); dir_vec.push((t, f));
} else if t == "file" } else if t == "file"
&& Path::new(&f).has_extension(&[ && Path::new(&f).has_extension(&[
@ -62,19 +44,37 @@ impl FileBrowser {
} }
} }
// dir_vec.sort_by(|a, b| a.1.cmp(&b.1));
dir_vec.sort_by(|a, b| {
let num_a = a.1.parse::<u32>().unwrap_or(u32::MAX);
let num_b = b.1.parse::<u32>().unwrap_or(u32::MAX);
num_a
.cmp(&num_b)
.then_with(|| a.1.to_lowercase().cmp(&b.1.to_lowercase()))
});
file_vec.sort_by(|a, b| {
let num_a = a.1.parse::<u32>().unwrap_or(u32::MAX);
let num_b = b.1.parse::<u32>().unwrap_or(u32::MAX);
num_a
.cmp(&num_b)
.then_with(|| a.1.to_lowercase().cmp(&b.1.to_lowercase()))
});
// Add metadata
dir_vec.extend(file_vec); dir_vec.extend(file_vec);
self.filetree = dir_vec; self.filetree = dir_vec;
// Add metadata
self.songs.clear(); self.songs.clear();
for (t, song) in self.filetree.iter() { for (t, song) in self.filetree.iter() {
if t == "file" { if t == "file" {
let v = conn let v = conn
.conn .conn
.lsinfo(Song { .lsinfo(Song {
file: conn file: (self.path.clone() + "/" + song)
.get_full_path(song) .strip_prefix("./")
.unwrap_or_else(|| "Not a song".to_string()), .unwrap_or("")
.to_string(),
..Default::default() ..Default::default()
}) })
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
@ -103,12 +103,6 @@ impl FileBrowser {
if self.selected < self.filetree.len() - 1 { if self.selected < self.filetree.len() - 1 {
self.selected += 1; self.selected += 1;
} }
// if self.selected == self.filetree.len() - 1 {
// self.selected = 0;
// } else {
// self.selected += 1;
// }
} }
/// Go to previous item in filetree /// Go to previous item in filetree
@ -116,21 +110,18 @@ impl FileBrowser {
if self.selected != 0 { if self.selected != 0 {
self.selected -= 1; self.selected -= 1;
} }
// if self.selected == 0 {
// self.selected = self.filetree.len() - 1;
// } else {
// self.selected -= 1;
// }
} }
/// handles going back event /// handles going back event
pub fn handle_go_back(&mut self, conn: &mut Connection) -> AppResult<()> { pub fn handle_go_back(&mut self, conn: &mut Connection) -> AppResult<()> {
if self.prev_path != "." { if self.prev_path != "." {
let r = self.path.rfind('/').unwrap(); let r = self.path.rfind('/');
self.path = self.path.as_str()[..r].to_string(); if let Some(r) = r {
self.update_directory(conn)?; self.path = self.path.as_str()[..r].to_string();
self.update_directory(conn)?;
}
} else { } else {
self.path = self.prev_path.clone(); self.path.clone_from(&self.prev_path);
self.update_directory(conn)?; self.update_directory(conn)?;
} }
@ -139,8 +130,6 @@ impl FileBrowser {
} }
} }
impl Default for FileBrowser { impl Default for FileBrowser {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()

View File

@ -1,11 +1,16 @@
use crate::app::AppResult;
use crate::utils::is_installed;
use mpd::song::Song; use mpd::song::Song;
use mpd::{Client, State}; use mpd::{Client, State};
use simple_dmenu::dmenu; use simple_dmenu::dmenu;
use std::process::Command;
use std::time::Duration; use std::time::Duration;
pub type Result<T> = core::result::Result<T, Error>; /// Defines the current status of volume (Muted or UnMuted)
pub type Error = Box<dyn std::error::Error>; #[derive(Debug)]
pub enum VolumeStatus {
Muted(i8),
Unmuted,
}
#[derive(Debug)] #[derive(Debug)]
/// struct storing the mpd Client related stuff /// struct storing the mpd Client related stuff
@ -15,100 +20,94 @@ pub struct Connection {
pub state: String, pub state: String,
pub elapsed: Duration, pub elapsed: Duration,
pub total_duration: Duration, pub total_duration: Duration,
pub volume: u8,
pub repeat: bool,
pub random: bool,
pub current_song: Song, pub current_song: Song,
pub stats: mpd::Stats, pub stats: mpd::Stats,
pub status: mpd::Status,
pub volume_status: VolumeStatus,
} }
impl Connection { impl Connection {
/// Create a new connection /// Create a new connection
pub fn new(addrs: &str) -> Result<Self> { pub fn builder(addrs: &str) -> AppResult<Self> {
let mut conn = Client::connect(addrs).unwrap(); let mut conn = Client::connect(addrs).unwrap_or_else(|_| {
eprintln!("Error connecting to mpd server, Make sure mpd is running");
std::process::exit(1);
});
let empty_song = Song { let empty_song = Song {
file: "No Song playing or in Queue".to_string(), file: "No Song playing or in Queue".to_string(),
..Default::default() ..Default::default()
}; };
let songs_filenames: Vec<String> = conn let songs_filenames: Vec<String> = conn.listall()?.into_iter().map(|x| x.file).collect();
.listall()
.unwrap()
.into_iter()
.map(|x| x.file)
.collect();
let status = conn.status().unwrap(); let status = conn.status().unwrap();
let (elapsed, total) = status.time.unwrap_or_default(); let (elapsed, total) = status.time.unwrap_or_default();
let volume: u8 = status.volume as u8;
let repeat = status.repeat;
let random = status.random;
let stats = conn.stats().unwrap_or_default(); let stats = conn.stats().unwrap_or_default();
let current_song = conn let current_song = conn
.currentsong() .currentsong()
.unwrap_or_else(|_| Some(empty_song.clone())) .unwrap_or_else(|_| Some(empty_song.clone()))
.unwrap_or(empty_song); .unwrap_or(empty_song);
let volume_status = VolumeStatus::Unmuted;
Ok(Self { Ok(Self {
conn, conn,
songs_filenames, songs_filenames,
state: "Stopped".to_string(), state: "Stopped".to_string(),
elapsed, elapsed,
total_duration: total, total_duration: total,
volume,
repeat,
random,
current_song, current_song,
stats, stats,
status,
volume_status,
}) })
} }
/// Dmenu prompt for selecting songs /// Dmenu prompt for selecting songs
pub fn play_dmenu(&mut self) -> Result<()> { pub fn play_dmenu(&mut self) -> AppResult<()> {
is_installed("dmenu")?; if is_installed("dmenu") {
let ss: Vec<&str> = self.songs_filenames.iter().map(|x| x.as_str()).collect(); let ss: Vec<&str> = self.songs_filenames.iter().map(|x| x.as_str()).collect();
let op = dmenu!(iter &ss; args "-p", "Choose a song: ", "-l", "30"); let op = dmenu!(iter &ss; args "-p", "Choose a song: ", "-l", "30");
let index = ss.iter().position(|s| s == &op); let index = ss.iter().position(|s| s == &op);
if let Some(i) = index { if let Some(i) = index {
let song = self.get_song_with_only_filename(ss.get(i).unwrap()); let song = self.get_song_with_only_filename(ss.get(i).unwrap());
self.push(&song)?; self.push(&song)?;
}
} }
Ok(()) Ok(())
} }
/// Update status /// Update status
pub fn update_status(&mut self) { pub fn update_status(&mut self) {
let status = self.conn.status().unwrap(); let status = self.conn.status().unwrap();
let stats = self.conn.stats().unwrap_or_default();
let empty_song = self.get_song_with_only_filename("No Song playing or in Queue"); let empty_song = self.get_song_with_only_filename("No Song playing or in Queue");
let current_song = self let current_song = self
.conn .conn
.currentsong() .currentsong()
.unwrap_or_else(|_| Some(empty_song.clone())) .unwrap_or_else(|_| Some(empty_song.clone()))
.unwrap_or(empty_song); .unwrap_or(empty_song);
let stats = self.conn.stats().unwrap_or_default();
// Status
self.status = status.clone();
// Playback State // Playback State
match status.state { self.state = match status.state {
State::Stop => self.state = "Stopped".to_string(), State::Stop => "Stopped".to_string(),
State::Play => self.state = "Playing".to_string(), State::Play => "Playing".to_string(),
State::Pause => self.state = "Paused".to_string(), State::Pause => "Paused".to_string(),
} };
// Progress // Progress
let (elapsed, total) = status.time.unwrap_or_default(); let (elapsed, total) = status.time.unwrap_or_default();
self.elapsed = elapsed; self.elapsed = elapsed;
self.total_duration = total; self.total_duration = total;
// Volume
self.volume = status.volume as u8;
// Repeat mode
self.repeat = status.repeat;
// Random mode
self.random = status.random;
// Current song // Current song
self.current_song = current_song; self.current_song = current_song;
@ -132,7 +131,7 @@ impl Connection {
} }
/// push the given song to queue /// push the given song to queue
pub fn push(&mut self, song: &Song) -> Result<()> { pub fn push(&mut self, song: &Song) -> AppResult<()> {
if self.conn.queue().unwrap().is_empty() { if self.conn.queue().unwrap().is_empty() {
self.conn.push(song).unwrap(); self.conn.push(song).unwrap();
self.conn.play().unwrap(); self.conn.play().unwrap();
@ -149,14 +148,14 @@ impl Connection {
} }
/// Push all songs of a playlist into queue /// Push all songs of a playlist into queue
pub fn load_playlist(&mut self, playlist: &str) -> Result<()> { pub fn load_playlist(&mut self, playlist: &str) -> AppResult<()> {
self.conn.load(playlist, ..)?; self.conn.load(playlist, ..)?;
self.conn.play()?; self.conn.play()?;
Ok(()) Ok(())
} }
/// Add given song to playlist /// Add given song to playlist
pub fn add_to_playlist(&mut self, playlist: &str, song: &Song) -> Result<()> { pub fn add_to_playlist(&mut self, playlist: &str, song: &Song) -> AppResult<()> {
self.conn.pl_push(playlist, song)?; self.conn.pl_push(playlist, song)?;
Ok(()) Ok(())
} }
@ -170,17 +169,17 @@ impl Connection {
} }
/// Given a song name from a directory, it returns the full path of the song in the database /// Given a song name from a directory, it returns the full path of the song in the database
pub fn get_full_path(&self, short_path: &str) -> Option<String> { pub fn get_full_path(&self, short_path: &str) -> Option<&str> {
for (i, f) in self.songs_filenames.iter().enumerate() { for (i, f) in self.songs_filenames.iter().enumerate() {
if f.contains(short_path) { if f.contains(short_path) {
return Some(self.songs_filenames.get(i).unwrap().to_string()); return Some(self.songs_filenames.get(i).unwrap());
} }
} }
None None
} }
/// Gives title of current playing song /// Gives title of current playing song
pub fn now_playing(&mut self) -> Result<Option<String>> { pub fn now_playing(&mut self) -> AppResult<Option<String>> {
if let Some(s) = &self.current_song.title { if let Some(s) = &self.current_song.title {
if let Some(a) = &self.current_song.artist { if let Some(a) = &self.current_song.artist {
Ok(Some(format!("\"{}\" By {}", s, a))) Ok(Some(format!("\"{}\" By {}", s, a)))
@ -205,50 +204,32 @@ impl Connection {
/// Toggle Repeat mode /// Toggle Repeat mode
pub fn toggle_repeat(&mut self) { pub fn toggle_repeat(&mut self) {
if self.conn.status().unwrap().repeat { let mode = self.status.repeat;
self.conn.repeat(false).unwrap(); self.conn.repeat(!mode).unwrap();
} else {
self.conn.repeat(true).unwrap();
}
} }
/// Toggle random mode /// Toggle random mode
pub fn toggle_random(&mut self) { pub fn toggle_random(&mut self) {
if self.conn.status().unwrap().random { let mode = self.status.random;
self.conn.random(false).unwrap(); self.conn.random(!mode).unwrap();
} else {
self.conn.random(true).unwrap();
}
} }
// Volume controls // Volume controls
/// Increase Volume /// Increase Volume
pub fn inc_volume(&mut self, v: i8) { pub fn inc_volume(&mut self, v: i8) {
let cur = self.conn.status().unwrap().volume; let cur = self.status.volume;
if cur + v <= 100 { if cur + v <= 100 {
self.volume_status = VolumeStatus::Unmuted;
self.conn.volume(cur + v).unwrap(); self.conn.volume(cur + v).unwrap();
} }
} }
/// Decrease volume /// Decrease volume
pub fn dec_volume(&mut self, v: i8) { pub fn dec_volume(&mut self, v: i8) {
let cur = self.conn.status().unwrap().volume; let cur = self.status.volume;
if cur - v >= 0 { if cur - v >= 0 {
self.volume_status = VolumeStatus::Unmuted;
self.conn.volume(cur - v).unwrap(); self.conn.volume(cur - v).unwrap();
} }
} }
} }
/// Checks if given program is installed in your system
fn is_installed(ss: &str) -> Result<()> {
let output = Command::new("which")
.arg(ss)
.output()
.expect("Failed to execute command");
if output.status.success() {
Ok(())
} else {
let err = format!("{} not installed", ss);
Err(err.into())
}
}

View File

@ -1,82 +0,0 @@
use crate::app::{App, AppResult, SelectedTab};
use crate::browser::FileExtension;
use crossterm::event::{KeyCode, KeyEvent};
use std::path::Path;
pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
match key_event.code {
KeyCode::Char('q') | KeyCode::Esc => {
app.playlist_popup = false;
}
KeyCode::Char('j') | KeyCode::Down => app.append_list.next(),
KeyCode::Char('k') | KeyCode::Up => app.append_list.prev(),
KeyCode::Enter => {
let pl_index = app.append_list.index;
let pl_name = &app.append_list.list.get(pl_index).unwrap();
let s_index: usize;
match app.selected_tab {
SelectedTab::Queue => {
s_index = app.queue_list.index;
let short_path = &app.queue_list.list.get(s_index).unwrap().file;
if let Some(full_path) = app.conn.get_full_path(short_path) {
let song = app.conn.get_song_with_only_filename(&full_path);
if *pl_name == "Current Playlist" {
app.conn.conn.push(&song)?;
app.update_queue();
} else {
app.conn.add_to_playlist(pl_name, &song)?;
}
}
}
SelectedTab::DirectoryBrowser => {
let (t, f) = app.browser.filetree.get(app.browser.selected).unwrap();
if t == "file" {
let short_path = f;
if let Some(full_path) = app.conn.get_full_path(short_path) {
let song = app.conn.get_song_with_only_filename(&full_path);
if *pl_name == "Current Playlist" {
app.conn.conn.push(&song)?;
app.update_queue();
} else {
app.conn.add_to_playlist(pl_name, &song)?;
}
}
} else if t == "directory" {
for (t, f) in app.conn.conn.listfiles(f)?.iter() {
// dir_vec.push((t, f));
if t == "file"
&& Path::new(&f).has_extension(&[
"mp3", "ogg", "flac", "m4a", "wav", "aac", "opus", "ape",
"wma", "mpc", "aiff", "dff", "mp2", "mka",
])
{
let full_path = app.conn.get_full_path(f).unwrap_or_default();
let song = app.conn.get_song_with_only_filename(&full_path);
if *pl_name == "Current Playlist" {
app.conn.conn.push(&song)?;
} else {
app.conn.add_to_playlist(pl_name, &song)?;
}
}
}
}
}
_ => {}
}
// hide the playlist popup
app.playlist_popup = false;
app.append_list.index = 0;
}
_ => {}
}
Ok(())
}

0
src/event/event.rs → src/event_handler/event.rs Executable file → Normal file
View File

97
src/event/handler.rs → src/event_handler/handler.rs Executable file → Normal file
View File

@ -1,11 +1,12 @@
use crate::{ use crate::{
app::{App, AppResult, SelectedTab}, app::{App, AppResult, SelectedTab},
connection::VolumeStatus,
ui::InputMode, ui::InputMode,
}; };
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use std::time::Duration; use std::time::Duration;
use super::{pl_append_keys, pl_rename_keys, search_keys}; use super::{new_pl_keys, pl_append_keys, pl_rename_keys, search_keys};
pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
// searching, playlist renaming, playlist appending // searching, playlist renaming, playlist appending
@ -13,6 +14,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
search_keys::handle_search_keys(key_event, app)?; search_keys::handle_search_keys(key_event, app)?;
} else if app.inputmode == InputMode::PlaylistRename { } else if app.inputmode == InputMode::PlaylistRename {
pl_rename_keys::handle_pl_rename_keys(key_event, app)?; pl_rename_keys::handle_pl_rename_keys(key_event, app)?;
} else if app.inputmode == InputMode::NewPlaylist {
new_pl_keys::handle_new_pl_keys(key_event, app)?;
} else if app.playlist_popup { } else if app.playlist_popup {
pl_append_keys::hande_pl_append_keys(key_event, app)?; pl_append_keys::hande_pl_append_keys(key_event, app)?;
} else { } else {
@ -33,10 +36,16 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
// Playback controls // Playback controls
// Toggle Pause // Toggle Pause
KeyCode::Char('p') => app.conn.toggle_pause(), KeyCode::Char('p') => {
app.conn.toggle_pause();
app.conn.update_status();
}
// Pause // Pause
KeyCode::Char('s') => app.conn.pause(), KeyCode::Char('s') => {
app.conn.pause();
app.conn.update_status();
}
// Toggle rpeat // Toggle rpeat
KeyCode::Char('r') => { KeyCode::Char('r') => {
@ -51,25 +60,36 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
} }
// Dmenu prompt // Dmenu prompt
KeyCode::Char('D') => app.conn.play_dmenu()?, KeyCode::Char('D') => {
app.conn.play_dmenu()?;
app.conn.update_status();
}
// add to queue // add to queue
KeyCode::Char('a') => app.playlist_popup = true, KeyCode::Char('a') => app.playlist_popup = true,
// Fast forward // Fast forward
KeyCode::Char('f') => { KeyCode::Char('f') => {
let place = app.conn.conn.status().unwrap().song.unwrap().pos; if !app.queue_list.list.is_empty() {
let (pos, _) = app.conn.conn.status().unwrap().time.unwrap(); let status = app.conn.conn.status().unwrap_or_default();
let pos = Duration::from_secs(pos.as_secs().wrapping_add(2)); let place = status.song.unwrap_or_default().pos;
app.conn.conn.seek(place, pos)?; let (pos, _) = status.time.unwrap_or_default();
let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
app.conn.conn.seek(place, pos)?;
app.conn.update_status();
}
} }
// backward // backward
KeyCode::Char('b') => { KeyCode::Char('b') => {
let place = app.conn.conn.status().unwrap().song.unwrap().pos; if !app.queue_list.list.is_empty() {
let (pos, _) = app.conn.conn.status().unwrap().time.unwrap(); let status = app.conn.conn.status().unwrap_or_default();
let pos = Duration::from_secs(pos.as_secs().wrapping_add(2)); let place = status.song.unwrap_or_default().pos;
app.conn.conn.seek(place, pos)?; let (pos, _) = status.time.unwrap_or_default();
let pos = Duration::from_secs(pos.as_secs().wrapping_sub(2));
app.conn.conn.seek(place, pos)?;
app.conn.update_status();
}
} }
// Cycle through tabs // Cycle through tabs
@ -111,7 +131,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
} }
// Volume controls // Volume controls
KeyCode::Char('=') => { KeyCode::Char('=') | KeyCode::Char('+') => {
app.conn.inc_volume(2); app.conn.inc_volume(2);
app.conn.update_status(); app.conn.update_status();
} }
@ -121,10 +141,26 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
app.conn.update_status(); app.conn.update_status();
} }
// Toggle Mute
KeyCode::Char('m') => {
match app.conn.volume_status {
VolumeStatus::Muted(v) => {
app.conn.conn.volume(v)?;
app.conn.volume_status = VolumeStatus::Unmuted;
}
VolumeStatus::Unmuted => {
let current_volume = app.conn.status.volume;
app.conn.conn.volume(0)?;
app.conn.volume_status = VolumeStatus::Muted(current_volume);
}
}
app.conn.update_status();
}
// Update MPD database // Update MPD database
KeyCode::Char('U') => { KeyCode::Char('U') => {
app.conn.conn.rescan()?; app.conn.conn.rescan()?;
app.browser.update_directory(&mut app.conn)?; app.should_update_song_list = true;
} }
// Search for songs // Search for songs
@ -137,7 +173,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
} }
// Add or Remove from Current Playlist // Add or Remove from Current Playlist
KeyCode::Char(' ') | KeyCode::Backspace => { KeyCode::Char(' ') => {
app.handle_add_or_remove_from_current_playlist()?; app.handle_add_or_remove_from_current_playlist()?;
} }
_ => {} _ => {}
@ -261,15 +297,19 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
// go to bottom of list // go to bottom of list
KeyCode::Char('G') => app.pl_list.index = app.pl_list.list.len() - 1, KeyCode::Char('G') => app.pl_list.index = app.pl_list.list.len() - 1,
// Change playlist name // Playlist Rename
KeyCode::Char('e') => app.change_playlist_name()?, KeyCode::Char('R') => {
app.inputmode = InputMode::PlaylistRename;
}
// add to current playlist // add to current playlist
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Char(' ') => { KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Char(' ') => {
// app.update_queue(); // app.update_queue();
app.conn if !app.pl_list.list.is_empty() {
.load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; app.conn
app.conn.update_status(); .load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
app.conn.update_status();
}
} }
_ => {} _ => {}
} }
@ -278,3 +318,18 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
} }
Ok(()) Ok(())
} }
pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<()> {
match mouse_event.kind {
MouseEventKind::ScrollUp => app.handle_scroll_up(),
MouseEventKind::ScrollDown => app.handle_scroll_down(),
MouseEventKind::Down(button) => {
let (x, y) = (mouse_event.column, mouse_event.row);
if button == crossterm::event::MouseButton::Left {
app.handle_mouse_left_click(x, y)?;
}
}
_ => {}
}
Ok(())
}

1
src/event/mod.rs → src/event_handler/mod.rs Executable file → Normal file
View File

@ -3,3 +3,4 @@ pub mod handler;
pub mod search_keys; pub mod search_keys;
pub mod pl_rename_keys; pub mod pl_rename_keys;
pub mod pl_append_keys; pub mod pl_append_keys;
pub mod new_pl_keys;

View File

@ -0,0 +1,47 @@
use crate::{
app::{App, AppResult},
ui::InputMode,
};
use crossterm::event::{KeyCode, KeyEvent};
pub fn handle_new_pl_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
match key_event.code {
KeyCode::Esc => {
app.pl_new_pl_input.clear();
app.reset_cursor();
app.inputmode = InputMode::Normal;
}
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Enter => {
let pl_name = &app.pl_new_pl_input;
for song in app.pl_new_pl_songs_buffer.iter() {
app.conn.conn.pl_push(pl_name, song)?;
}
app.pl_new_pl_input.clear();
app.pl_list.list = App::get_playlist(&mut app.conn.conn)?;
app.append_list = App::get_append_list(&mut app.conn.conn)?;
app.reset_cursor();
app.inputmode = InputMode::Normal;
}
KeyCode::Backspace => {
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
}
_ => {}
}
Ok(())
}

View File

@ -0,0 +1,115 @@
use crate::app::{App, AppResult, SelectedTab};
use crate::ui::InputMode;
use crate::utils::FileExtension;
use crossterm::event::{KeyCode, KeyEvent};
use std::path::Path;
pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
match key_event.code {
KeyCode::Char('q') | KeyCode::Esc => {
app.playlist_popup = false;
}
KeyCode::Char('j') | KeyCode::Down => app.append_list.next(),
KeyCode::Char('k') | KeyCode::Up => app.append_list.prev(),
KeyCode::Enter => {
// name of highlighted playlist in append list
let pl_name = &app.append_list.get_item_at_current_index();
match app.selected_tab {
SelectedTab::Queue => {
// Just exit out the menu if no item is selected in the Queue
if app.queue_list.list.is_empty() {
app.playlist_popup = false;
return Ok(());
}
if let Ok(songs) = app.conn.conn.songs(app.queue_list.index as u32) {
let option_song = songs.first();
if let Some(song) = option_song {
if *pl_name == "Current Playlist" {
app.conn.conn.push(song)?;
app.update_queue();
} else if *pl_name == "New Playlist" {
app.pl_new_pl_songs_buffer.clear();
app.pl_new_pl_songs_buffer.push(song.clone());
app.inputmode = InputMode::NewPlaylist;
} else {
app.conn.add_to_playlist(pl_name, song)?;
}
}
}
}
SelectedTab::DirectoryBrowser => {
let (t, f) = app.browser.filetree.get(app.browser.selected).unwrap();
if t == "file" {
let short_path = f;
if let Some(full_path) = app.conn.get_full_path(short_path) {
let song = app.conn.get_song_with_only_filename(full_path);
if *pl_name == "Current Playlist" {
app.conn.conn.push(&song)?;
app.update_queue();
} else if *pl_name == "New Playlist" {
app.pl_new_pl_songs_buffer.clear();
app.pl_new_pl_songs_buffer.push(song.clone());
app.inputmode = InputMode::NewPlaylist;
} else {
app.conn.add_to_playlist(pl_name, &song)?;
}
}
} else if t == "directory" {
let file = format!("{}/{}", app.browser.path, f);
app.pl_new_pl_songs_buffer.clear();
for (t, f) in app.conn.conn.listfiles(&file)?.iter() {
// dir_vec.push((t, f));
if t == "file"
&& Path::new(&f).has_extension(&[
"mp3", "ogg", "flac", "m4a", "wav", "aac", "opus", "ape",
"wma", "mpc", "aiff", "dff", "mp2", "mka",
])
{
let full_path = app.conn.get_full_path(f).unwrap_or_default();
let song = app.conn.get_song_with_only_filename(full_path);
if *pl_name == "Current Playlist" {
app.conn.conn.push(&song)?;
} else if *pl_name == "New Playlist" {
app.pl_new_pl_songs_buffer.push(song.clone());
app.inputmode = InputMode::NewPlaylist;
} else {
app.conn.add_to_playlist(pl_name, &song)?;
}
}
}
}
}
SelectedTab::Playlists => {
let playlist_name = app.pl_list.get_item_at_current_index();
if *pl_name == "Current Playlist" {
app.conn.load_playlist(playlist_name)?;
app.update_queue();
} else if *pl_name == "New Playlist" {
app.inputmode = InputMode::NewPlaylist;
} else {
let songs = app.conn.conn.playlist(playlist_name)?;
for song in songs {
// We ignore the Err() since there could be songs in playlists, which do not exist in the db anymore.
// So instead of panicking, we just ignore if the song does not exists
app.conn.add_to_playlist(pl_name, &song).unwrap_or(());
}
}
}
}
// hide the playlist popup
app.playlist_popup = false;
app.append_list.index = 0;
}
_ => {}
}
Ok(())
}

View File

@ -16,7 +16,7 @@ pub fn handle_pl_rename_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()
} }
KeyCode::Enter => { KeyCode::Enter => {
app.conn.conn.pl_rename( app.conn.conn.pl_rename(
app.pl_list.list.get(app.pl_list.index).unwrap(), app.pl_list.get_item_at_current_index(),
&app.pl_newname_input, &app.pl_newname_input,
)?; )?;
app.pl_list.list = App::get_playlist(&mut app.conn.conn)?; app.pl_list.list = App::get_playlist(&mut app.conn.conn)?;

View File

View File

@ -14,7 +14,10 @@ pub mod list;
pub mod browser; pub mod browser;
/// Event Handler/ keymaps /// Event Handler/ keymaps
pub mod event; pub mod event_handler;
/// Application /// Application
pub mod app; pub mod app;
/// Utilities
pub mod utils;

View File

@ -30,6 +30,11 @@ impl<T> ContentList<T> {
pub fn reset_index(&mut self) { pub fn reset_index(&mut self) {
self.index = 0; self.index = 0;
} }
/// Returns the self.list[self.index] item
pub fn get_item_at_current_index(&mut self) -> &T {
self.list.get(self.index).unwrap()
}
} }
impl<T> Default for ContentList<T> { impl<T> Default for ContentList<T> {

View File

@ -1,15 +1,12 @@
use ratatui::prelude::*;
use rmptui::app::App; use rmptui::app::App;
use rmptui::app::AppResult; use rmptui::app::AppResult;
use rmptui::event::event::Event; use rmptui::event_handler::event::Event;
use rmptui::event::event::EventHandler; use rmptui::event_handler::event::EventHandler;
use rmptui::event::handler; use rmptui::event_handler::handler;
use rmptui::tui; use rmptui::tui;
use std::env; use std::env;
use std::io; use std::io;
use ratatui::prelude::*;
pub type Result<T> = core::result::Result<T, Error>;
pub type Error = Box<dyn std::error::Error>;
fn main() -> AppResult<()> { fn main() -> AppResult<()> {
// Connection // Connection
@ -26,14 +23,19 @@ fn main() -> AppResult<()> {
tui.init()?; tui.init()?;
// initial directory read // initial directory read
app.browser.update_directory(&mut app.conn).unwrap(); app.browser.update_directory(&mut app.conn)?;
// initially set the queue's highlighted item to the current playing item
if let Ok(item) = app.conn.conn.currentsong() {
app.queue_list.index = item.unwrap_or_default().place.unwrap_or_default().pos as usize;
}
while app.running { while app.running {
tui.draw(&mut app)?; tui.draw(&mut app)?;
match tui.events.next()? { match tui.events.next()? {
Event::Tick => app.tick(), Event::Tick => app.tick()?,
Event::Key(key_event) => handler::handle_key_events(key_event, &mut app)?, Event::Key(key_event) => handler::handle_key_events(key_event, &mut app)?,
Event::Mouse(_) => {} Event::Mouse(mouse_event) => handler::handle_mouse_events(mouse_event, &mut app)?,
Event::Resize(_, _) => {} Event::Resize(_, _) => {}
} }
} }

View File

@ -5,7 +5,7 @@ use crossterm::terminal::{self, *};
use std::panic; use std::panic;
use crate::app::{App, AppResult}; use crate::app::{App, AppResult};
use crate::event::event::EventHandler; use crate::event_handler::event::EventHandler;
pub type CrosstermTerminal = ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stderr>>; pub type CrosstermTerminal = ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stderr>>;

119
src/ui.rs
View File

@ -1,6 +1,9 @@
use std::time::Duration; use std::time::Duration;
use crate::app::{App, SelectedTab}; use crate::{
app::{App, SelectedTab},
connection::VolumeStatus,
};
use ratatui::{ use ratatui::{
prelude::*, prelude::*,
widgets::{block::Title, *}, widgets::{block::Title, *},
@ -11,6 +14,7 @@ pub enum InputMode {
Editing, Editing,
Normal, Normal,
PlaylistRename, PlaylistRename,
NewPlaylist,
} }
/// Renders the user interface widgets /// Renders the user interface widgets
@ -37,6 +41,9 @@ pub fn render(app: &mut App, frame: &mut Frame) {
InputMode::PlaylistRename => { InputMode::PlaylistRename => {
draw_rename_playlist(frame, app, layout[1]); draw_rename_playlist(frame, app, layout[1]);
} }
InputMode::NewPlaylist => {
draw_new_playlist(frame, app, layout[1]);
}
} }
if app.playlist_popup { if app.playlist_popup {
@ -76,7 +83,8 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
let mut status: bool = false; let mut status: bool = false;
for sn in app.queue_list.list.iter() { for sn in app.queue_list.list.iter() {
if sn.file.contains(s) { let file = sn.file.split('/').last().unwrap_or_default();
if file.eq(s) {
status = true; status = true;
} }
} }
@ -90,7 +98,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
]); ]);
if status { if status {
row.magenta().bold() row.bold()
} else { } else {
row row
} }
@ -100,7 +108,6 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
} }
}); });
let mut state = TableState::new();
let header = ["Artist", "Track", "Title", "Album", "Time"] let header = ["Artist", "Track", "Title", "Album", "Time"]
.into_iter() .into_iter()
.map(Cell::from) .map(Cell::from)
@ -121,13 +128,18 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
Block::default() Block::default()
.title(format!("Song Browser: {}", app.browser.path.clone()).bold()) .title(format!("Song Browser: {}", app.browser.path.clone()).bold())
.title( .title(
Title::from(format!("Total Songs: {}", total_songs).bold().green()) Title::from(format!("Total Songs: {}", total_songs).green())
.alignment(Alignment::Center), .alignment(Alignment::Center),
) )
.title( .title(match app.conn.volume_status {
Title::from(format!("Volume: {}%", app.conn.volume).bold().green()) VolumeStatus::Unmuted => {
.alignment(Alignment::Right), Title::from(format!("Volume: {}%", app.conn.status.volume).green())
) .alignment(Alignment::Right)
}
VolumeStatus::Muted(_v) => {
Title::from("Muted".red()).alignment(Alignment::Right)
}
})
.borders(Borders::ALL), .borders(Borders::ALL),
) )
.highlight_style( .highlight_style(
@ -139,8 +151,8 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
.header(header) .header(header)
.flex(layout::Flex::Legacy); .flex(layout::Flex::Legacy);
state.select(Some(app.browser.selected)); app.browser_state.select(Some(app.browser.selected));
frame.render_stateful_widget(table, size, &mut state); frame.render_stateful_widget(table, size, &mut app.browser_state);
} }
/// draws playing queue /// draws playing queue
@ -168,17 +180,25 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0))); let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
let row = Row::new(vec![ let row = Row::new(vec![
Cell::from(artist), Cell::from(artist.clone()),
Cell::from(track.green()), Cell::from(track.clone().green()),
Cell::from(title), Cell::from(title.clone()),
Cell::from(album.cyan()), Cell::from(album.clone().cyan()),
Cell::from(time.to_string().magenta()), Cell::from(time.to_string().magenta()),
]); ]);
let place = app.conn.current_song.place; let place = app.conn.current_song.place;
if let Some(pos) = place { if let Some(pos) = place {
if i == pos.pos as usize { if i == pos.pos as usize {
row.magenta().bold() let row = Row::new(vec![
Cell::from(format!("> {}", artist)),
Cell::from(format!(" {}", track).green()),
Cell::from(format!(" {}", title)),
Cell::from(album.cyan()),
Cell::from(time.to_string().magenta()),
]);
row
} else { } else {
row row
} }
@ -187,7 +207,6 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
} }
}); });
let mut state = TableState::new();
let header = ["Artist", "Track", "Title", "Album", "Time"] let header = ["Artist", "Track", "Title", "Album", "Time"]
.into_iter() .into_iter()
.map(Cell::from) .map(Cell::from)
@ -210,10 +229,15 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
.title(Title::from( .title(Title::from(
format!("({} items)", app.queue_list.list.len()).bold(), format!("({} items)", app.queue_list.list.len()).bold(),
)) ))
.title( .title(match app.conn.volume_status {
Title::from(format!("Volume: {}%", app.conn.volume).bold().green()) VolumeStatus::Unmuted => {
.alignment(Alignment::Right), Title::from(format!("Volume: {}%", app.conn.status.volume).green())
) .alignment(Alignment::Right)
}
VolumeStatus::Muted(_v) => {
Title::from("Muted".red()).alignment(Alignment::Right)
}
})
.borders(Borders::ALL), .borders(Borders::ALL),
) )
.highlight_style( .highlight_style(
@ -225,8 +249,8 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
.header(header) .header(header)
.flex(layout::Flex::Legacy); .flex(layout::Flex::Legacy);
state.select(Some(app.queue_list.index)); app.queue_state.select(Some(app.queue_list.index));
frame.render_stateful_widget(table, size, &mut state); frame.render_stateful_widget(table, size, &mut app.queue_state);
} }
// Draw search bar // Draw search bar
@ -264,24 +288,24 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
// Get the current playing state // Get the current playing state
let mut state: String = String::new(); let mut state: String = String::new();
if !app.queue_list.list.is_empty() { if !app.queue_list.list.is_empty() {
state = app.conn.state.clone(); state.clone_from(&app.conn.state);
state.push(':'); state.push(':');
} }
// Get the current modes // Get the current modes
let mut modes_bottom: String = String::new(); let mut modes_bottom: String = String::new();
// we do this to check if at least one mode is enabled so we can push "[]" // we do this to check if at least one mode is enabled so we can push "[]"
if app.conn.repeat | app.conn.random { if app.conn.status.repeat | app.conn.status.random {
modes_bottom.push('r'); modes_bottom.push('r');
} }
if !modes_bottom.is_empty() { if !modes_bottom.is_empty() {
modes_bottom.clear(); modes_bottom.clear();
modes_bottom.push('['); modes_bottom.push('[');
if app.conn.repeat { if app.conn.status.repeat {
modes_bottom.push('r'); modes_bottom.push('r');
} }
if app.conn.random { if app.conn.status.random {
modes_bottom.push('z'); modes_bottom.push('z');
} }
modes_bottom.push(']'); modes_bottom.push(']');
@ -301,7 +325,7 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
// Define the title // Define the title
let title = Block::default() let title = Block::default()
.title(Title::from(state.red().bold())) .title(Title::from(state.red().bold()))
.title(Title::from(song.green().bold())) .title(Title::from(song.green()))
.title(Title::from(duration.cyan().bold()).alignment(Alignment::Right)) .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
.title(Title::from(modes_bottom).position(block::Position::Bottom)) .title(Title::from(modes_bottom).position(block::Position::Bottom))
.borders(Borders::ALL); .borders(Borders::ALL);
@ -327,7 +351,6 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
.split(area); .split(area);
// Draw list of playlists // Draw list of playlists
let mut state = ListState::default();
let title = Block::default().title(Title::from("Playlist".green().bold())); let title = Block::default().title(Title::from("Playlist".green().bold()));
let list = List::new(app.pl_list.list.clone()) let list = List::new(app.pl_list.list.clone())
.block(title.borders(Borders::ALL)) .block(title.borders(Borders::ALL))
@ -339,11 +362,23 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
.add_modifier(Modifier::REVERSED), .add_modifier(Modifier::REVERSED),
) )
.repeat_highlight_symbol(true); .repeat_highlight_symbol(true);
state.select(Some(app.pl_list.index)); app.playlists_state.select(Some(app.pl_list.index));
frame.render_stateful_widget(list, layouts[0], &mut state); frame.render_stateful_widget(list, layouts[0], &mut app.playlists_state);
// Playlist viewer // Playlist viewer
let pl_name = app.pl_list.list.get(app.pl_list.index).unwrap();
// Handle if there are no playlists in the mpd database
if app.pl_list.list.is_empty() {
let title = Block::default()
.title(Title::from("No Playlists Found".red().bold()))
.title_alignment(Alignment::Center)
.borders(Borders::ALL);
frame.render_widget(Clear, area); //this clears out the background
frame.render_widget(title, area);
return;
}
let pl_name = app.pl_list.get_item_at_current_index();
let songs = app.conn.conn.playlist(pl_name).unwrap(); let songs = app.conn.conn.playlist(pl_name).unwrap();
let rows = songs.iter().map(|song| { let rows = songs.iter().map(|song| {
let title = song.clone().title.unwrap_or_default().cyan(); let title = song.clone().title.unwrap_or_default().cyan();
@ -422,6 +457,26 @@ fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
frame.render_widget(input, area); frame.render_widget(input, area);
} }
fn draw_new_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
#[allow(clippy::cast_possible_truncation)]
frame.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
area.x + app.pl_new_pl_cursor_pos as u16 + 2,
// Move one line down, from the border to the input line
area.y + 1,
);
let input = Paragraph::new("/".to_string() + &app.pl_new_pl_input)
.style(Style::default())
.block(
Block::default()
.borders(Borders::ALL)
.title("Enter New Playlist's Name: ".bold().green()),
);
frame.render_widget(input, area);
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([ let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage((100 - percent_y) / 2),

32
src/utils.rs Normal file
View File

@ -0,0 +1,32 @@
use std::process::Command;
use std::ffi::OsStr;
use std::path::Path;
/// Checks if given program is installed in your system
pub fn is_installed(ss: &str) -> bool {
let output = Command::new("which")
.arg(ss)
.output()
.expect("Failed to execute command");
output.status.success()
}
/// Checks if a file has a given extension
// https://stackoverflow.com/questions/72392835/check-if-a-file-is-of-a-given-type
pub trait FileExtension {
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool;
}
impl<P: AsRef<Path>> FileExtension for P {
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool {
if let Some(extension) = self.as_ref().extension().and_then(OsStr::to_str) {
return extensions
.iter()
.any(|x| x.as_ref().eq_ignore_ascii_case(extension));
}
false
}
}