Initial commit

* Working UI for minimal MPD client
* Header, Now Playing and Playlist panels
* ? toggles keyboard shortcut help
This commit is contained in:
2025-07-30 21:57:27 -07:00
commit e0ee903f1a
5 changed files with 1019 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

643
Cargo.lock generated Normal file
View File

@@ -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"

10
Cargo.toml Normal file
View File

@@ -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 = "*"

6
scripts/vscode Executable file
View File

@@ -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

359
src/main.rs Normal file
View File

@@ -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<Song>,
playlist: Vec<Song>,
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<Self, anyhow::Error> {
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<Song> = 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<B: Backend>(terminal: &mut Terminal<B>, 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<ListItem> = 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);
}
}