Krolyxon
Commit c847cda

rmptuisrcmain.rs

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