Compare commits

...

75 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
krolxon 3d569cbf79 fix dmenu_play adding song even when exit without seleciton 2024-05-12 17:30:55 +05:30
krolxon 0ded1507aa update status when switching songs 2024-05-12 17:21:51 +05:30
krolxon 520dad7c9a fix highlight delay when swapping songs 2024-05-12 17:19:55 +05:30
krolxon f3fb60a1a0 fix highlight delay when song deleted from queue 2024-05-12 17:17:57 +05:30
krolxon 6338417002 fix #1 2024-05-12 12:35:50 +05:30
krolxon 6af7553fc8 highlight current playing songs in lists 2024-05-10 22:50:56 +05:30
krolxon 0c8cfe3a9c add stats to Connection struct 2024-05-10 22:50:44 +05:30
krolxon 8a5c7877bd fix deleting from queue when only two elements
if you have only two songs in the queue right now, and you try to delete
the second song, it deletes the first song instead.
2024-05-09 15:21:24 +05:30
krolxon 83c86d3c2d fix keys being registerd in editing modes 2024-05-09 12:22:27 +05:30
krolxon 1187e4f0b5 remove cli mod, dependent methods 2024-05-09 00:30:46 +05:30
krolxon cef5f73478 general and tab specific keys seperated 2024-05-07 22:56:32 +05:30
krolxon c15bb2275f remove highlight symbols 2024-05-06 11:21:23 +05:30
krolxon b04423d18d add swapping keys 2024-05-02 14:44:03 +05:30
krolxon b7fc6bc1a7 move the keymaps to thier individual files 2024-05-01 23:45:05 +05:30
krolxon 955532893f remove queue struct, and use generics of ContentList 2024-05-01 12:08:56 +05:30
25 changed files with 1152 additions and 1074 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

217
Cargo.lock generated
View File

