Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
~erin | 7ccf7f48a9 | |
~erin | 09ccbe9cd4 | |
~erin | ef19940418 | |
~erin | b0b0a4e058 | |
~erin | 373de985f8 | |
~erin | 377ee3b3cd | |
~erin | 98884cac19 | |
~erin | bfbc791a7b | |
~erin | e7fcad9049 | |
~erin | 701345de29 | |
~erin | 70b77cfb72 | |
~erin | 1bfaecb74f | |
~erin | 075c56423f | |
~erin | 8997dd64c1 | |
~erin | 6fa50943a8 | |
~erin | abfccf1a17 | |
~erin | 1d33339439 | |
~erin | 521ca5de0a | |
~erin | 8e26850fb8 |
|
@ -0,0 +1,735 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
|
||||
|
||||
[[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 = "cc"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||
|
||||
[[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.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098"
|
||||
dependencies = [
|
||||
"bitflags 2.0.2",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"is-terminal",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpd"
|
||||
version = "0.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57a20784da57fa01bf7910a5da686d9f39ff37feaa774856b71f050e4331bf82"
|
||||
dependencies = [
|
||||
"bufstream",
|
||||
"rustc-serialize",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
|
||||
|
||||
[[package]]
|
||||
name = "paris"
|
||||
version = "1.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eaf2319cd71dd9ff38c72bebde61b9ea657134abcf26ae4205f54f772a32810"
|
||||
|
||||
[[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.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.36.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.158"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.158"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
|
||||
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.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cassowary",
|
||||
"crossterm",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[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.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[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-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[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.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d020b441f92996c80d94ae9166e8501e59c7bb56121189dc9eab3bd8216966"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenmotif"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"crossterm",
|
||||
"dirs",
|
||||
"mpd",
|
||||
"paris",
|
||||
"serde",
|
||||
"toml",
|
||||
"tui",
|
||||
]
|
13
Cargo.toml
13
Cargo.toml
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "xenmotif"
|
||||
description = "A TUI client for mpd"
|
||||
authors = ["Erin <contact@the-system.eu.org>"]
|
||||
homepage = "https://xenmotif.the-system.eu.org"
|
||||
repository = "https://git.lavender.software/erin/xenmotif"
|
||||
license = "CNPLv7+"
|
||||
license-file = "LICENSE.md"
|
||||
keywords = ["audio", "client", "tui"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
@ -17,13 +17,18 @@ notifications = []
|
|||
dbus = []
|
||||
images = []
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tui = "0.19"
|
||||
crossterm = "0.25"
|
||||
mpd = "*"
|
||||
paris = { version = "1.5", features = ["no_logger", "macros"] }
|
||||
clap = { version = "4.1.11", features = ["derive"] }
|
||||
toml = "0.7.3"
|
||||
serde = { version = "1.0.158", features = ["derive"] }
|
||||
dirs = "5.0"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
#upx --best --lzma target/release/min-sized-rust
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
debug:
|
||||
@cargo run
|
||||
release:
|
||||
@cargo build --release
|
||||
@upx --best --lzma target/release/xenmotif
|
31
README.md
31
README.md
|
@ -6,19 +6,34 @@ A simple `TUI` and commandline client for `mpd`.
|
|||
- Modern features such as album art, scrobbling, notifications, etc.
|
||||
- Simple configuration via a `TOML` file, and commandline options
|
||||
|
||||
## Configuration
|
||||
If a configuration file is not found, `xenmotif` will create a new `config.toml` file in the OS config directory.
|
||||
A custom configuration file can be passed as an argument, and arguments will override options in the config file.
|
||||
|
||||
## Keybindings
|
||||
`p` - Toggle play/pause
|
||||
|
||||
`s` - Stop
|
||||
|
||||
`Tab` - Swap through search sections
|
||||
|
||||
`i` - Insert mode (search)
|
||||
|
||||
`<esc>` - Return to normal mode
|
||||
|
||||
`q` - Quit
|
||||
|
||||
## API (WIP)
|
||||
### Commandline Options
|
||||
`--help` - Print help options
|
||||
`-h/--help` - Print help options
|
||||
|
||||
`-p/--port` - Specify MPD port
|
||||
`--port` - Specify MPD port
|
||||
|
||||
`-h/--host` - Specify MPD host
|
||||
`--host` - Specify MPD host
|
||||
|
||||
`-c/--config` - Use a specific configuration file
|
||||
|
||||
`-v/--version` - Print version information
|
||||
|
||||
`-j/--json` - Output in JSON format
|
||||
`-V/--version` - Print version information
|
||||
|
||||
### Subcommands
|
||||
`stop` - Stop playing
|
||||
|
@ -26,7 +41,3 @@ A simple `TUI` and commandline client for `mpd`.
|
|||
`play` - Start/resume playing
|
||||
|
||||
`pause` - Pause playing
|
||||
|
||||
`queue` - Print off queue
|
||||
|
||||
`info` - Print current song information
|
||||
|
|
563
src/main.rs
563
src/main.rs
|
@ -1,3 +1,562 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
fs, io,
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, BorderType, Borders, Gauge, List, ListItem, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use mpd::{search::{Query, Term}, Song};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use paris::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod structs;
|
||||
use structs::{App, InputMode, StatefulList, Blocks, SongList, Search, ArtistList};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Config {
|
||||
host: Option<String>,
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// The port of the MPD server to connect to
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
|
||||
/// The IP of the MPD server to connect to
|
||||
#[arg(long)]
|
||||
host: Option<String>,
|
||||
|
||||
/// Specify a configuration file
|
||||
#[arg(short, long)]
|
||||
config: Option<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Stop playing
|
||||
Stop,
|
||||
/// Start/resume playing
|
||||
Play,
|
||||
/// Pause playing
|
||||
Pause,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Parse arguments
|
||||
let args = Args::parse();
|
||||
|
||||
let mut default_config = dirs::config_dir().unwrap();
|
||||
default_config.push("xenmotif");
|
||||
default_config.push("config.toml");
|
||||
|
||||
let config_file = match args.config {
|
||||
Some(file) => PathBuf::from(file),
|
||||
None => default_config.clone(),
|
||||
};
|
||||
|
||||
let config_contents = match fs::read_to_string(&config_file) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
error!("Unable to read config file! {}", e);
|
||||
let config = Config {
|
||||
host: Some("127.0.0.1".to_string()),
|
||||
port: Some(6600),
|
||||
};
|
||||
let toml = toml::to_string(&config).unwrap();
|
||||
default_config.pop();
|
||||
fs::create_dir_all(&default_config).unwrap();
|
||||
default_config.push("config.toml");
|
||||
fs::write(&default_config, toml).unwrap();
|
||||
|
||||
info!("Created new config file at {}", &default_config.display());
|
||||
exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
let config: Config = match toml::from_str(&config_contents) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Couldn't parse config file! {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let port = match args.port {
|
||||
Some(p) => p,
|
||||
None => match config.port {
|
||||
Some(h) => h,
|
||||
None => 6600,
|
||||
},
|
||||
};
|
||||
let host = match args.host {
|
||||
Some(h) => h,
|
||||
None => match config.host {
|
||||
Some(h) => h,
|
||||
None => "127.0.0.1".to_string(),
|
||||
},
|
||||
};
|
||||
let mut app = App::new(port, host);
|
||||
|
||||
match args.command {
|
||||
Some(Commands::Stop) => {
|
||||
app.mpd_client.stop().unwrap();
|
||||
exit(0);
|
||||
}
|
||||
Some(Commands::Play) => {
|
||||
app.mpd_client.play().unwrap();
|
||||
exit(0);
|
||||
}
|
||||
Some(Commands::Pause) => {
|
||||
app.mpd_client.pause(true).unwrap();
|
||||
exit(0);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(5);
|
||||
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_millis(1));
|
||||
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('i') => app.input_mode = InputMode::Editing,
|
||||
KeyCode::Char('p') => app.mpd_client.toggle_pause().unwrap(),
|
||||
KeyCode::Char('s') => app.mpd_client.stop().unwrap(),
|
||||
KeyCode::Tab => app.switch_block(),
|
||||
KeyCode::Char('l') => {
|
||||
match app.selected_block {
|
||||
Blocks::Songs => {
|
||||
match app.songs.choose() {
|
||||
Some(s) => {
|
||||
match app.mpd_client.clear() {
|
||||
Ok(..) => {},
|
||||
Err(e) => error!("Could not clear queue: {}", e),
|
||||
}
|
||||
match app.mpd_client.push(s) {
|
||||
Ok(..) => {},
|
||||
Err(e) => error!("Cannot play song! {}", e),
|
||||
}
|
||||
app.mpd_client.play().unwrap();
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
},
|
||||
Blocks::Artists => {
|
||||
// match app.artists.choose() {
|
||||
// Some(s) => {
|
||||
// match app.mpd_client.clear() {
|
||||
// Ok(..) => {},
|
||||
// Err(e) => error!("Could not clear queue: {}", e),
|
||||
// }
|
||||
// match app.mpd_client.push(s) {
|
||||
// Ok(..) => {},
|
||||
// Err(e) => error!("Cannot play song! {}", e),
|
||||
// }
|
||||
// app.mpd_client.play().unwrap();
|
||||
// },
|
||||
// None => {},
|
||||
// }
|
||||
},
|
||||
Blocks::Albums => {
|
||||
// match app.albums.choose() {
|
||||
// Some(s) => {
|
||||
// match app.mpd_client.clear() {
|
||||
// Ok(..) => {},
|
||||
// Err(e) => error!("Could not clear queue: {}", e),
|
||||
// }
|
||||
// match app.mpd_client.push(s) {
|
||||
// Ok(..) => {},
|
||||
// Err(e) => error!("Cannot play song! {}", e),
|
||||
// }
|
||||
// app.mpd_client.play().unwrap();
|
||||
// },
|
||||
// None => {},
|
||||
// }
|
||||
},
|
||||
}
|
||||
},
|
||||
KeyCode::Char('h') => {
|
||||
match app.selected_block {
|
||||
Blocks::Songs => app.songs.unselect(),
|
||||
Blocks::Artists => app.artists.unselect(),
|
||||
Blocks::Albums => app.albums.unselect(),
|
||||
}
|
||||
},
|
||||
KeyCode::Char('j') => {
|
||||
match app.selected_block {
|
||||
Blocks::Songs => app.songs.next(),
|
||||
Blocks::Artists => app.artists.next(),
|
||||
Blocks::Albums => app.albums.next(),
|
||||
}
|
||||
},
|
||||
KeyCode::Char('k') => {
|
||||
match app.selected_block {
|
||||
Blocks::Songs => app.songs.previous(),
|
||||
Blocks::Artists => app.artists.previous(),
|
||||
Blocks::Albums => app.albums.previous(),
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
},
|
||||
InputMode::Editing => {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
let last_results = match app.messages.last() {
|
||||
Some(r) => r,
|
||||
None => "",
|
||||
};
|
||||
app.last_songs = app.songs.mpd_search(&mut app.mpd_client, last_results.to_string(), (0,50));
|
||||
app.last_artists = app.artists.mpd_search(&mut app.mpd_client, last_results.to_string(), (0,50));
|
||||
app.artists = ArtistList::with_items(app.last_artists.clone());
|
||||
app.songs = SongList::with_items(app.last_songs.clone());
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.input.push(c);
|
||||
let results = app.input.clone();
|
||||
app.last_songs = app.songs.mpd_search(&mut app.mpd_client, results.to_string(), (0,50));
|
||||
app.last_artists = app.artists.mpd_search(&mut app.mpd_client, results.to_string(), (0,50));
|
||||
app.artists = ArtistList::with_items(app.last_artists.clone());
|
||||
app.songs = SongList::with_items(app.last_songs.clone());
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.input.pop();
|
||||
let results = app.input.clone();
|
||||
app.last_songs = app.songs.mpd_search(&mut app.mpd_client, results.to_string(), (0,50));
|
||||
app.last_artists = app.artists.mpd_search(&mut app.mpd_client, results.to_string(), (0,50));
|
||||
app.artists = ArtistList::with_items(app.last_artists.clone());
|
||||
app.songs = SongList::with_items(app.last_songs.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
// app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let mpd_status = match app.mpd_client.status() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
let current_song = match app.mpd_client.currentsong() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Couldn't get current song: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
let size = f.size();
|
||||
|
||||
// Surrounding block
|
||||
let title = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Xenmotif")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(title, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(68),
|
||||
Constraint::Percentage(2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
// Top two inner blocks
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||
.split(chunks[0]);
|
||||
// Song info & Search bar
|
||||
let song_search = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(80), Constraint::Percentage(20)].as_ref())
|
||||
.margin(2)
|
||||
.split(top_chunks[1]);
|
||||
|
||||
// Cover art block
|
||||
let image = Block::default()
|
||||
.title(vec![
|
||||
Span::styled("With", Style::default().fg(Color::Yellow)),
|
||||
Span::from(" background"),
|
||||
])
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(image, top_chunks[0]);
|
||||
|
||||
// Song/now playing
|
||||
let mut title = String::new();
|
||||
let song_text = match current_song {
|
||||
Some(song) => {
|
||||
let mut info: Vec<Spans> = vec![];
|
||||
|
||||
title = match song.title {
|
||||
Some(s) => s,
|
||||
None => "<unnamed>".to_string(),
|
||||
};
|
||||
let artist = match song.tags.get("AlbumArtist") {
|
||||
Some(val) => Span::styled(format!("{val}"), Style::default().fg(Color::Green)),
|
||||
None => Span::styled("<unknown artist>", Style::default().fg(Color::Red)),
|
||||
};
|
||||
let album = match song.tags.get("Album") {
|
||||
Some(val) => Span::styled(format!("{val}"), Style::default().fg(Color::Cyan)),
|
||||
None => Span::styled("<unknown album>", Style::default().fg(Color::Red)),
|
||||
};
|
||||
let date = match song.tags.get("Date") {
|
||||
Some(val) => Span::styled(format!("{val}"), Style::default().fg(Color::White)),
|
||||
None => Span::styled("<unknown date>", Style::default().fg(Color::Red)),
|
||||
};
|
||||
|
||||
info.push(Spans::from(vec![
|
||||
artist,
|
||||
Span::raw(" - "),
|
||||
album,
|
||||
Span::raw(" ("),
|
||||
date,
|
||||
Span::raw(")"),
|
||||
]));
|
||||
|
||||
let duration = match song.duration {
|
||||
Some(s) => {
|
||||
let minutes = s.num_minutes();
|
||||
let seconds = s.num_seconds() - minutes * 60;
|
||||
|
||||
let s_seconds;
|
||||
if seconds < 10 {
|
||||
s_seconds = format!("0{}", seconds);
|
||||
} else {
|
||||
s_seconds = format!("{}", seconds);
|
||||
}
|
||||
format!("{}:{}", minutes, s_seconds)
|
||||
}
|
||||
None => "none".to_string(),
|
||||
};
|
||||
let elapsed = match mpd_status.time {
|
||||
Some(t) => {
|
||||
let minutes = t.0.num_minutes();
|
||||
let seconds = t.0.num_seconds() - minutes * 60;
|
||||
|
||||
let s_seconds;
|
||||
if seconds < 10 {
|
||||
s_seconds = format!("0{}", seconds);
|
||||
} else {
|
||||
s_seconds = format!("{}", seconds);
|
||||
}
|
||||
format!("{}:{}", minutes, s_seconds)
|
||||
}
|
||||
None => "none".to_string(),
|
||||
};
|
||||
info.push(Spans::from(Span::styled(
|
||||
format!("({}/{})", elapsed, duration),
|
||||
Style::default(),
|
||||
)));
|
||||
info
|
||||
}
|
||||
None => vec![Spans::from(Span::styled(
|
||||
"Not Playing",
|
||||
Style::default().fg(Color::Red),
|
||||
))],
|
||||
};
|
||||
|
||||
let song = Paragraph::new(song_text)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(vec![
|
||||
Span::styled("──┤ ", Style::default().fg(Color::White)),
|
||||
Span::raw(title),
|
||||
Span::styled(" ├──", Style::default().fg(Color::White)),
|
||||
])
|
||||
.title_alignment(Alignment::Center),
|
||||
)
|
||||
.style(Style::default())
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(song, song_search[0]);
|
||||
|
||||
// Search
|
||||
let input = Paragraph::new(app.input.as_ref())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Search"));
|
||||
f.render_widget(input, song_search[1]);
|
||||
match app.input_mode {
|
||||
InputMode::Normal => {}
|
||||
InputMode::Editing => f.set_cursor(
|
||||
song_search[1].x + app.input.len() as u16 + 1,
|
||||
song_search[1].y + 1,
|
||||
),
|
||||
}
|
||||
|
||||
// Sort & View
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[1]);
|
||||
|
||||
// Artists
|
||||
let artists: Vec<ListItem> = app
|
||||
.artists
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(i.0.clone())];
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect();
|
||||
let artists_block = List::new(artists)
|
||||
.block(Block::default().title("Artists").borders(Borders::ALL)
|
||||
.border_style(match app.selected_block {
|
||||
Blocks::Artists => Style::default().fg(Color::Yellow),
|
||||
_ => Style::default(),
|
||||
}))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.highlight_symbol(">");
|
||||
f.render_stateful_widget(artists_block, bottom_chunks[0], &mut app.artists.state);
|
||||
|
||||
// Albums
|
||||
let albums: Vec<ListItem> = app
|
||||
.albums
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(i.0.clone())];
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect();
|
||||
let albums_block = List::new(albums)
|
||||
.block(Block::default().title("Albums").borders(Borders::ALL)
|
||||
.border_style(match app.selected_block {
|
||||
Blocks::Albums => Style::default().fg(Color::Yellow),
|
||||
_ => Style::default(),
|
||||
}))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.highlight_symbol(">");
|
||||
f.render_stateful_widget(albums_block, bottom_chunks[1], &mut app.albums.state);
|
||||
|
||||
// Songs
|
||||
let songs: Vec<ListItem> = app
|
||||
.songs
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = vec![Spans::from(i.0.clone())];
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect();
|
||||
let songs_block = List::new(songs)
|
||||
.block(Block::default().title("Songs").borders(Borders::ALL)
|
||||
.border_style(match app.selected_block {
|
||||
Blocks::Songs => Style::default().fg(Color::Yellow),
|
||||
_ => Style::default(),
|
||||
}))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.highlight_symbol(">");
|
||||
f.render_stateful_widget(songs_block, bottom_chunks[2], &mut app.songs.state);
|
||||
|
||||
// Timeline
|
||||
let percent = match mpd_status.time {
|
||||
Some(t) => {
|
||||
let played = t.0.num_seconds() as f64;
|
||||
let total = t.1.num_seconds() as f64;
|
||||
let result = played / total;
|
||||
result * 100.0
|
||||
}
|
||||
None => 0.0,
|
||||
};
|
||||
let timeline = Gauge::default()
|
||||
.block(Block::default().borders(Borders::NONE))
|
||||
.gauge_style(Style::default().fg(Color::Red))
|
||||
.percent(percent as u16);
|
||||
f.render_widget(timeline, chunks[2]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,326 @@
|
|||
use mpd::Client;
|
||||
use tui::widgets::ListState;
|
||||
|
||||
use paris::error;
|
||||
use std::process::exit;
|
||||
|
||||
use mpd::{song::Song, Query, Term};
|
||||
|
||||
pub trait Search {
|
||||
type Item;
|
||||
|
||||
fn mpd_search(&self, client: &mut Client, query: String, range: (u32, u32)) -> Vec<Self::Item>;
|
||||
fn fuzzy_search(&self, client: &Client, query: String, range: (u32, u32)) -> Vec<Self::Item>;
|
||||
}
|
||||
|
||||
pub trait StatefulList {
|
||||
type Item;
|
||||
type List;
|
||||
type Selector;
|
||||
|
||||
fn with_items(items: Vec<Self::Item>) -> Self::List;
|
||||
fn next(&mut self);
|
||||
fn previous(&mut self);
|
||||
fn choose(&mut self) -> Option<Self::Selector>;
|
||||
fn unselect(&mut self);
|
||||
}
|
||||
|
||||
pub struct SongList {
|
||||
pub state: ListState,
|
||||
pub items: Vec<(String, usize, Option<Song>)>,
|
||||
}
|
||||
|
||||
impl Search for SongList {
|
||||
type Item = (String, usize, Option<Song>);
|
||||
|
||||
fn mpd_search(&self, client: &mut Client, query: String, range: (u32, u32)) -> Vec<Self::Item> {
|
||||
match client.search(&Query::new().and(Term::File, query), range) {
|
||||
Ok(s) => {
|
||||
let mut results: Vec<(String, usize, Option<Song>)> = vec![];
|
||||
let mut x = 0;
|
||||
for i in s {
|
||||
let name = match i.title.clone() {
|
||||
Some(n) => n,
|
||||
None => "<unnamed>".to_string(),
|
||||
};
|
||||
results.push((name, x, Some(i.clone())));
|
||||
x += 1;
|
||||
}
|
||||
results
|
||||
},
|
||||
Err(e) => vec![("<no results>".to_string(), 0, None)],
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzzy_search(&self, client: &Client, query: String, range: (u32, u32)) -> Vec<Self::Item> {
|
||||
error!("Unimplemented!");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulList for SongList {
|
||||
type Item = (String, usize, Option<Song>);
|
||||
type List = SongList;
|
||||
type Selector = Song;
|
||||
|
||||
fn with_items(items: Vec<(String, usize, Option<Song>)>) -> SongList {
|
||||
SongList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
|
||||
fn choose(&mut self) -> Option<Song> {
|
||||
let selected = match self.state.selected() {
|
||||
Some(i) => i,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let songs = self.items[selected].2.clone();
|
||||
return songs;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArtistList {
|
||||
pub state: ListState,
|
||||
pub items: Vec<(String, usize)>,
|
||||
}
|
||||
|
||||
impl Search for ArtistList {
|
||||
type Item = (String, usize);
|
||||
|
||||
fn mpd_search(&self, client: &mut Client, query: String, range: (u32, u32)) -> Vec<Self::Item> {
|
||||
match client.list(&Term::Tag(std::borrow::Cow::Borrowed("AlbumArtist")), &Query::new().and(Term::Any, query)) {
|
||||
Ok(s) => {
|
||||
let mut results: Vec<(String, usize)> = vec![];
|
||||
let mut x = 0;
|
||||
for i in s {
|
||||
error!("{}", i);
|
||||
results.push((i, x));
|
||||
x += 1;
|
||||
}
|
||||
results
|
||||
},
|
||||
Err(e) => vec![("<no results>".to_string(), 0)],
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzzy_search(&self, client: &Client, query: String, range: (u32, u32)) -> Vec<Self::Item> {
|
||||
error!("Unimplemented!");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulList for ArtistList {
|
||||
type Item = (String, usize);
|
||||
type List = ArtistList;
|
||||
type Selector = String;
|
||||
|
||||
fn with_items(items: Vec<(String, usize)>) -> ArtistList {
|
||||
ArtistList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
|
||||
fn choose(&mut self) -> Option<String> {
|
||||
let selected = match self.state.selected() {
|
||||
Some(i) => i,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let artist = self.items[selected].1.clone();
|
||||
return Some(artist.to_string());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct AlbumList {
|
||||
pub state: ListState,
|
||||
pub items: Vec<(String, usize)>,
|
||||
}
|
||||
|
||||
impl StatefulList for AlbumList {
|
||||
type Item = (String, usize);
|
||||
type List = AlbumList;
|
||||
type Selector = String;
|
||||
|
||||
fn with_items(items: Vec<(String, usize)>) -> AlbumList {
|
||||
AlbumList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
|
||||
fn choose(&mut self) -> Option<String> {
|
||||
let selected = match self.state.selected() {
|
||||
Some(i) => i,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let album = self.items[selected].0.clone();
|
||||
return Some(album);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
|
||||
/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
|
||||
/// and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
pub struct App {
|
||||
pub songs: SongList,
|
||||
pub last_songs: Vec<(String, usize, Option<Song>)>,
|
||||
pub artists: ArtistList,
|
||||
pub last_artists: Vec<(String, usize)>,
|
||||
pub albums: AlbumList,
|
||||
pub last_albums: Vec<(String, usize)>,
|
||||
pub mpd_client: Client,
|
||||
pub input: String,
|
||||
pub input_mode: InputMode,
|
||||
pub selected_block: Blocks,
|
||||
pub messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn switch_block(&mut self) {
|
||||
let current_block = &self.selected_block;
|
||||
self.selected_block = match current_block {
|
||||
Blocks::Artists => Blocks::Albums,
|
||||
Blocks::Albums => Blocks::Songs,
|
||||
Blocks::Songs => Blocks::Artists,
|
||||
};
|
||||
}
|
||||
pub fn new(port: u16, host: String) -> App {
|
||||
let client = match Client::connect(format!("{}:{}", host, port)) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
error!("Could not connect to MPD daemon: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
App {
|
||||
songs: SongList::with_items(vec![]),
|
||||
last_songs: vec![],
|
||||
artists: ArtistList::with_items(vec![]),
|
||||
last_artists: vec![],
|
||||
albums: AlbumList::with_items(vec![]),
|
||||
last_albums: vec![],
|
||||
mpd_client: client,
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
selected_block: Blocks::Songs,
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum InputMode {
|
||||
Normal,
|
||||
Editing,
|
||||
}
|
||||
|
||||
pub enum Blocks {
|
||||
Artists,
|
||||
Albums,
|
||||
Songs,
|
||||
}
|
Loading…
Reference in New Issue