diff --git a/Cargo.lock b/Cargo.lock index ba402c3..161af92 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,24 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" version = "0.6.13" @@ -37,7 +55,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -47,15 +65,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "bufstream" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.4" @@ -84,7 +141,7 @@ version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -102,12 +159,136 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mpd" version = "0.1.0" @@ -117,6 +298,41 @@ dependencies = [ "bufstream", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "proc-macro2" version = "1.0.81" @@ -135,12 +351,43 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +dependencies = [ + "bitflags 2.5.0", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "rmptui" version = "0.1.0" dependencies = [ "clap", + "crossterm", "mpd", + "ratatui", "rust_fzf", "simple-dmenu", ] @@ -151,18 +398,110 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a94b9c6a9880356cc6de038a52397509f8ac86a18c30d76da904b08fec2cb1" +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simple-dmenu" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f392360283341235b298e79f0360bd924db6e6ad1e0964cbf4c43ea79c237eb" +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.60" @@ -180,19 +519,89 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -201,28 +610,46 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -235,26 +662,70 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +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]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +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]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +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]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index b1aeb39..1127b66 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ mpd = "0.1.0" clap = { version = "4.0.29", features = ["derive"] } rust_fzf = "0.3.1" simple-dmenu = "0.1.0" +ratatui = "0.26.2" +crossterm = "0.27.0" diff --git a/src/app.rs b/src/app.rs new file mode 100755 index 0000000..d57c0b6 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,61 @@ +use crate::connection::Connection; +use crate::list::ContentList; +use std::collections::VecDeque; + +// Application result type +pub type AppResult = std::result::Result>; + +/// Application +#[derive(Debug)] +pub struct App { + /// check if app is running + pub running: bool, + pub conn: Connection, + pub play_deque: VecDeque, + pub list: ContentList, +} + +impl App { + pub fn new(addrs: &str) -> Self { + let mut conn = Connection::new(addrs).unwrap(); + let mut vec: VecDeque = VecDeque::new(); + Self::get_queue(&mut conn, &mut vec); + Self { + running: true, + conn, + play_deque: vec, + list: ContentList::new(), + } + } + + pub fn tick(&self) {} + + pub fn quit(&mut self) { + self.running = false; + } + + pub fn get_queue(conn: &mut Connection, vec: &mut VecDeque) { + conn.conn.queue().unwrap().into_iter().for_each(|x| { + if let Some(title) = x.title { + if let Some(artist) = x.artist { + vec.push_back(format!("{} - {}", artist, title)); + } else { + vec.push_back(title) + } + } else { + vec.push_back(x.file) + } + }); + } + + pub fn update_queue(&mut self) { + self.play_deque.clear(); + Self::get_queue(&mut self.conn, &mut self.play_deque); + } +} + +fn to_vecdeque(filenames: &Vec) -> VecDeque { + let mut v: VecDeque = VecDeque::new(); + v = filenames.iter().map(|x| x.to_string()).collect(); + v +} diff --git a/src/cli.rs b/src/cli.rs index f1d648c..131f746 100755 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,28 +1,12 @@ use clap::{Parser, Subcommand}; + #[derive(Parser, Debug)] -#[command(version, about)] -#[clap(author = "krolyxon")] +#[clap(version, about, author = "krolyxon")] /// MPD client made with Rust pub struct Args { - /// pause - #[clap(short, long, default_value = "false")] - pub pause: bool, - - /// toggle pause - #[arg(short, long, default_value = "false")] - pub toggle_pause: bool, - - /// show current status - #[arg(short, long, default_value = "false")] - pub show_status: bool, - - /// use fzf selector for selecting songs - #[arg(short, long, default_value = "false")] - pub fzf_select: bool, - - /// use dmenu selector for selecting songss - #[arg(short, long, default_value = "false")] - pub dmenu_select: bool, + /// No TUI + #[arg(short= 'n', default_value="false")] + pub no_tui: bool, #[command(subcommand)] pub command: Command, @@ -30,14 +14,30 @@ pub struct Args { #[derive(Debug, Subcommand)] pub enum Command { - #[command(arg_required_else_help = true)] + #[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, + } diff --git a/src/connection.rs b/src/connection.rs index 81fb176..f2499e6 100755 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,15 +1,20 @@ use mpd::song::Song; use mpd::{Client, State}; use simple_dmenu::dmenu; +use std::process::Command; +pub type Result = core::result::Result; +pub type Error = Box; + +#[derive(Debug)] pub struct Connection { pub conn: Client, pub songs_filenames: Vec, } impl Connection { - pub fn new(addrs: &str) -> Result { - let mut conn = Client::connect(addrs)?; + pub fn new(addrs: &str) -> Result { + let mut conn = Client::connect(addrs).unwrap(); let songs_filenames: Vec = conn .listall() .unwrap() @@ -23,23 +28,29 @@ impl Connection { }) } - pub fn play_fzf(&mut self) { + pub fn play_fzf(&mut self) -> Result<()> { + is_installed("fzf").map_err(|ex| ex)?; + 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.get(0).unwrap()); let song = self.get_song_with_only_filename(ss.get(index).unwrap()); - self.push(&song); + self.push(&song)?; + + Ok(()) } - pub fn play_dmenu(&mut self) { + pub fn play_dmenu(&mut self) -> Result<()> { + is_installed("dmenu").map_err(|ex| ex)?; let ss: Vec<&str> = self.songs_filenames.iter().map(|x| x.as_str()).collect(); let op = dmenu!(iter &ss; args "-l", "30"); let index = get_choice_index(&self.songs_filenames, &op); let song = self.get_song_with_only_filename(ss.get(index).unwrap()); - self.push(&song); + self.push(&song)?; + Ok(()) } - fn push(&mut self, song: &Song) { + pub fn push(&mut self, song: &Song) -> Result<()> { if self.conn.queue().unwrap().is_empty() { self.conn.push(song).unwrap(); self.conn.play().unwrap(); @@ -50,9 +61,11 @@ impl Connection { } self.conn.next().unwrap(); } + + Ok(()) } - fn get_song_with_only_filename(&self, filename: &str) -> Song { + pub fn get_song_with_only_filename(&self, filename: &str) -> Song { Song { file: filename.to_string(), artist: None, @@ -66,6 +79,10 @@ impl Connection { } } + pub fn get_current_song(&mut self) -> Option { + self.conn.currentsong().unwrap().unwrap_or_default().title + + } pub fn status(&mut self) { let current_song = self.conn.currentsong(); let status = self.conn.status().unwrap(); @@ -112,3 +129,16 @@ fn get_choice_index(ss: &Vec, selection: &str) -> usize { choice } + +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()) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100755 index 0000000..1e5b119 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub enum Error { + DmenuNotInstalled, + FzfNotInstalled, +} + diff --git a/src/event.rs b/src/event.rs new file mode 100755 index 0000000..439c31b --- /dev/null +++ b/src/event.rs @@ -0,0 +1,79 @@ +use crate::app::AppResult; +use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +/// Terminal events. +#[derive(Clone, Copy, Debug)] +pub enum Event { + /// Terminal tick. + Tick, + /// Key press. + Key(KeyEvent), + /// Mouse click/scroll. + Mouse(MouseEvent), + /// Terminal resize. + Resize(u16, u16), +} + +/// Terminal event handler. +#[allow(dead_code)] +#[derive(Debug)] +pub struct EventHandler { + /// Event sender channel. + sender: mpsc::Sender, + /// Event receiver channel. + receiver: mpsc::Receiver, + /// Event handler thread. + handler: thread::JoinHandle<()>, +} + +impl EventHandler { + /// Constructs a new instance of [`EventHandler`]. + pub fn new(tick_rate: u64) -> Self { + let tick_rate = Duration::from_millis(tick_rate); + let (sender, receiver) = mpsc::channel(); + let handler = { + let sender = sender.clone(); + thread::spawn(move || { + let mut last_tick = Instant::now(); + loop { + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or(tick_rate); + + if event::poll(timeout).expect("failed to poll new events") { + match event::read().expect("unable to read event") { + CrosstermEvent::Key(e) => sender.send(Event::Key(e)), + CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), + CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), + CrosstermEvent::FocusGained => Ok(()), + CrosstermEvent::FocusLost => Ok(()), + CrosstermEvent::Paste(_) => unimplemented!(), + } + .expect("failed to send terminal event") + } + + if last_tick.elapsed() >= tick_rate { + sender.send(Event::Tick).expect("failed to send tick event"); + last_tick = Instant::now(); + } + } + }) + }; + Self { + sender, + receiver, + handler, + } + } + + /// Receive the next event from the handler thread. + /// + /// This function will always block the current thread if + /// there is no data available and it's possible for more data to be sent. + pub fn next(&self) -> AppResult { + Ok(self.receiver.recv()?) + } +} diff --git a/src/handler.rs b/src/handler.rs new file mode 100755 index 0000000..32b4b6f --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,47 @@ +use crate::app::{App, AppResult}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { + match key_event.code { + KeyCode::Char('q') | KeyCode::Esc => app.quit(), + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.quit(); + } + } + + KeyCode::Char('j') => { + app.list.next(); + } + + KeyCode::Char('k') => { + app.list.prev(); + } + + KeyCode::Enter | KeyCode::Char('l') => { + let song = app.conn.get_song_with_only_filename(app.conn.songs_filenames.get(app.list.index).unwrap()); + app.conn.push(&song).unwrap(); + app.update_queue(); + } + + // Playback controls + // Toggle Pause + KeyCode::Char('p') => { + app.conn.toggle_pause(); + } + + // Pause + KeyCode::Char('s') => { + app.conn.pause(); + } + + // Clearn Queue + KeyCode::Char('x') => { + app.conn.conn.clear()?; + app.update_queue(); + } + _ => {} + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100755 index 0000000..60df19e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,23 @@ +/// Command line interface (deprecated) +pub mod cli; + +/// Handle mpd connection +pub mod connection; + +/// Widget renderer +pub mod ui; + +/// Terminal user Interface +pub mod tui; + +/// Content list +pub mod list; + +/// Event Handler +pub mod event; + +/// KeyEvent Handler +pub mod handler; + +/// Application +pub mod app; diff --git a/src/list.rs b/src/list.rs new file mode 100755 index 0000000..669d1af --- /dev/null +++ b/src/list.rs @@ -0,0 +1,24 @@ +#[derive(Debug)] +pub struct ContentList { + pub index: usize +} + +impl ContentList { + pub fn new() -> Self { + ContentList { + index: 0 + } + } + + // Go to next item in list + pub fn next(&mut self) { + self.index += 1; + } + + /// Go to previous item in list + pub fn prev(&mut self) { + if self.index != 0 { + self.index -= 1; + } + } +} diff --git a/src/main.rs b/src/main.rs index 09c7363..3fbfc14 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,63 @@ -mod cli; -mod connection; +#![allow(unused_imports)] use clap::Parser; -use cli::Args; -use cli::Command; -use connection::Connection; +use rmptui::app; +use rmptui::app::App; +use rmptui::app::AppResult; +use rmptui::cli::Args; +use rmptui::cli::Command; +use rmptui::connection::Connection; +use rmptui::event::Event; +use rmptui::event::EventHandler; +use rmptui::handler; +use rmptui::tui; +use std::io; -fn main() -> Result<(), Box> { - let args = Args::parse(); - let mut conn = Connection::new("127.0.0.1:6600")?; +use crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind}; +use ratatui::{ + prelude::*, + symbols::border, + widgets::{block::*, *}, +}; - match args.command { - Command::Volume { vol } => { - conn.set_volume(vol); +pub type Result = core::result::Result; +pub type Error = Box; + +fn main() -> AppResult<()> { + // let args = Args::parse(); + + // if args.no_tui { + // handle_tui()?; + // } else { + // match args.command { + // Command::Volume { vol } => { + // conn.set_volume(vol); + // } + // Command::Dmenu => conn.play_dmenu().unwrap(), + // Command::Fzf => conn.play_fzf().unwrap(), + // Command::Status => conn.status(), + // Command::Pause => conn.pause(), + // Command::Toggle => conn.toggle_pause(), + // }; + // } + + let backend = CrosstermBackend::new(io::stderr()); + let terminal = Terminal::new(backend)?; + let mut app = App::new("127.0.0.1:6600"); + let events = EventHandler::new(250); + + let mut tui = tui::Tui::new(terminal, events); + tui.init()?; + + while app.running { + tui.draw(&mut app)?; + match tui.events.next()? { + Event::Tick => app.tick(), + Event::Key(key_event) => handler::handle_key_events(key_event, &mut app)?, + Event::Mouse(_) => {} + Event::Resize(_, _) => {} } - Command::Dmenu => conn.play_dmenu(), - Command::Fzf => conn.play_fzf(), - Command::Status => conn.status(), - Command::Pause => conn.pause(), - Command::Toggle => conn.toggle_pause(), } + Ok(()) } diff --git a/src/tui.rs b/src/tui.rs new file mode 100755 index 0000000..5c94b3b --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,66 @@ +use crate::connection::Connection; +use crate::ui; +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::{ + terminal::{self, *}, +}; +use ratatui::prelude::*; +use std::io::{self, stdout, Stdout}; +use std::panic; + +use crate::app::{App, AppResult}; +use crate::event::EventHandler; + +pub type CrosstermTerminal = ratatui::Terminal>; + +#[derive(Debug)] +pub struct Tui { + terminal: CrosstermTerminal, + pub events: EventHandler, +} + +impl Tui { + pub fn new(terminal: CrosstermTerminal, events: EventHandler) -> Self { + Self { terminal, events } + } + + pub fn init(&mut self) -> AppResult<()> { + terminal::enable_raw_mode()?; + crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; + + let panic_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic| { + Self::reset().expect("failed to reset the terminal"); + panic_hook(panic); + })); + Ok(()) + } + + /// [`Draw`] the terminal interface by [`rendering`] the widgets. + /// + /// [`Draw`]: ratatui::Terminal::draw + /// [`rendering`]: crate::ui::render + pub fn draw(&mut self, app: &mut App) -> AppResult<()> { + self.terminal.draw(|frame| ui::render(app, frame))?; + Ok(()) + } + + /// Resets the terminal interface. + /// + /// This function is also used for the panic hook to revert + /// the terminal properties if unexpected errors occur. + fn reset() -> AppResult<()> { + terminal::disable_raw_mode()?; + crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; + Ok(()) + } + + /// Exits the terminal interface. + /// + /// It disables the raw mode and reverts back the terminal properties. + pub fn exit(&mut self) -> AppResult<()> { + Self::reset()?; + self.terminal.show_cursor()?; + Ok(()) + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100755 index 0000000..3f2eec8 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,37 @@ +use crate::{app::App, connection::Connection}; +use ratatui::{prelude::*, widgets::*}; + +/// Renders the user interface widgets +pub fn render(app: &mut App, frame: &mut Frame) { + // This is where you add new widgets. + // See the following resources: + // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html + // - https://github.com/ratatui-org/ratatui/tree/master/examples + + // List of songs + let mut state = ListState::default(); + let size = Rect::new(100, 0, frame.size().width, frame.size().height - 3); + let list = List::new(app.conn.songs_filenames.clone()) + .block(Block::default().title("Song List").borders(Borders::ALL)) + .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) + .highlight_symbol(">>") + .repeat_highlight_symbol(true); + + state.select(Some(app.list.index)); + frame.render_stateful_widget(list, size, &mut state); + + // Play Queue + let size = Rect::new(0, 0, 100, frame.size().height - 25); + let list = List::new(app.play_deque.clone()) + .block(Block::default().title("Play Queue").borders(Borders::ALL)) + .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) + .highlight_symbol(">>") + .repeat_highlight_symbol(true); + frame.render_widget(list, size); + + // Status + let size = Rect::new(0, frame.size().height - 3, frame.size().width, 3); + let status = Paragraph::new(app.conn.conn.status().unwrap().volume.to_string()) + .block(Block::default().title("Status").borders(Borders::ALL)); + frame.render_widget(status, size); +}