@ -20,54 +20,6 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.2.0" version = "1.2.0"
@ -113,52 +65,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "compact_str" name = "compact_str"
version = "0.7.1" version = "0.7.1"
@ -219,12 +125,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "indoc" name = "indoc"
version = "2.0.5" version = "2.0.5"
@ -286,7 +186,7 @@ dependencies = [
"libc", "libc",
"log", "log",
"wasi", "wasi",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@ -324,7 +224,7 @@ dependencies = [
"libc", "libc",
"redox_syscall", "redox_syscall",
"smallvec", "smallvec",
"windows-targets 0.48.5", "windows-targets",
] ]
[[package]] [[package]]
@ -382,14 +282,12 @@ dependencies = [
[[package]] [[package]]
name = "rmptui" name = "rmptui"
version = "0.1.0" version = "0.1.6"
dependencies = [ dependencies = [
"clap",
"crossterm", "crossterm",
"mpd", "mpd",
"ratatui", "ratatui",
"rust-fuzzy-search", "rust-fuzzy-search",
"rust_fzf",
"simple-dmenu", "simple-dmenu",
] ]
@ -399,12 +297,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2"
[[package]]
name = "rust_fzf"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a94b9c6a9880356cc6de038a52397509f8ac86a18c30d76da904b08fec2cb1"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.15" version = "1.0.15"
@ -481,12 +373,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.26.2" version = "0.26.2"
@ -502,7 +388,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [ dependencies = [
"heck 0.4.1", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
@ -538,12 +424,6 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -584,16 +464,7 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [ dependencies = [
"windows-targets 0.48.5", "windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
] ]
[[package]] [[package]]
@ -602,29 +473,13 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.48.5", "windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.48.5", "windows_aarch64_msvc",
"windows_i686_gnu 0.48.5", "windows_i686_gnu",
"windows_i686_msvc 0.48.5", "windows_i686_msvc",
"windows_x86_64_gnu 0.48.5", "windows_x86_64_gnu",
"windows_x86_64_gnullvm 0.48.5", "windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.48.5", "windows_x86_64_msvc",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
] ]
[[package]] [[package]]
@ -633,90 +488,42 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.32" version = "0.7.32"

View File

@ -1,15 +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"
clap = { version = "4.0.29", features = ["derive"] }
rust_fzf = "0.3.1"
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

72
README.md Executable file → Normal file
View File

@ -1,36 +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 |
- `l` OR `Right` add song to playlist or go inside the directory | `D` | Get dmenu prompt |
- `h` OR `Left` to go back to previous directory | `j`/`Down` | Scroll down |
- `Tab` to cycle through tabs | `k`/`Up` | Scroll up |
- `1` to go to queue | `J` | Swap highlighted song with next one |
- `2` to go to directory browser | `K` | Swap highlighted song with previous one |
- `3` to go to playlists view | `l`/`Right` | Add song to playlist or go inside the directory |
- `Enter` OR `l` OR `Right` to add song/playlist to current playlist | `h`/`Left` | Go back to previous directory |
- `a` to append the song to current playing queue | `Tab` | Cycle through tabs |
- `Space`/`BackSpace` to delete the highlighted song from queue | `1` | Go to queue |
- `f` to go forwards | `2` | Go to directory browser |
- `b` to go backwards | `3` | Go to playlists view |
- `>` to play next song from queue | `Enter`/`l`/`Right` | Add song/playlist to current playlist |
- `<` to play previous song from queue | `a` | Append the song to current playing queue |
- `U` to update the MPD database | `Space` | Delete the highlighted song from queue |
- `r` to toggle repeat | `f` | Go forwards |
- `z` to toggle random | `b` | Go backwards |
- `/` to search | `>` | Play next song from queue |
- `g` to go to top of list | `<` | Play previous song from queue |
- `G` to go to bottom of list | `U` | Update the MPD database |
| `r` | Toggle repeat |
| `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
@ -41,3 +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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -1,11 +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::queue::Queue;
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>>;
@ -13,11 +14,10 @@ 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 {
/// check if app is running pub running: bool, // Check if app is running
pub running: bool, pub conn: Connection, // Connection
pub conn: Connection,
pub browser: FileBrowser, // Directory browser pub browser: FileBrowser, // Directory browser
pub queue_list: Queue, // 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
@ -25,13 +25,26 @@ pub struct App {
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)]
@ -43,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 = Queue::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,
@ -66,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) {
@ -80,19 +124,7 @@ 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.title.unwrap());
vec.push(x); vec.push(x);
}); });
} }
@ -111,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());
} }
@ -122,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();
if content_type == "directory" {
let file = format!("{}/{}", self.browser.path, content);
let songs = self.conn.conn.listfiles(&file).unwrap_or_default();
for (t, f) in songs.iter() {
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; let mut status = false;
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) { let song_path = song.file.split('/').last().unwrap_or_default();
if song_path.eq(content) {
self.conn.conn.delete(i as u32).unwrap(); self.conn.conn.delete(i as u32).unwrap();
status = true; 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;
} }
@ -157,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
@ -172,6 +232,7 @@ impl App {
} }
self.update_queue(); self.update_queue();
self.conn.update_status();
Ok(()) Ok(())
} }
@ -190,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
filename.remove(0);
filename.remove(0);
let song = self.conn.get_song_with_only_filename(&filename);
self.conn.push(&song)?; 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();
@ -240,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);
}
_ => {} _ => {}
} }
} }
@ -254,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);
}
_ => {} _ => {}
} }
} }
@ -262,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,
}; };
@ -287,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();
} }
@ -321,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,
} }
@ -334,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;
}
_ => {} _ => {}
} }
} }
@ -351,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('/');
if let Some(r) = r {
self.path = self.path.as_str()[..r].to_string(); self.path = self.path.as_str()[..r].to_string();
self.update_directory(conn)?; 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,43 +0,0 @@
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[clap(version, about, author = "krolyxon")]
/// MPD client made with Rust
pub struct Args {
/// No TUI
#[arg(short= 'n', default_value="false")]
pub tui: bool,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(arg_required_else_help = true, long_flag = "volume" , short_flag = 'v')]
/// Set Volume
Volume {
vol: String,
},
/// Use dmenu for selection
#[command(long_flag = "dmenu" , short_flag = 'd')]
Dmenu,
/// Use Fzf for selection
#[command(long_flag = "fzf" , short_flag = 'f')]
Fzf,
/// Check Status
#[command(long_flag = "status" , short_flag = 's')]
Status,
/// Pause playback
#[command(long_flag = "pause" , short_flag = 'p')]
Pause,
/// Toggle Playback
#[command(long_flag = "toggle" , short_flag = 't')]
Toggle,
}

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,107 +20,99 @@ 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 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 stats = conn.stats().unwrap_or_default();
let repeat = status.repeat;
let random = status.random;
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,
status,
volume_status,
}) })
} }
/// Fzf prompt for selecting song
pub fn play_fzf(&mut self) -> Result<()> {
is_installed("fzf")?;
let ss = &self.songs_filenames;
let fzf_choice = rust_fzf::select(ss.clone(), Vec::new()).unwrap();
let index = get_choice_index(&self.songs_filenames, fzf_choice.first().unwrap());
let song = self.get_song_with_only_filename(ss.get(index).unwrap());
self.push(&song)?;
Ok(())
}
/// 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 = get_choice_index(&self.songs_filenames, &op); let index = ss.iter().position(|s| s == &op);
let song = self.get_song_with_only_filename(ss.get(index).unwrap()); if let Some(i) = index {
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);
// 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 // Current song
self.volume = status.volume as u8;
// Repeat mode
self.repeat = status.repeat;
// Random mode
self.random = status.random;
self.current_song = current_song; self.current_song = current_song;
// Stats
self.stats = stats;
} }
/// Get progress ratio of current playing song /// Get progress ratio of current playing song
@ -134,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();
@ -151,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(())
} }
@ -172,37 +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
// Ok(filename.to_string())
}
/// Print status to stdout
pub fn status(&mut self) {
let current_song = self.conn.currentsong();
let status = self.conn.status().unwrap();
if current_song.is_ok() && status.state != State::Stop {
let song = current_song.unwrap();
if let Some(s) = song {
println!("{} - {}", s.artist.unwrap(), s.title.unwrap());
}
}
println!(
"volume: {}\trepeat: {}\trandom: {}\tsingle: {}\tconsume: {}",
status.volume, status.repeat, status.random, status.single, status.consume
);
} }
/// 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)))
@ -227,60 +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();
} }
} }
} }
/// Gets the index of the string from the Vector
fn get_choice_index(ss: &[String], selection: &str) -> usize {
let mut choice: usize = 0;
if let Some(index) = ss.iter().position(|s| s == selection) {
choice = index;
}
choice
}
/// 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())
}
}

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

View File

@ -0,0 +1,335 @@
use crate::{
app::{App, AppResult, SelectedTab},
connection::VolumeStatus,
ui::InputMode,
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use std::time::Duration;
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<()> {
// searching, playlist renaming, playlist appending
if app.inputmode == InputMode::Editing {
search_keys::handle_search_keys(key_event, app)?;
} else if app.inputmode == InputMode::PlaylistRename {
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 {
pl_append_keys::hande_pl_append_keys(key_event, app)?;
} else {
// General KeyMaps
match key_event.code {
// Quit
KeyCode::Char('q') => app.quit(),
KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL {
app.quit();
} else {
app.conn.conn.clear()?;
app.conn.update_status();
app.queue_list.list.clear();
app.queue_list.reset_index();
}
}
// Playback controls
// Toggle Pause
KeyCode::Char('p') => {
app.conn.toggle_pause();
app.conn.update_status();
}
// Pause
KeyCode::Char('s') => {
app.conn.pause();
app.conn.update_status();
}
// Toggle rpeat
KeyCode::Char('r') => {
app.conn.toggle_repeat();
app.conn.update_status();
}
// Toggle random
KeyCode::Char('z') => {
app.conn.toggle_random();
app.conn.update_status();
}
// Dmenu prompt
KeyCode::Char('D') => {
app.conn.play_dmenu()?;
app.conn.update_status();
}
// add to queue
KeyCode::Char('a') => app.playlist_popup = true,
// Fast forward
KeyCode::Char('f') => {
if !app.queue_list.list.is_empty() {
let status = app.conn.conn.status().unwrap_or_default();
let place = status.song.unwrap_or_default().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
KeyCode::Char('b') => {
if !app.queue_list.list.is_empty() {
let status = app.conn.conn.status().unwrap_or_default();
let place = status.song.unwrap_or_default().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
KeyCode::Tab => {
app.cycle_tabls();
}
// Directory browser tab
KeyCode::Char('1') => {
app.selected_tab = SelectedTab::Queue;
}
// Playing queue tab
KeyCode::Char('2') => {
app.selected_tab = SelectedTab::DirectoryBrowser;
}
// Playlists tab
KeyCode::Char('3') => {
app.selected_tab = SelectedTab::Playlists;
}
// Play next song
KeyCode::Char('>') => {
if !app.queue_list.list.is_empty() {
app.conn.conn.next()?;
app.update_queue();
app.conn.update_status();
}
}
// Play previous song
KeyCode::Char('<') => {
if !app.queue_list.list.is_empty() {
app.conn.conn.prev()?;
app.update_queue();
app.conn.update_status();
}
}
// Volume controls
KeyCode::Char('=') | KeyCode::Char('+') => {
app.conn.inc_volume(2);
app.conn.update_status();
}
KeyCode::Char('-') => {
app.conn.dec_volume(2);
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
KeyCode::Char('U') => {
app.conn.conn.rescan()?;
app.should_update_song_list = true;
}
// Search for songs
KeyCode::Char('/') => {
if app.inputmode == InputMode::Normal {
app.inputmode = InputMode::Editing;
} else {
app.inputmode = InputMode::Normal;
}
}
// Add or Remove from Current Playlist
KeyCode::Char(' ') => {
app.handle_add_or_remove_from_current_playlist()?;
}
_ => {}
}
// Tab specific keymaps
match app.selected_tab {
SelectedTab::Queue => {
match key_event.code {
// Go Up
KeyCode::Char('j') | KeyCode::Down => app.queue_list.next(),
// Go down
KeyCode::Char('k') | KeyCode::Up => app.queue_list.prev(),
// Next directory
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => {
app.conn.conn.switch(app.queue_list.index as u32)?;
app.conn.update_status();
}
// Delete highlighted song from the queue
KeyCode::Char('d') => {
if app.queue_list.index >= app.queue_list.list.len()
&& app.queue_list.index != 0
{
app.queue_list.index -= 1;
}
app.conn.conn.delete(app.queue_list.index as u32)?;
if app.queue_list.index >= app.queue_list.list.len().saturating_sub(1)
&& app.queue_list.index != 0
{
app.queue_list.index -= 1;
}
app.conn.update_status();
app.update_queue();
}
// Swap highlighted song with next one
KeyCode::Char('J') => {
let current: u32 = app.queue_list.index as u32;
let next: u32 = if (current + 1) as usize == app.queue_list.list.len() {
app.queue_list.index as u32
} else {
app.queue_list.index += 1;
current + 1
};
app.conn.conn.swap(current, next)?;
app.update_queue();
app.conn.update_status();
}
// Swap highlighted song with previous one
KeyCode::Char('K') => {
let current: u32 = app.queue_list.index as u32;
let prev: u32 = if current == 0 {
app.queue_list.index as u32
} else {
app.queue_list.index -= 1;
current - 1
};
app.conn.conn.swap(current, prev)?;
app.update_queue();
app.conn.update_status();
}
// go to top of list
KeyCode::Char('g') => app.queue_list.index = 0,
// go to bottom of list
KeyCode::Char('G') => app.queue_list.index = app.queue_list.list.len() - 1,
_ => {}
}
}
SelectedTab::DirectoryBrowser => {
match key_event.code {
// Go Up
KeyCode::Char('j') | KeyCode::Down => app.browser.next(),
// Go down
KeyCode::Char('k') | KeyCode::Up => app.browser.prev(),
// Next directory
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => {
// app.update_queue();
app.handle_enter()?;
app.conn.update_status();
}
// head back to previous directory
KeyCode::Char('h') | KeyCode::Left => {
app.browser.handle_go_back(&mut app.conn)?
}
// go to top of list
KeyCode::Char('g') => app.browser.selected = 0,
// go to bottom of list
KeyCode::Char('G') => app.browser.selected = app.browser.filetree.len() - 1,
_ => {}
}
}
SelectedTab::Playlists => {
match key_event.code {
// Go Up
KeyCode::Char('j') | KeyCode::Down => app.pl_list.next(),
// Go down
KeyCode::Char('k') | KeyCode::Up => app.pl_list.prev(),
// go to top of list
KeyCode::Char('g') => app.pl_list.index = 0,
// go to bottom of list
KeyCode::Char('G') => app.pl_list.index = app.pl_list.list.len() - 1,
// Playlist Rename
KeyCode::Char('R') => {
app.inputmode = InputMode::PlaylistRename;
}
// add to current playlist
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Char(' ') => {
// app.update_queue();
if !app.pl_list.list.is_empty() {
app.conn
.load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
app.conn.update_status();
}
}
_ => {}
}
}
}
}
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(())
}

6
src/event_handler/mod.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod event;
pub mod handler;
pub mod search_keys;
pub mod pl_rename_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

@ -0,0 +1,43 @@
use crate::{
app::{App, AppResult},
ui::InputMode,
};
use crossterm::event::{KeyCode, KeyEvent};
pub fn handle_pl_rename_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
match key_event.code {
KeyCode::Esc => {
app.pl_newname_input.clear();
app.reset_cursor();
app.inputmode = InputMode::Normal;
}
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Enter => {
app.conn.conn.pl_rename(
app.pl_list.get_item_at_current_index(),
&app.pl_newname_input,
)?;
app.pl_list.list = App::get_playlist(&mut app.conn.conn)?;
app.pl_newname_input.clear();
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,110 @@
use crate::{
app::{App, AppResult, SelectedTab},
ui::InputMode,
};
use crossterm::event::{KeyCode, KeyEvent};
use rust_fuzzy_search::{self, fuzzy_search_sorted};
pub fn handle_search_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
match app.selected_tab {
SelectedTab::DirectoryBrowser => {
let list: Vec<&str> = app
.browser
.filetree
.iter()
.map(|(_, f)| f.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
for (i, (_, item)) in app.browser.filetree.iter().enumerate() {
if item.contains(res.first().unwrap()) {
app.browser.selected = i;
}
}
}
SelectedTab::Queue => {
let list: Vec<&str> = app
.queue_list
.list
.iter()
.map(|f| f.file.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
for (i, item) in app.queue_list.list.iter().enumerate() {
if item.file.contains(res.first().unwrap()) {
app.queue_list.index = i;
}
}
}
SelectedTab::Playlists => {
let list: Vec<&str> = app
.pl_list
.list
.iter()
.map(|f| f.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
for (i, item) in app.pl_list.list.iter().enumerate() {
if item.contains(res.first().unwrap()) {
app.pl_list.index = i;
}
}
}
}
// Keybind for searching
//
// Keybinds for when the search prompt is visible
match key_event.code {
KeyCode::Esc => {
app.inputmode = InputMode::Normal;
}
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Enter => {
let list: Vec<&str> = app
.browser
.filetree
.iter()
.map(|(_, f)| f.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let (res, _) = res.first().unwrap();
for (i, (_, item)) in app.browser.filetree.iter().enumerate() {
if item.contains(res) {
app.browser.selected = i;
}
}
app.search_input.clear();
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

@ -1,429 +0,0 @@
use crate::browser::FileExtension;
use crate::{
app::{App, AppResult, SelectedTab},
ui::InputMode,
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use rust_fuzzy_search::{self, fuzzy_search_sorted};
use std::{path::Path, time::Duration};
pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
if app.inputmode == InputMode::Editing {
// Live search update
match app.selected_tab {
SelectedTab::DirectoryBrowser => {
let list: Vec<&str> = app
.browser
.filetree
.iter()
.map(|(_, f)| f.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
for (i, (_, item)) in app.browser.filetree.iter().enumerate() {
if item.contains(res.first().unwrap()) {
app.browser.selected = i;
}
}
}
SelectedTab::Queue => {
let list: Vec<&str> = app
.queue_list
.list
.iter()
.map(|f| f.file.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
for (i, item) in app.queue_list.list.iter().enumerate() {
if item.file.contains(res.first().unwrap()) {
app.queue_list.index = i;
}
}
}
SelectedTab::Playlists => {
let list: Vec<&str> = app
.pl_list
.list
.iter()
.map(|f| f.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let res = res.iter().map(|(x, _)| *x).collect::<Vec<&str>>();
for (i, item) in app.pl_list.list.iter().enumerate() {
if item.contains(res.first().unwrap()) {
app.pl_list.index = i;
}
}
}
}
// Keybind for searching
//
// Keybinds for when the search prompt is visible
match key_event.code {
KeyCode::Esc => {
app.inputmode = InputMode::Normal;
}
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Enter => {
let list: Vec<&str> = app
.browser
.filetree
.iter()
.map(|(_, f)| f.as_str())
.collect::<Vec<&str>>();
let res: Vec<(&str, f32)> = fuzzy_search_sorted(&app.search_input, &list);
let (res, _) = res.first().unwrap();
for (i, (_, item)) in app.browser.filetree.iter().enumerate() {
if item.contains(res) {
app.browser.selected = i;
}
}
app.search_input.clear();
app.reset_cursor();
app.inputmode = InputMode::Normal;
}
KeyCode::Backspace => {
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
}
_ => {}
}
} else if app.inputmode == InputMode::PlaylistRename {
match key_event.code {
KeyCode::Esc => {
app.pl_newname_input.clear();
app.reset_cursor();
app.inputmode = InputMode::Normal;
}
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Enter => {
app.conn.conn.pl_rename(
app.pl_list.list.get(app.pl_list.index).unwrap(),
&app.pl_newname_input,
)?;
app.pl_list.list = App::get_playlist(&mut app.conn.conn)?;
app.pl_newname_input.clear();
app.reset_cursor();
app.inputmode = InputMode::Normal;
}
KeyCode::Backspace => {
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
}
_ => {}
}
// Playlist popup keybinds
//
// Keybind for when the "append to playlist" popup is visible
} else if app.playlist_popup {
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;
}
_ => {}
}
} else {
// Global keymaps
//
// Keymaps related to all the tabs
match key_event.code {
// Quit
KeyCode::Char('q') => app.quit(),
KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL {
app.quit();
} else {
app.conn.conn.clear()?;
app.conn.update_status();
app.queue_list.list.clear();
app.queue_list.reset_index();
}
}
// Go Up
KeyCode::Char('j') | KeyCode::Down => match app.selected_tab {
SelectedTab::DirectoryBrowser => app.browser.next(),
SelectedTab::Queue => app.queue_list.next(),
SelectedTab::Playlists => app.pl_list.next(),
},
// Go down
KeyCode::Char('k') | KeyCode::Up => match app.selected_tab {
SelectedTab::DirectoryBrowser => app.browser.prev(),
SelectedTab::Queue => app.queue_list.prev(),
SelectedTab::Playlists => app.pl_list.prev(),
},
// Next directory
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => {
// app.update_queue();
match app.selected_tab {
SelectedTab::DirectoryBrowser => {
app.handle_enter()?;
}
SelectedTab::Queue => {
app.conn.conn.switch(app.queue_list.index as u32)?;
}
SelectedTab::Playlists => {
app.conn
.load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
}
}
app.conn.update_status();
}
// head back to previous directory
KeyCode::Char('h') | KeyCode::Left => match app.selected_tab {
SelectedTab::DirectoryBrowser => {
app.browser.handle_go_back(&mut app.conn)?;
}
SelectedTab::Queue => {}
SelectedTab::Playlists => {}
},
// Playback controls
// Toggle Pause
KeyCode::Char('p') => app.conn.toggle_pause(),
// Pause
KeyCode::Char('s') => app.conn.pause(),
// Toggle rpeat
KeyCode::Char('r') => {
app.conn.toggle_repeat();
app.conn.update_status();
}
// Toggle random
KeyCode::Char('z') => {
app.conn.toggle_random();
app.conn.update_status();
}
// Dmenu prompt
KeyCode::Char('D') => app.conn.play_dmenu()?,
// add to queue
KeyCode::Char('a') => app.playlist_popup = true,
// Fast forward
KeyCode::Char('f') => {
let place = app.conn.conn.status().unwrap().song.unwrap().pos;
let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
app.conn.conn.seek(place, pos)?;
}
// backward
KeyCode::Char('b') => {
let place = app.conn.conn.status().unwrap().song.unwrap().pos;
let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
app.conn.conn.seek(place, pos)?;
}
// Cycle through tabs
KeyCode::Tab => {
app.cycle_tabls();
}
// Directory browser tab
KeyCode::Char('1') => {
app.selected_tab = SelectedTab::Queue;
}
// Playing queue tab
KeyCode::Char('2') => {
app.selected_tab = SelectedTab::DirectoryBrowser;
}
// Playlists tab
KeyCode::Char('3') => {
app.selected_tab = SelectedTab::Playlists;
}
// Play next song
KeyCode::Char('>') => {
if !app.queue_list.list.is_empty() {
app.conn.conn.next()?;
app.update_queue();
}
}
// Play previous song
KeyCode::Char('<') => {
if !app.queue_list.list.is_empty() {
app.conn.conn.prev()?;
app.update_queue();
}
}
// Volume controls
KeyCode::Char('=') => {
app.conn.inc_volume(2);
app.conn.update_status();
}
KeyCode::Char('-') => {
app.conn.dec_volume(2);
app.conn.update_status();
}
// Delete highlighted song from the queue
KeyCode::Char('d') => {
if app.queue_list.index >= app.queue_list.list.len() - 1
&& app.queue_list.index != 0
{
app.queue_list.index -= 1;
}
app.conn.conn.delete(app.queue_list.index as u32)?;
app.update_queue();
}
// Update MPD database
KeyCode::Char('U') => {
app.conn.conn.rescan()?;
app.browser.update_directory(&mut app.conn)?;
}
// Search for songs
KeyCode::Char('/') => {
if app.inputmode == InputMode::Normal {
app.inputmode = InputMode::Editing;
} else {
app.inputmode = InputMode::Normal;
}
}
// Remove from Current Playlsit
KeyCode::Char(' ') | KeyCode::Backspace => {
app.handle_add_or_remove_from_current_playlist()?;
}
// go to top of list
KeyCode::Char('g') => match app.selected_tab {
SelectedTab::Queue => app.queue_list.index = 0,
SelectedTab::DirectoryBrowser => app.browser.selected = 0,
SelectedTab::Playlists => app.pl_list.index = 0,
},
// go to bottom of list
KeyCode::Char('G') => match app.selected_tab {
SelectedTab::Queue => app.queue_list.index = app.queue_list.list.len() - 1,
SelectedTab::DirectoryBrowser => {
app.browser.selected = app.browser.filetree.len() - 1
}
SelectedTab::Playlists => app.pl_list.index = app.pl_list.list.len() - 1,
},
// Change playlist name
KeyCode::Char('e') => app.change_playlist_name()?,
_ => {}
}
}
Ok(())
}

View File

@ -1,6 +1,3 @@
/// Command line interface (deprecated)
pub mod cli;
/// Handle mpd connection /// Handle mpd connection
pub mod connection; pub mod connection;
@ -16,14 +13,11 @@ pub mod list;
/// File Browser /// File Browser
pub mod browser; pub mod browser;
/// Event Handler /// Event Handler/ keymaps
pub mod event; pub mod event_handler;
/// KeyEvent Handler
pub mod handler;
/// Application /// Application
pub mod app; pub mod app;
/// The Queue structure /// Utilities
pub mod queue; 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,66 +1,41 @@
#![allow(unused_imports)] use ratatui::prelude::*;
use clap::Parser;
use rmptui::app;
use rmptui::app::App; use rmptui::app::App;
use rmptui::app::AppResult; use rmptui::app::AppResult;
use rmptui::cli::Args; use rmptui::event_handler::event::Event;
use rmptui::cli::Command; use rmptui::event_handler::event::EventHandler;
use rmptui::connection::Connection; use rmptui::event_handler::handler;
use rmptui::event::Event;
use rmptui::event::EventHandler;
use rmptui::handler;
use rmptui::tui; use rmptui::tui;
use std::env; use std::env;
use std::io; use std::io;
use crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{
prelude::*,
symbols::border,
widgets::{block::*, *},
};
pub type Result<T> = core::result::Result<T, Error>;
pub type Error = Box<dyn std::error::Error>;
fn main() -> AppResult<()> { fn main() -> AppResult<()> {
let args = Args::parse(); // Connection
let env_host = env::var("MPD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let env_host = env::var("MPD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let env_port = env::var("MPD_PORT").unwrap_or_else(|_| "6600".to_string()); let env_port = env::var("MPD_PORT").unwrap_or_else(|_| "6600".to_string());
let mut app = App::builder(format!("{}:{}", env_host, env_port).as_str())?; let url = format!("{}:{}", env_host, env_port);
let mut app = App::builder(&url)?;
if !args.tui { // UI
handle_tui(&mut app)?;
} else {
match args.command {
Some(Command::Dmenu) => app.conn.play_dmenu()?,
Some(Command::Fzf) => app.conn.play_fzf().unwrap(),
Some(Command::Status) => app.conn.status(),
Some(Command::Pause) => app.conn.pause(),
Some(Command::Toggle) => app.conn.toggle_pause(),
_ => {}
}
}
Ok(())
}
pub fn handle_tui(app: &mut App) -> AppResult<()> {
let backend = CrosstermBackend::new(io::stderr()); let backend = CrosstermBackend::new(io::stderr());
let terminal = Terminal::new(backend)?; let terminal = Terminal::new(backend)?;
let events = EventHandler::new(1000); let events = EventHandler::new(1000);
let mut tui = tui::Tui::new(terminal, events); let mut tui = tui::Tui::new(terminal, events);
tui.init()?; tui.init()?;
// update the directory // 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(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, 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

@ -1,41 +0,0 @@
use mpd::Song;
#[derive(Debug)]
pub struct Queue {
pub list: Vec<Song>,
pub index: usize,
}
impl Queue {
pub fn new() -> Self {
Queue {
list: Vec::new(),
index: 0,
}
}
// Go to next item in list
pub fn next(&mut self) {
let len = self.list.len();
if len != 0 && self.index < len - 1 {
self.index += 1;
}
}
/// Go to previous item in list
pub fn prev(&mut self) {
if self.index != 0 {
self.index -= 1;
}
}
pub fn reset_index(&mut self) {
self.index = 0;
}
}
impl Default for Queue {
fn default() -> Self {
Self::new()
}
}

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::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>>;

139
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 {
@ -46,7 +53,7 @@ pub fn render(app: &mut App, frame: &mut Frame) {
/// Draws the directory /// Draws the directory
fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
let total_songs = app.conn.conn.stats().unwrap().songs.to_string(); let total_songs = app.conn.stats.songs.to_string();
let rows = app.browser.filetree.iter().enumerate().map(|(i, (t, s))| { let rows = app.browser.filetree.iter().enumerate().map(|(i, (t, s))| {
if t == "file" { if t == "file" {
@ -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(
@ -136,17 +148,16 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
.fg(Color::Cyan) .fg(Color::Cyan)
.bg(Color::Black), .bg(Color::Black),
) )
.highlight_symbol(">>")
.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
fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
let rows = app.queue_list.list.iter().map(|song| { let rows = app.queue_list.list.iter().enumerate().map(|(i, song)| {
// metadata // metadata
let title = song.clone().title.unwrap_or_else(|| song.clone().file); let title = song.clone().title.unwrap_or_else(|| song.clone().file);
let artist = song.clone().artist.unwrap_or_default().cyan(); let artist = song.clone().artist.unwrap_or_default().cyan();
@ -169,21 +180,33 @@ 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.clone().cyan()),
Cell::from(time.to_string().magenta()),
]);
let place = app.conn.current_song.place;
if let Some(pos) = place {
if i == pos.pos as usize {
let row = Row::new(vec![
Cell::from(format!("> {}", artist)),
Cell::from(format!(" {}", track).green()),
Cell::from(format!(" {}", title)),
Cell::from(album.cyan()), Cell::from(album.cyan()),
Cell::from(time.to_string().magenta()), Cell::from(time.to_string().magenta()),
]); ]);
if song.file.contains(&app.conn.current_song.file) { row
row.magenta().bold() } else {
row
}
} else { } else {
row row
} }
}); });
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)
@ -206,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(
@ -218,12 +246,11 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
.fg(Color::Cyan) .fg(Color::Cyan)
.bg(Color::Black), .bg(Color::Black),
) )
.highlight_symbol(">>")
.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
@ -261,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(']');
@ -298,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);
@ -324,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))
@ -335,13 +361,24 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
.add_modifier(Modifier::BOLD) .add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED), .add_modifier(Modifier::REVERSED),
) )
.highlight_symbol(">>")
.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();
@ -362,9 +399,9 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
let table = Table::new( let table = Table::new(
rows, rows,
vec![ vec![
Constraint::Min(40), Constraint::Min(48),
Constraint::Percentage(40), Constraint::Percentage(48),
Constraint::Percentage(20), Constraint::Percentage(4),
], ],
) )
.block(title) .block(title)
@ -374,7 +411,6 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
.fg(Color::Cyan) .fg(Color::Cyan)
.bg(Color::Black), .bg(Color::Black),
) )
.highlight_symbol(">>")
.flex(layout::Flex::SpaceBetween); .flex(layout::Flex::SpaceBetween);
frame.render_widget(table, layouts[1]); frame.render_widget(table, layouts[1]);
} }
@ -394,7 +430,6 @@ fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
.add_modifier(Modifier::BOLD) .add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED), .add_modifier(Modifier::REVERSED),
) )
.highlight_symbol(">>")
.repeat_highlight_symbol(true); .repeat_highlight_symbol(true);
state.select(Some(app.append_list.index)); state.select(Some(app.append_list.index));
@ -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
}
}