use base64::{engine::general_purpose, Engine as _};
use crossterm::{
cursor,
event::{read, Event, KeyCode, KeyEvent},
execute, style,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
terminal,
};
use scraper::{Html, Selector};
use std::{
env,
error::Error,
io::{self, Write},
};
use viuer::Config;
fn fetch_and_show(channel: i32) -> Result<(), Box<dyn Error>> {
let url = format!("https://www.svt.se/text-tv/{}", channel);
let html = reqwest::blocking::get(&url)?.text()?;
let document = Html::parse_document(&html);
let selector = Selector::parse("img.Content_pageImage__bS0mg")?;
let elem = document.select(&selector).next().ok_or("Page not found")?;
let data_url = elem.value().attr("src").unwrap();
let b64 = data_url.splitn(2, ',').nth(1).ok_or("invalid data URL")?;
let img_data = general_purpose::STANDARD.decode(b64)?;
let img = image::load_from_memory(&img_data)?;
let config = Config {
width: Some(100),
..Default::default()
};
viuer::print(&img, &config)?;
Ok(())
}
fn print_status(channel: i32) -> Result<(), Box<dyn Error>> {
execute!(
io::stdout(),
cursor::MoveToColumn(0),
cursor::MoveToNextLine(1),
)?;
println!(
"← prev → next g: go to page q: quit (now on {})",
channel
);
Ok(())
}
fn prompt_goto(current: i32) -> Result<Option<i32>, Box<dyn Error>> {
let mut input = String::new();
execute!(
io::stdout(),
cursor::MoveToColumn(0)
)?;
print!("Go to page (100–801): ");
io::stdout().flush()?;
loop {
if let Event::Key(KeyEvent { code, .. }) = read()? {
match code {
KeyCode::Char(c) if c.is_ascii_digit() => {
input.push(c);
print!("{}", c);
io::stdout().flush()?;
}
KeyCode::Backspace => {
if input.pop().is_some() {
execute!(
io::stdout(),
cursor::MoveLeft(1),
style::Print(" "),
cursor::MoveLeft(1),
)?;
io::stdout().flush()?;
}
}
KeyCode::Enter => break,
KeyCode::Esc => {
execute!(
io::stdout(),
cursor::MoveToColumn(0),
terminal::Clear(ClearType::CurrentLine),
)?;
io::stdout().flush()?;
return Ok(None);
}
_ => {}
}
}
}
if let Ok(n) = input.parse::<i32>() {
let page = n.clamp(100, 801);
if page != current {
return Ok(Some(page));
}
}
Ok(None)
}
fn print_page_not_found(page: i32) -> () {
let _ = execute!(
io::stdout(),
style::SetForegroundColor(style::Color::Red),
style::Print(format!("Page {} not found\n", page)),
style::ResetColor,
);
}
fn main() -> Result<(), Box<dyn Error>> {
execute!(io::stdout(), cursor::Hide)?;
let mut channel = env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(100)
.clamp(100, 801);
execute!(io::stdout(), Clear(ClearType::All), cursor::MoveTo(0, 0),)?;
if let Err(_) = fetch_and_show(channel) {
print_page_not_found(channel);
}
print_status(channel)?;
enable_raw_mode()?;
loop {
if let Event::Key(KeyEvent { code, .. }) = read()? {
match code {
KeyCode::Left => {
let new_ch = (channel - 1).max(100);
if new_ch != channel {
channel = new_ch;
execute!(io::stdout(), Clear(ClearType::All), cursor::MoveTo(0, 0))?;
if let Err(_) = fetch_and_show(channel) {
print_page_not_found(channel);
}
print_status(channel)?;
}
}
KeyCode::Right => {
let new_ch = (channel + 1).min(801);
if new_ch != channel {
channel = new_ch;
execute!(io::stdout(), Clear(ClearType::All), cursor::MoveTo(0, 0))?;
if let Err(_) = fetch_and_show(channel) {
print_page_not_found(channel);
}
print_status(channel)?;
}
}
KeyCode::Char('g') => {
if let Some(new_ch) = prompt_goto(channel)? {
channel = new_ch;
execute!(io::stdout(), Clear(ClearType::All), cursor::MoveTo(0, 0))?;
if let Err(_) = fetch_and_show(channel) {
print_page_not_found(channel);
}
print_status(channel)?;
}
}
KeyCode::Char('q') => break,
_ => {}
}
}
}
disable_raw_mode()?;
execute!(io::stdout(), cursor::Show)?;
Ok(())
}