use qrcode::{EcLevel, QrCode};
use ratatui::{
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style},
text::Line,
widgets::{Block, Borders, Clear, Paragraph},
Frame,
};
use tmai_core::state::AppState;
pub struct QrScreen;
impl QrScreen {
pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
let url = state.get_web_url();
let qr_info = url.as_ref().and_then(|u| {
QrCode::with_error_correction_level(u.as_bytes(), EcLevel::L)
.ok()
.map(|code| {
let width = code.width();
let height = width.div_ceil(2);
(code, width, height)
})
});
let (qr_width, qr_height) = qr_info
.as_ref()
.map(|(_, w, h)| (*w, *h))
.unwrap_or((21, 11));
let popup_width = (qr_width as u16 + 4)
.max(35)
.min(area.width.saturating_sub(2));
let popup_height = (qr_height as u16 + 6).min(area.height.saturating_sub(2));
let popup_x = (area.width.saturating_sub(popup_width)) / 2;
let popup_y = (area.height.saturating_sub(popup_height)) / 2;
let popup_area = Rect::new(
area.x + popup_x,
area.y + popup_y,
popup_width,
popup_height,
);
frame.render_widget(Clear, popup_area);
let block = Block::default()
.title(" Web Remote Control ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
let inner = block.inner(popup_area);
frame.render_widget(block, popup_area);
match (url, qr_info) {
(Some(url), Some((code, _, _))) => {
Self::render_qr_content(frame, inner, &url, &code);
}
(Some(url), None) => {
Self::render_qr_error(frame, inner, &url);
}
_ => Self::render_error(frame, inner),
}
}
fn render_qr_content(frame: &mut Frame, area: Rect, url: &str, code: &QrCode) {
let qr_string = Self::render_qr_to_string(code);
let qr_lines: Vec<&str> = qr_string.lines().collect();
let qr_height = qr_lines.len() as u16;
let chunks = Layout::vertical([
Constraint::Length(1), Constraint::Length(qr_height), Constraint::Length(1), Constraint::Length(1), Constraint::Min(1), ])
.split(area);
let title = Paragraph::new("Scan with your smartphone")
.style(Style::default().fg(Color::White))
.alignment(Alignment::Center);
frame.render_widget(title, chunks[0]);
let qr_text: Vec<Line> = qr_lines
.iter()
.map(|line| Line::from(*line).style(Style::default().fg(Color::White)))
.collect();
let qr_widget = Paragraph::new(qr_text).alignment(Alignment::Center);
frame.render_widget(qr_widget, chunks[1]);
let max_chars = area.width.saturating_sub(2) as usize;
let display_url = if url.chars().count() > max_chars {
let truncated: String = url.chars().take(max_chars.saturating_sub(3)).collect();
format!("{}...", truncated)
} else {
url.to_string()
};
let url_widget = Paragraph::new(display_url)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Center);
frame.render_widget(url_widget, chunks[3]);
let instructions = Paragraph::new("Press 'r' or Esc to close")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(instructions, chunks[4]);
}
fn render_qr_error(frame: &mut Frame, area: Rect, url: &str) {
let chunks = Layout::vertical([
Constraint::Min(1),
Constraint::Length(2),
Constraint::Length(1),
])
.split(area);
let error = Paragraph::new("QR code generation failed")
.style(Style::default().fg(Color::Red))
.alignment(Alignment::Center);
frame.render_widget(error, chunks[0]);
let url_widget = Paragraph::new(url.to_string())
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Center);
frame.render_widget(url_widget, chunks[1]);
let instructions = Paragraph::new("Press 'r' or Esc to close")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(instructions, chunks[2]);
}
fn render_error(frame: &mut Frame, area: Rect) {
let error = Paragraph::new(vec![
Line::from("Web server not initialized"),
Line::from(""),
Line::from("Check settings.web.enabled in config"),
])
.style(Style::default().fg(Color::Red))
.alignment(Alignment::Center);
frame.render_widget(error, area);
}
fn render_qr_to_string(code: &QrCode) -> String {
let width = code.width();
let mut result = String::new();
for y in (0..width).step_by(2) {
for x in 0..width {
let top = code[(x, y)] == qrcode::Color::Dark;
let bottom = if y + 1 < width {
code[(x, y + 1)] == qrcode::Color::Dark
} else {
false
};
let ch = match (top, bottom) {
(true, true) => '\u{2588}', (true, false) => '\u{2580}', (false, true) => '\u{2584}', (false, false) => ' ',
};
result.push(ch);
}
result.push('\n');
}
result
}
}