added informationa and url yank

This commit is contained in:
2026-06-04 14:07:33 +02:00
parent 38b5e0c557
commit 416fcbd57e
3 changed files with 144 additions and 8 deletions
+72 -8
View File
@@ -2,10 +2,10 @@ use super::{App, InputMode};
use crate::radio::station::StationInfo;
use ratatui::{
Frame,
layout::{Constraint, Layout},
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, List, ListItem, Paragraph},
widgets::{Block, Clear, List, ListItem, Paragraph, Wrap},
};
impl App {
@@ -31,6 +31,12 @@ impl App {
frame.render_widget(self.info_panel(), top_right);
frame.render_widget(self.played_songs_list(), bottom_right);
frame.render_widget(Paragraph::new(self.footer_hints()), footer);
if self.input_mode == InputMode::Info {
let popup_area = centered_rect(area, 62, 8);
frame.render_widget(Clear, popup_area);
frame.render_widget(self.station_info_popup(), popup_area);
}
}
fn info_panel(&self) -> Paragraph<'static> {
@@ -153,24 +159,71 @@ impl App {
List::new(items).block(Block::bordered().title("Played Songs"))
}
fn station_info_popup(&self) -> Paragraph<'static> {
let station = self
.station_list_state
.selected()
.and_then(|i| self.filtered_stations.get(i));
let lines = if let Some(s) = station {
let status_line = if let Some(msg) = &self.clipboard_message {
let color = if msg.starts_with("Error") { Color::Red } else { Color::Green };
Line::from(Span::styled(msg.clone(), Style::default().fg(color)))
} else {
Line::from(Span::styled(
"c copy url Esc / Enter / i close",
Style::default().add_modifier(Modifier::DIM),
))
};
vec![
Line::from(Span::styled(
s.name.clone(),
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(Span::styled(s.url.clone(), Style::default().fg(Color::Cyan))),
Line::from(""),
status_line,
]
} else {
vec![Line::from(Span::styled(
"No station selected",
Style::default().add_modifier(Modifier::DIM),
))]
};
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.block(
Block::bordered()
.title("Station Info [i]")
.border_style(Style::default().fg(Color::Cyan)),
)
}
fn footer_hints(&self) -> Line<'static> {
if self.input_mode == InputMode::Search {
Line::from(vec![
match self.input_mode {
InputMode::Search => Line::from(vec![
key_hint(" Esc "), Span::raw(" cancel "),
key_hint(" Enter "), Span::raw(" confirm"),
])
} else {
Line::from(vec![
]),
InputMode::Info => Line::from(vec![
key_hint(" Esc "), Span::raw(" / "),
key_hint(" Enter "), Span::raw(" / "),
key_hint(" i "), Span::raw(" close"),
]),
InputMode::Normal => Line::from(vec![
key_hint(" / "), Span::raw(" search "),
key_hint(" Esc "), Span::raw(" clear "),
key_hint(" j/k "), Span::raw(" navigate "),
key_hint(" ^U/^D "), Span::raw(" half page "),
key_hint(" Enter "), Span::raw(" play "),
key_hint(" s "), Span::raw(" stop "),
key_hint(" i "), Span::raw(" info "),
key_hint(" f "), Span::raw(" favorite "),
key_hint(" Tab "), Span::raw(" all/favs "),
key_hint(" q "), Span::raw(" quit"),
])
]),
}
}
}
@@ -184,3 +237,14 @@ fn key_hint(label: &'static str) -> Span<'static> {
.add_modifier(Modifier::BOLD),
)
}
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
let x = area.x + area.width.saturating_sub(width) / 2;
let y = area.y + area.height.saturating_sub(height) / 2;
Rect {
x,
y,
width: width.min(area.width),
height: height.min(area.height),
}
}
+26
View File
@@ -19,6 +19,7 @@ impl App {
match self.input_mode {
InputMode::Normal => self.handle_normal_key(key_event).await,
InputMode::Search => self.handle_search_key(key_event),
InputMode::Info => self.handle_info_key(key_event),
}
}
@@ -33,6 +34,7 @@ impl App {
self.show_favorites_only = !self.show_favorites_only;
self.update_filter();
}
KeyCode::Char('i') => self.input_mode = InputMode::Info,
KeyCode::Char('f') => self.toggle_favorite(),
KeyCode::Char('s') => self.stop_playback().await,
KeyCode::Esc if !self.search_query.is_empty() => {
@@ -75,6 +77,30 @@ impl App {
}
}
fn handle_info_key(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('i') | KeyCode::Char('q') => {
self.clipboard_message = None;
self.input_mode = InputMode::Normal;
self.needs_clear = true;
}
KeyCode::Char('c') => {
let url = self
.station_list_state
.selected()
.and_then(|i| self.filtered_stations.get(i))
.map(|s| s.url.clone());
if let Some(url) = url {
self.clipboard_message = Some(match self.copy_url_to_clipboard(&url) {
Ok(()) => "Copied!".into(),
Err(e) => format!("Error: {e}"),
});
}
}
_ => {}
}
}
fn handle_search_key(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Char(c) => {
+46
View File
@@ -17,10 +17,12 @@ mod player;
#[cfg(test)]
mod tests;
#[derive(Debug, PartialEq)]
pub enum InputMode {
Normal,
Search,
Info,
}
#[derive(Debug)]
@@ -40,6 +42,9 @@ pub struct App {
spawn_player: Option<player::SpawnFn>,
favorites: HashSet<String>,
show_favorites_only: bool,
clipboard_message: Option<String>,
clipboard_child: Option<std::process::Child>,
needs_clear: bool,
}
impl Default for App {
@@ -64,11 +69,48 @@ impl Default for App {
spawn_player: Some(player::new_mpv_spawn()),
favorites: favorites::load_favorites(),
show_favorites_only: false,
clipboard_message: None,
clipboard_child: None,
needs_clear: false,
}
}
}
impl App {
pub(super) fn copy_url_to_clipboard(&mut self, url: &str) -> Result<(), String> {
use std::io::Write;
use std::process::{Command, Stdio};
if let Some(mut old) = self.clipboard_child.take() {
old.kill().ok();
}
let tools: &[(&str, &[&str])] = &[
("wl-copy", &[]),
("xclip", &["-selection", "clipboard"]),
("xsel", &["--clipboard", "--input"]),
];
for (cmd, args) in tools {
let Ok(mut child) = Command::new(cmd)
.args(*args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
else {
continue;
};
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(url.as_bytes()).ok();
}
self.clipboard_child = Some(child);
return Ok(());
}
Err("No clipboard tool found — install wl-copy, xclip, or xsel".into())
}
pub fn without_player(mut self) -> Self {
self.spawn_player = None;
self
@@ -76,6 +118,10 @@ impl App {
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
while !self.exit {
if self.needs_clear {
terminal.clear().wrap_err("terminal clear failed")?;
self.needs_clear = false;
}
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()
.await