ised 0.1.0

An interactive tool for find-and-replace across many files
Documentation
use std::fs;
use tui::{
    backend::Backend,
    layout::{Constraint, Direction, Layout},
    style::{Color, Modifier, Style},
    text::{Span, Spans, Text},
    widgets::{Block, Borders, List, ListItem, Paragraph},
    Frame,
};

use crate::app::{App, ConfirmState, Focus};
use crate::utils::apply_substitution_partial;
use crate::utils::highlight_diff_lines;
use crate::utils::highlight_match;

pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App, filtered_files: &[String]) {
    let size = f.size();
    let columns = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(size);

    let left_rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(10),
            Constraint::Length(3),
            Constraint::Length(3),
        ])
        .split(columns[0]);

    let right_rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(10),
            Constraint::Length(3),
            Constraint::Length(3),
        ])
        .split(columns[1]);

    let list_height = left_rows[0].height as usize - 2;
    let mut offset = app.offset;
    if app.selected >= offset + list_height {
        offset = app.selected + 1 - list_height;
    } else if app.selected < offset {
        offset = app.selected;
    }

    let visible_files = filtered_files
        .iter()
        .skip(offset)
        .take(list_height)
        .enumerate()
        .map(|(i, fpath)| {
            let content = highlight_match(fpath, &app.filter_input);
            let mut item = ListItem::new(content);
            if i + offset == app.selected {
                item = item.style(
                    Style::default()
                        .fg(Color::Yellow)
                        .add_modifier(Modifier::BOLD),
                );
            }
            item
        })
        .collect::<Vec<_>>();

    let file_list = List::new(visible_files).block(
        Block::default()
            .title("File [L]ist")
            .borders(Borders::ALL)
            .border_style(if app.focus == Focus::FileList {
                Style::default().fg(Color::Cyan)
            } else {
                Style::default()
            }),
    );
    f.render_widget(file_list, left_rows[0]);

    let filter_input = Paragraph::new(Text::from(app.filter_input.as_str())).block(
        Block::default()
            .title("[G]lob Filter")
            .borders(Borders::ALL)
            .border_style(if app.focus == Focus::FilePathFilter {
                Style::default().fg(Color::Cyan)
            } else {
                Style::default()
            }),
    );
    f.render_widget(filter_input, left_rows[1]);

    let blank_text = match &app.confirm {
        ConfirmState::Confirming(path) => format!("Apply changes to {}? (y/n)", path),
        ConfirmState::ConfirmingAll(_) => "Apply changes to ALL files? (y/n)".to_string(),
        ConfirmState::None => "ised v0.1.0".to_string(),
    };
    let blank = Paragraph::new(Text::from(blank_text));
    f.render_widget(blank, left_rows[2]);

    let selected_file = filtered_files.get(app.selected).map(|s| s.to_string());
    let diff_output = if let Some(file_path) = selected_file {
        if let Ok(content) = fs::read_to_string(&file_path) {
            let replaced = apply_substitution_partial(&content, &app.from_input, &app.to_input);
            highlight_diff_lines(content, replaced)
        } else {
            vec![Spans::from(Span::styled(
                "Failed to read file.",
                Style::default().fg(Color::Red),
            ))]
        }
    } else {
        vec![Spans::from("No file selected.")]
    };

    let height = right_rows[0].height as usize - 2;
    let visible_diff = diff_output
        .into_iter()
        .skip(app.diff_scroll)
        .take(height)
        .collect::<Vec<_>>();

    let diff_view = Paragraph::new(visible_diff).block(
        Block::default()
            .title("[D]iff")
            .borders(Borders::ALL)
            .border_style(if app.focus == Focus::DiffView {
                Style::default().fg(Color::Cyan)
            } else {
                Style::default()
            }),
    );
    f.render_widget(diff_view, right_rows[0]);

    let from_paragraph = Paragraph::new(Text::from(app.from_input.as_str())).block(
        Block::default()
            .title("[F]rom")
            .borders(Borders::ALL)
            .border_style(if app.focus == Focus::From {
                Style::default().fg(Color::Cyan)
            } else {
                Style::default()
            }),
    );
    f.render_widget(from_paragraph, right_rows[1]);

    let to_paragraph = Paragraph::new(Text::from(app.to_input.as_str())).block(
        Block::default()
            .title("[T]o")
            .borders(Borders::ALL)
            .border_style(if app.focus == Focus::To {
                Style::default().fg(Color::Cyan)
            } else {
                Style::default()
            }),
    );
    f.render_widget(to_paragraph, right_rows[2]);
}