Krolyxon
Commit 7f77734

rmptuisrcmain.rs

167 lines · 5.7 KB · Rust

1use std::{env, io, net::Ipv6Addr, str::FromStr, time::Duration};
2
3use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEventKind};
4use futures_util::StreamExt;
5use ratatui::prelude::*;
6use rmptui::{
7 app::App,
8 config::Config,
9 connection::{MpdClient, MpdSnapshot},
10 event_handler::handler,
11 tui, AppResult,
12};
13use tokio::{sync::mpsc, time};
14
15#[tokio::main]
16async fn main() -> AppResult<()> {
17 let (config, config_path) = Config::load_or_create()?;
18 eprintln!("Using configuration: {}", config_path.display());
19
20 let configured_host = env::var("MPD_HOST").unwrap_or_else(|_| config.mpd.host.clone());
21 let (host, password) = configured_host
22 .split_once('@')
23 .map(|(password, host)| (host, Some(password.to_string())))
24 .unwrap_or_else(|| {
25 (
26 configured_host.as_str(),
27 (!config.mpd.password.is_empty()).then(|| config.mpd.password.clone()),
28 )
29 });
30 if host.is_empty() {
31 return Err(io::Error::new(io::ErrorKind::InvalidInput, "MPD host cannot be empty").into());
32 }
33 let port = env::var("MPD_PORT")
34 .map(|port| {
35 port.parse::<u16>().map_err(|error| {
36 io::Error::new(
37 std::io::ErrorKind::InvalidInput,
38 format!("invalid MPD_PORT value `{port}`: {error}"),
39 )
40 })
41 })
42 .unwrap_or(Ok(config.mpd.port))?;
43 if port == 0 {
44 return Err(io::Error::new(
45 std::io::ErrorKind::InvalidInput,
46 "MPD port must be between 1 and 65535",
47 )
48 .into());
49 }
50 let address = mpd_address(host, port);
51 let mut app = App::new(&address, password.as_deref(), config).await?;
52
53 let backend = CrosstermBackend::new(io::stderr());
54 let terminal = Terminal::new(backend)?;
55 let mut tui = tui::Tui::new(terminal);
56 tui.init()?;
57
58 let (updates_tx, mut updates_rx) = mpsc::channel(16);
59 let watcher_address = address.clone();
60 let watcher_password = password.clone();
61 let watcher =
62 tokio::spawn(async move { watch_mpd(watcher_address, watcher_password, updates_tx).await });
63 let mut terminal_events = EventStream::new();
64 let mut redraw = time::interval(Duration::from_millis(50));
65 redraw.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
66
67 let run_result: AppResult<()> = async {
68 while app.running {
69 tui.draw(&mut app)?;
70 tokio::select! {
71 event = terminal_events.next() => {
72 match event {
73 Some(Ok(CrosstermEvent::Key(key)))
74 if key.kind != KeyEventKind::Release =>
75 {
76 handler::handle_key_events(key, &mut app).await?;
77 }
78 Some(Ok(CrosstermEvent::Key(_))) => {}
79 Some(Ok(CrosstermEvent::Mouse(mouse))) => {
80 handler::handle_mouse_events(mouse, &mut app).await?;
81 }
82 Some(Ok(CrosstermEvent::Resize(_, _)))
83 | Some(Ok(CrosstermEvent::FocusGained))
84 | Some(Ok(CrosstermEvent::FocusLost))
85 | Some(Ok(CrosstermEvent::Paste(_))) => {}
86 Some(Err(error)) => {
87 return Err(Box::new(error) as Box<dyn std::error::Error + Send + Sync>);
88 }
89 None => {
90 return Err(std::io::Error::other("terminal event stream ended").into());
91 }
92 }
93 }
94 _ = redraw.tick() => {}
95 snapshot = updates_rx.recv() => {
96 match snapshot {
97 Some(snapshot) => app.apply_snapshot(snapshot).await?,
98 None => {
99 return Err(std::io::Error::other(
100 "MPD change watcher stopped unexpectedly",
101 )
102 .into());
103 }
104 }
105 }
106 }
107 }
108 Ok(())
109 }
110 .await;
111
112 watcher.abort();
113 let exit_result = tui.exit();
114 run_result?;
115 exit_result?;
116 Ok(())
117}
118
119async fn watch_mpd(
120 address: String,
121 password: Option<String>,
122 updates: mpsc::Sender<MpdSnapshot>,
123) -> AppResult<()> {
124 let mut client = MpdClient::connect(&address, password.as_deref()).await?;
125 loop {
126 client.idle().await?;
127 let snapshot = client.snapshot().await?;
128 if updates.send(snapshot).await.is_err() {
129 return Ok(());
130 }
131 }
132}
133
134fn mpd_address(host: &str, port: u16) -> String {
135 if host.starts_with('[') && host.contains("]:") {
136 host.to_string()
137 } else if let Some(ipv6) = host
138 .strip_prefix('[')
139 .and_then(|host| host.strip_suffix(']'))
140 .filter(|host| Ipv6Addr::from_str(host).is_ok())
141 {
142 format!("[{ipv6}]:{port}")
143 } else if host.contains(':') && Ipv6Addr::from_str(host).is_ok() {
144 format!("[{host}]:{port}")
145 } else if host
146 .rsplit_once(':')
147 .is_some_and(|(_, suffix)| suffix.parse::<u16>().is_ok())
148 {
149 host.to_string()
150 } else {
151 format!("{host}:{port}")
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::mpd_address;
158
159 #[test]
160 fn formats_mpd_host_addresses() {
161 assert_eq!(mpd_address("localhost", 6600), "localhost:6600");
162 assert_eq!(mpd_address("localhost:6601", 6600), "localhost:6601");
163 assert_eq!(mpd_address("[::1]", 6600), "[::1]:6600");
164 assert_eq!(mpd_address("[::1]:6601", 6600), "[::1]:6601");
165 assert_eq!(mpd_address("::1", 6600), "[::1]:6600");
166 }
167}