oyo 0.1.66

CLI TUI for oyo - Step-through diff viewer
use crate::*;
use std::sync::{mpsc, Arc};

pub(crate) struct PreparedRemote {
    diff: MultiFileDiff,
    branch: Option<String>,
    restore_review: crate::app::startup::RestoreRemoteReview,
    control: Result<control::ControlSession, String>,
}

pub(crate) enum StartupEvent {
    Progress(&'static str),
    Ready(Result<Box<PreparedRemote>, String>),
}

struct StartupWorker {
    context: remote::loading::ReadContext,
    rx: mpsc::Receiver<StartupEvent>,
    contents: Option<remote::loading::ReadContext>,
}

impl Drop for StartupWorker {
    fn drop(&mut self) {
        self.context.cancel();
        if let Some(contents) = &self.contents {
            contents.cancel();
        }
    }
}

fn start(
    url: String,
    config: Arc<config::Config>,
    args: Arc<Args>,
    _light: bool,
    session: Option<String>,
    last_applied_seq: u64,
) -> StartupWorker {
    let context = remote::loading::ReadContext::default();
    let worker_context = context.clone();
    let contents = remote::loading::ReadContext::default();
    let worker_contents = contents.clone();
    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        worker_context.run(|| {
            let result = (|| -> Result<PreparedRemote> {
                let (built, patch_session) = remote::stream::start(&url, worker_contents.clone())?;
                let _ = tx.send(StartupEvent::Progress("Preparing review..."));
                if worker_context.is_cancelled() {
                    anyhow::bail!("Remote loading cancelled");
                }
                let speed = if args.speed != 200 {
                    args.speed
                } else {
                    config.playback.speed
                };
                let mut app = App::new(
                    built.multi_diff,
                    resolve_view_mode(args.view, &config),
                    speed,
                    args.autoplay || config.playback.autoplay,
                    built.branch,
                );
                app.remote_validation = built.remote_validation;
                app.remote_stream = patch_session;
                app.start_content_loading_with_context(worker_contents);
                if app.remote_stream.is_some() {
                    let review_config = config.clone();
                    let review_args = args.clone();
                    let review_url = url.clone();
                    let workspace = built.workspace_root;
                    app.pending_remote_review = Some(Box::new(move |app| {
                        configure_review_state_for_app(
                            app,
                            &review_config,
                            &review_args,
                            workspace.clone(),
                            &InputMode::Remote {
                                url: review_url.clone(),
                            },
                            None,
                            None,
                            true,
                        )
                    }));
                } else {
                    configure_review_state_for_app(
                        &mut app,
                        &config,
                        &args,
                        built.workspace_root,
                        &InputMode::Remote { url: url.clone() },
                        None,
                        None,
                        true,
                    )?;
                }
                if worker_context.is_cancelled() {
                    anyhow::bail!("Remote loading cancelled");
                }
                let workspace = current_review_workspace()?;
                let control = control::ControlSession::start(
                    &workspace,
                    &app.control_target_label(),
                    session.as_deref(),
                    last_applied_seq,
                )
                .map_err(|e| e.to_string());
                let branch = Some(url);
                let (diff, restore_review) = app.into_remote_startup();
                Ok(PreparedRemote {
                    diff,
                    branch,
                    restore_review,
                    control,
                })
            })()
            .map_err(|error| error.to_string());
            let _ = tx.send(StartupEvent::Ready(result.map(Box::new)));
        })
    });
    StartupWorker {
        context,
        rx,
        contents: Some(contents),
    }
}

