From e0ee903f1abade965a24b26863970d7fdd987033 Mon Sep 17 00:00:00 2001 From: Mahesh Asolkar Date: Wed, 30 Jul 2025 21:57:27 -0700 Subject: [PATCH] Initial commit * Working UI for minimal MPD client * Header, Now Playing and Playlist panels * ? toggles keyboard shortcut help --- .gitignore | 1 + Cargo.lock | 643 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 + scripts/vscode | 6 + src/main.rs | 359 +++++++++++++++++++++++++++ 5 files changed, 1019 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100755 scripts/vscode create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0561402 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,643 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[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", + "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 = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +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", +] + +[[package]] +name = "mpd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e12cace5746cb0aa78faa6cd2caec9f9c01882fc0e6b54d34685a2d3303ea34" +dependencies = [ + "bufstream", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools", + "lru", + "paste", + "stability", + "strum", + "strum_macros", + "time", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rsmpd" +version = "0.1.0" +dependencies = [ + "anyhow", + "crossterm", + "mpd", + "ratatui", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +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 = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[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-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]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bde7108 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rsmpd" +version = "0.1.0" +edition = "2021" + +[dependencies] +mpd = "*" +crossterm = "0.27.0" +ratatui = { version = "0.27.0", features = ["all-widgets"] } +anyhow = "*" diff --git a/scripts/vscode b/scripts/vscode new file mode 100755 index 0000000..3e853c2 --- /dev/null +++ b/scripts/vscode @@ -0,0 +1,6 @@ +#!/bin/bash + +/opt/vscode/VSCode-linux-x64/code \ + --enable-features=UseOzonePlatform,WaylandWindowDecorations \ + --ozone-platform-hint=auto \ + --unity-launch %F . & disown %1 diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7d9d018 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,359 @@ +// src/main.rs + +use crossterm::{ + event::{self, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use mpd::{Client, Song, State}; +use ratatui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Margin, Alignment}, + style::{Color, Modifier, Style}, + widgets::{Block, Borders, List, ListItem, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, + Frame, Terminal, +}; +use ratatui::backend::CrosstermBackend; +use std::{io, time::Duration}; + +/// Application state +struct App { + client: Client, + current_song: Option, + playlist: Vec, + status: mpd::Status, + selected_index: usize, // Currently selected song in the playlist + scroll_offset: usize, // Offset for playlist scrolling + show_help: bool, // Whether to show the help panel +} + +impl App { + /// Create a new App instance, connecting to MPD + fn new() -> Result { + let mut client = Client::connect("127.0.0.1:6600")?; + let status = client.status()?; + let playlist = client.queue()?; + let current_song = client.currentsong()?; + + Ok(Self { + client, + current_song, + playlist, + status, + selected_index: 0, + scroll_offset: 0, + show_help: false, + }) + } + + /// Refresh the application state from MPD + fn refresh(&mut self, playlist_height: usize) -> Result<(), anyhow::Error> { + self.status = self.client.status()?; + self.current_song = self.client.currentsong()?; + + // Get the initial queue + if let Ok(queue) = self.client.queue() { + // Process the queue with a temporary vector to avoid mutations while iterating + let mut processed: Vec = Vec::with_capacity(queue.len()); + + // Only add songs we haven't seen before + for song in queue { + let duplicate = processed.iter().any(|s: &Song| { + // Two songs are the same if they have the same file path + song.file == s.file + }); + + if !duplicate { + processed.push(song); + } + } + + self.playlist = processed; + } else { + self.playlist.clear(); + } + + // Update scroll position + self.update_scroll(playlist_height); + + Ok(()) + } + + /// Move selection up, adjusting scroll if necessary + fn move_up(&mut self) { + if self.selected_index > 0 { + self.selected_index -= 1; + } + } + + /// Move selection down, adjusting scroll if necessary + fn move_down(&mut self) { + if self.selected_index + 1 < self.playlist.len() { + self.selected_index += 1; + } + } + + /// Update scroll offset to ensure selected item is visible + fn update_scroll(&mut self, height: usize) { + if self.selected_index >= self.scroll_offset + height { + self.scroll_offset = self.selected_index.saturating_sub(height) + 1; + } else if self.selected_index < self.scroll_offset { + self.scroll_offset = self.selected_index; + } + } +} + +fn main() -> anyhow::Result<()> { + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Create app and get initial height + let mut app = App::new()?; + let height = terminal.size()?.height.saturating_sub(12) as usize; + app.refresh(height)?; + + // Run the main loop + run_app(&mut terminal, &mut app)?; + + // Restore terminal + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen,)?; + terminal.show_cursor()?; + + Ok(()) +} + +fn run_app(terminal: &mut Terminal, app: &mut App) -> anyhow::Result<()> { + loop { + // Get the available height for the playlist + let height = terminal.size()?.height.saturating_sub(12) as usize; + app.refresh(height)?; + + terminal.draw(|f| ui(f, app))?; + + if event::poll(Duration::from_millis(250))? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Char('p') => { + if app.status.state == State::Play { + app.client.pause(true)?; + } else { + app.client.play()?; + } + } + KeyCode::Char('s') => app.client.stop()?, + KeyCode::Char('c') => { + app.client.clear()?; + app.client.update()?; + app.client.play()?; + } + // Navigation + KeyCode::Up => app.move_up(), + KeyCode::Down => app.move_down(), + // Play selected song + KeyCode::Enter => { + if !app.playlist.is_empty() { + // Play the selected song by its position in the playlist + app.client.switch(app.selected_index as u32)?; + app.client.play()?; + } + } + // Volume control + KeyCode::Char('+') | KeyCode::Char('=') => { + let new_vol = (app.status.volume + 5).min(100); + app.client.volume(new_vol)?; + } + KeyCode::Char('-') => { + let new_vol = app.status.volume.saturating_sub(5); + app.client.volume(new_vol)?; + } + KeyCode::Char('?') => { + app.show_help = !app.show_help; + } + _ => () + } + } + } + } +} + +/// Helper function to create a centered rect using up certain percentage of the available rect +fn centered_rect(percent_x: u16, percent_y: u16, r: ratatui::layout::Rect) -> ratatui::layout::Rect { + // Calculate the width and height of the popup + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ].as_ref()) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ].as_ref()) + .split(popup_layout[1])[1] +} + +pub(crate) fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Header + Constraint::Length(10), // Status + Constraint::Min(0) // Playlist + ].as_ref()) + .split(f.size()); + + // --- Header Widget --- + let header = Paragraph::new("Sonnet MPD") + .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) + .alignment(ratatui::layout::Alignment::Center) + .block(Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Cyan))); + + f.render_widget(header, chunks[0]); + + // --- Status and Current Song Widget --- + let status_text = format!( + "State: {:?}\nVolume: {}%\nRepeat: {:?}, Random: {:?}", + app.status.state, app.status.volume, app.status.repeat, app.status.random + ); + + let song_title = if let Some(song) = &app.current_song { + song.title.as_deref().unwrap_or("Unknown Title") + } else { + "None" + }; + + let status_block = Block::default().title("Status").borders(Borders::ALL); + let status_paragraph = Paragraph::new(status_text).block(status_block); + f.render_widget(status_paragraph, chunks[1]); + + let current_song_block = Block::default() + .title("Now Playing") + .borders(Borders::ALL); + let song_paragraph = Paragraph::new(song_title) + .style(Style::default().fg(Color::Yellow)) + .block(current_song_block); + + // We can render one widget on top of another by rendering it on the same area + f.render_widget(song_paragraph, chunks[1]); + + + // --- Playlist Widget --- + // Get visible portion of the playlist + let visible_height = chunks[2].height.saturating_sub(2) as usize; + + let playlist_items: Vec = app + .playlist + .iter() + .enumerate() + .skip(app.scroll_offset) + .take(visible_height) + .map(|(i, song)| { + let title = song.title.as_deref().unwrap_or("Unknown"); + let artist = song.tags.iter() + .find(|(key, _)| key == "Artist") + .map(|(_, val)| val.as_str()) + .unwrap_or("Unknown"); + let album = song.tags.iter() + .find(|(key, _)| key == "Album") + .map(|(_, val)| val.as_str()) + .unwrap_or("Unknown"); + + let line = format!("{}. {} - {} ({})", i + 1, title, artist, album); + let mut list_item = ListItem::new(line); + + // Style for the selected item + if i == app.selected_index { + list_item = list_item.style(Style::default().add_modifier(Modifier::REVERSED)); + } + + // Additional highlight for the currently playing song + if let Some(current) = &app.current_song { + if current.file == song.file { + list_item = list_item.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); + } + } + list_item + }) + .collect(); + + let playlist_list = List::new(playlist_items) + .block(Block::default().title(format!("Playlist ({} songs)", app.playlist.len())).borders(Borders::ALL)) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol(">> "); + + f.render_widget(playlist_list.clone(), chunks[2]); + + // Add a scrollbar if there are more items than visible space + if app.playlist.len() > visible_height { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .track_style(Style::default().fg(Color::DarkGray)) + .thumb_style(Style::default().fg(Color::White)); + + let scrollbar_area = chunks[2].inner(Margin { + vertical: 1, + horizontal: 0 + }); + + // Calculate maximum scroll position (total items - visible items) + let max_scroll = app.playlist.len().saturating_sub(visible_height); + + // Normalize scroll position to be between 0 and max_scroll + let scroll_pos = app.scroll_offset.min(max_scroll); + + let mut scrollbar_state = ScrollbarState::default() + .content_length(max_scroll + 1) // +1 because we need to include the last position + .position(scroll_pos); + + f.render_stateful_widget( + scrollbar, + scrollbar_area, + &mut scrollbar_state + ); + } + + // Show help panel if enabled + if app.show_help { + let help_text = vec![ + "Keyboard Shortcuts", + "─────────────────", + "", + "q Quit", + "p Play/Pause", + "s Stop", + "c Clear playlist", + "↑ Move selection up", + "↓ Move selection down", + "⏎ Play selected song", + "+/= Volume up", + "- Volume down", + "? Toggle this help panel", + ].join("\n"); + + let area = centered_rect(50, 60, f.size()); + + let help_paragraph = Paragraph::new(help_text) + .block(Block::default() + .title("Help") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Cyan))) + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Left); + + f.render_widget(ratatui::widgets::Clear, area); // Clear the background + f.render_widget(help_paragraph, area); + } +}