use crate::links::link_ranges;
use crossterm::event::KeyCode;
use ratatui::backend::CrosstermBackend;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::Terminal;
use std::io::Stdout;
use std::io::Write;
use std::process::{Command, Stdio};
pub struct VisualState {
pub insert_active: bool,
pub visual_active: bool,
pub anchor: Option<(usize, usize)>,
pub cursor: Option<(usize, usize)>,
pub buffer: String,
}
impl VisualState {
pub fn new() -> Self {
VisualState {
insert_active: false,
visual_active: false,
anchor: None,
cursor: None,
buffer: String::new(),
}
}
}
pub fn handle_key_event(
vs: &mut VisualState,
key: KeyCode,
body: &Option<String>,
scroll: &mut u16,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> bool {
if let KeyCode::Char('i') = key {
vs.visual_active = false;
vs.insert_active = !vs.insert_active;
if vs.insert_active {
let row = *scroll as usize;
vs.cursor = Some((row, 0));
} else {
vs.cursor = None;
}
return true;
}
if !vs.insert_active && !vs.visual_active {
return false; }
if let Some((mut row, mut col)) = vs.cursor {
if vs.insert_active && matches!(key, KeyCode::Char('v')) {
vs.insert_active = false;
vs.visual_active = true;
vs.anchor = Some((row, col));
return true;
}
match key {
KeyCode::Char('h') => {
if col == 0 {
if row > 0 {
row -= 1;
if let Some(prev_line) = body.as_ref().and_then(|t| t.lines().nth(row)) {
col = prev_line.chars().count().saturating_sub(1);
}
}
} else {
col -= 1;
}
}
KeyCode::Char('l') => {
if let Some(curr_line) = body.as_ref().and_then(|t| t.lines().nth(row)) {
let line_len = curr_line.chars().count().saturating_sub(1);
if col >= line_len
&& row + 1 < body.as_ref().map(|t| t.lines().count()).unwrap_or(0)
{
row += 1;
col = 0;
} else {
col = (col + 1).min(line_len);
}
}
}
KeyCode::Char('j') => {
row += 1;
}
KeyCode::Char('k') => {
row = row.saturating_sub(1);
}
KeyCode::Esc => {
vs.insert_active = false;
vs.visual_active = false;
vs.anchor = None;
vs.cursor = None;
return true;
}
KeyCode::Char('y') if vs.visual_active => {
if let (Some((ar, ac)), Some((cr, cc)), Some(text)) =
(vs.anchor, vs.cursor, body.as_deref())
{
let (start_row, start_col, end_row, end_col) = if (ar, ac) <= (cr, cc) {
(ar, ac, cr, cc)
} else {
(cr, cc, ar, ac)
};
let mut selected = Vec::new();
for (i, line) in text
.lines()
.enumerate()
.skip(start_row)
.take(end_row - start_row + 1)
{
let start = if i == start_row { start_col } else { 0 };
let end = if i == end_row {
end_col
} else {
line.chars().count().saturating_sub(1)
};
let segment: String =
line.chars().skip(start).take(end - start + 1).collect();
selected.push(segment);
}
vs.buffer = selected.join("\n");
if let Ok(mut proc) = Command::new("wl-copy").stdin(Stdio::piped()).spawn() {
if let Some(mut stdin) = proc.stdin.take() {
let _ = stdin.write_all(vs.buffer.as_bytes());
}
}
}
vs.visual_active = false;
vs.anchor = None;
vs.cursor = None;
return true;
}
_ => {}
}
if let Some(b) = body {
let max_row = b.lines().count().saturating_sub(1);
row = row.min(max_row);
}
vs.cursor = Some((row, col));
if vs.visual_active {
if let Ok(area) = terminal.size() {
let visible_height = area.height.saturating_sub(8) as usize;
if row < *scroll as usize {
*scroll = row as u16;
} else if row >= (*scroll as usize + visible_height) {
*scroll = (row + 1 - visible_height) as u16;
}
}
}
return true;
}
false
}
pub fn render_body<'a>(
vs: &VisualState,
body: &'a str,
scroll: u16,
height: usize,
_width: usize,
) -> Vec<Line<'a>> {
let raw: Vec<&str> = body.lines().collect();
let total_lines = raw.len();
let start = scroll as usize;
let end = (start + height).min(total_lines);
let url_ranges = link_ranges(body);
let mut out = Vec::with_capacity(end - start);
let (r0, c0, r1, c1) = if vs.visual_active {
if let (Some((ar, ac)), Some((cr, cc))) = (vs.anchor, vs.cursor) {
if (ar, ac) <= (cr, cc) {
(ar, ac, cr, cc)
} else {
(cr, cc, ar, ac)
}
} else {
(usize::MAX, 0, usize::MAX, 0)
}
} else {
(usize::MAX, 0, usize::MAX, 0)
};
let multi = r0 != r1;
for row in start..end {
let line = raw[row];
let mut spans = Vec::new();
for (col, ch) in line.chars().enumerate() {
let mut style = Style::default();
for &(s, e) in &url_ranges[row] {
if col >= s && col <= e {
style = style
.fg(Color::LightGreen)
.add_modifier(Modifier::UNDERLINED);
break;
}
}
if vs.insert_active {
if let Some((cr, cc)) = vs.cursor {
if cr == row && cc == col {
style = Style::default().bg(Color::White).fg(Color::Black);
}
}
}
if vs.visual_active {
if multi {
if (row > r0 && row < r1)
|| (row == r0 && col >= c0)
|| (row == r1 && col <= c1)
{
style = style.bg(Color::Blue);
}
} else if row == r0 && col >= c0 && col <= c1 {
style = style.bg(Color::Blue);
}
}
spans.push(Span::styled(ch.to_string(), style));
}
out.push(Line::from(spans));
}
out
}