pub(crate) fn remote_app(
    terminal: &mut TuiTerminal,
    url: &str,
    config: Arc<config::Config>,
    args: Arc<Args>,
    light: bool,
    session: Option<String>,
    last_applied_seq: u64,
) -> Result<Option<(App, Result<control::ControlSession, String>)>> {
    let mut worker = Some(start(
        url.to_owned(),
        config.clone(),
        args.clone(),
        light,
        session.clone(),
        last_applied_seq,
    ));
    let theme = config.ui.theme.resolve(light);
    let mut status = "Loading remote snapshot...".to_owned();
    let mut prepared_app = None;
    loop {
        if let Some(active) = worker.as_ref().filter(|_| prepared_app.is_none()) {
            loop {
                let event = match active.rx.try_recv() {
                    Ok(event) => event,
                    Err(mpsc::TryRecvError::Empty) => break,
                    Err(mpsc::TryRecvError::Disconnected) => {
                        status = "Unable to load review.".to_owned();
                        worker = None;
                        break;
                    }
                };
                match event {
                    StartupEvent::Progress(message) => status = message.to_owned(),
                    StartupEvent::Ready(Ok(prepared)) => {
                        let prepared = *prepared;
                        let speed = if args.speed != 200 {
                            args.speed
                        } else {
                            config.playback.speed
                        };
                        let mut app = App::new(
                            prepared.diff,
                            resolve_view_mode(args.view, &config),
                            speed,
                            args.autoplay || config.playback.autoplay,
                            prepared.branch,
                        );
                        apply_config_to_app(&mut app, &config, &args, light);
                        (prepared.restore_review)(&mut app);
                        app.move_selection_off_hidden_reviewed_file();
                        prepared_app = Some((app, prepared.control));
                        status = "Loading diff...".to_owned();
                        break;
                    }
                    StartupEvent::Ready(Err(error)) => {
                        status = format!("Unable to load review\n\n{error}");
                        worker = None;
                        break;
                    }
                }
            }
        }
        if let Some((app, _)) = prepared_app.as_mut() {
            app.poll_content_responses();
            app.poll_diff_responses();
            app.move_selection_off_hidden_reviewed_file();
            app.queue_current_file_diff();
            if app.multi_diff.file_count() == 0
                || !matches!(
                    app.multi_diff.diff_status(app.multi_diff.selected_index),
                    oyo_core::multi::DiffStatus::Loading
                        | oyo_core::multi::DiffStatus::Deferred
                        | oyo_core::multi::DiffStatus::Computing
                )
            {
                worker.as_mut().and_then(|worker| worker.contents.take());
                return Ok(prepared_app.take());
            }
        }
        terminal.draw(|frame| {
            use ratatui::{
                layout::{Alignment, Rect},
                style::{Modifier, Style},
                text::Line,
                widgets::{Block, Paragraph, Wrap},
            };
            let area = frame.area();
            let mut background = Style::default().fg(theme.text);
            if let Some(color) = theme.background {
                background = background.bg(color);
            }
            frame.render_widget(Block::default().style(background), area);
            let width = area.width.min(64);
            let text_rows = Paragraph::new(status.as_str())
                .wrap(Wrap { trim: true })
                .line_count(width)
                .min(usize::from(u16::MAX)) as u16;
            let height = area.height.min(text_rows.saturating_add(2));
            let content = Rect::new(
                area.x + area.width.saturating_sub(width) / 2,
                area.y + area.height.saturating_sub(height) / 2,
                width,
                height,
            );
            let hint = if width < 15 && worker.is_none() {
                "r/q"
            } else if width < 28 && worker.is_none() {
                "r retry  q quit"
            } else if width < 6 {
                "q"
            } else if width < 15 {
                "q quit"
            } else if worker.is_some() {
                "q / Esc to quit"
            } else {
                "r to retry   q / Esc to quit"
            };
            let body_height = height.saturating_sub(2);
            frame.render_widget(
                Paragraph::new(status.as_str())
                    .style(
                        Style::default()
                            .fg(if worker.is_some() {
                                theme.accent
                            } else {
                                theme.error
                            })
                            .add_modifier(Modifier::BOLD),
                    )
                    .alignment(Alignment::Center)
                    .wrap(Wrap { trim: true }),
                Rect::new(content.x, content.y, width, body_height.max(1).min(height)),
            );
            if height > 2 {
                frame.render_widget(
                    Paragraph::new(Line::from(hint))
                        .style(Style::default().fg(theme.text_muted))
                        .alignment(Alignment::Center),
                    Rect::new(content.x, content.y + height - 1, width, 1),
                );
            }
        })?;
        if input_event_poll(Duration::from_millis(16))? {
            if let Event::Key(key) = input_event_read()? {
                if key.kind == KeyEventKind::Release {
                    continue;
                }
                match key.code {
                    KeyCode::Char('q') | KeyCode::Esc => return Ok(None),
                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                        return Ok(None)
                    }
                    KeyCode::Char('r') if worker.is_none() => {
                        status = "Loading remote snapshot...".to_owned();
                        worker = Some(start(
                            url.to_owned(),
                            config.clone(),
                            args.clone(),
                            light,
                            session.clone(),
                            last_applied_seq,
                        ));
                    }
                    _ => {}
                }
            }
        }
    }
}