monothink 0.1.1

A totally non-agentic coding assistant
/*
 * This file is part of monothink.
 *
 * Wavething is free software: you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * Wavething is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with Wavething.
 * If not, see <https://www.gnu.org/licenses/>.
 */

use crate::{
    core::conversation::ConversationItem,
    session::{SessionMessage, TokenUsage},
    ui::{
        conversation::{ConversationView, ConversationViewModel, HighlightConfig},
        statusline::{InputMode, StatusBar},
    },
};

use crossterm::event::EventStream;
use futures::StreamExt;

use anyhow::Result;
use crossterm::event::{Event, KeyCode};
use ratatui::{
    Frame, Terminal,
    layout::{Constraint, Layout},
    prelude::CrosstermBackend,
};
use ratatui_textarea::TextArea;
use std::io::Stdout;
use tokio::{
    sync::mpsc::{Receiver, Sender},
    time::{Duration, interval},
};

pub mod conversation;
pub mod statusline;

#[derive(Clone, Debug)]
pub enum UiMessage {
    NewConversationItem(ConversationItem),
    UpdatedConversationItem(ConversationItem),
    GenerationFinished(TokenUsage),
    //GenerationStarted,
    //ToolStarted(String),
    //ToolFinished(String),
    //Error(String),
}

#[derive(Debug)]
pub struct Ui {
    conversation: ConversationViewModel, // partially moves to Session
    input: TextArea<'static>,
    status: StatusBar<'static>,
    session_tx: Sender<SessionMessage>,
    rx: Receiver<UiMessage>,
    dirty: bool,
}

impl Ui {
    pub fn new(session_tx: Sender<SessionMessage>, rx: Receiver<UiMessage>) -> Self {
        // write to json for testing
        // let file = File::create("saved.json").unwrap();
        // let writer = io::BufWriter::new(file);
        // serde_json::to_writer(writer, &app.conversation).unwrap();

        Ui {
            conversation: ConversationViewModel::new(HighlightConfig::new()),
            input: TextArea::default(),
            status: StatusBar {
                input_mode: InputMode::Insert,
                model: "Qwen3-32B",
                context_used: 41_287,
                context_total: 65_536,
                tokens: TokenUsage::default(),
                assistant_mode: "Code",
            },
            session_tx,
            rx,
            dirty: true,
        }
    }

    pub async fn run(&mut self, mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
        let mut input_events = EventStream::new();
        let mut redraw_tick = interval(Duration::from_millis(20));

        // Main loop
        loop {
            tokio::select! {
                    Some(Ok(event)) = input_events.next() => {
                        if let Event::Key(key) = event {
                            if key.code == KeyCode::Esc {
                                self.session_tx.send(SessionMessage::QuitRequest).await.unwrap();
                                break Ok(());
                            } else if key.code == KeyCode::Up {
                                self.conversation.move_line_up();
                            } else if key.code == KeyCode::Down {
                                self.conversation.move_line_down();
                            } else if key.code == KeyCode::PageUp {
                                self.conversation.move_page_up();
                            } else if key.code == KeyCode::PageDown {
                                self.conversation.move_page_down();
                            } else if key.code == KeyCode::Enter {
                                if let Some(prompt) = self.input.lines().first() {
                                self.session_tx.send(SessionMessage::SubmitPrompt(prompt.to_string())).await.unwrap();
                                self.input.clear();
                                }
                            } else {
                                self.input.input_without_shortcuts(key);
                            }
                        // @todo self.handle_input_event(event);
                        self.dirty = true;
                        }
                    }

                    Some(event) = self.rx.recv() => {
                        self.handle_session_event(event);
                    }

                    _ = redraw_tick.tick() => {
                        // @todo catch errors to safe history

                        if self.dirty {
                            terminal.draw(|frame| self.render(frame))?;
                            self.dirty = false;
                        }
                    }
            }
        }

        // if !app.conversation.conversation.messages.is_empty() { // @todo and anything has changed
        //     write to file \<uuid\>.json in sessions folder, but only if the session is not empty.
        //     If the file exists, overwrite.
        // }
    }

    fn render(&mut self, frame: &mut Frame) {
        let [content, status_area] =
            Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(frame.area());

        //let input_height = Self::wrapped_height(&self.input, content.width).clamp(1, 5);
        let input_height = 1;

        let [conversation_area, input_area] =
            Layout::vertical([Constraint::Min(0), Constraint::Length(input_height)]).areas(content);

        // let color = Color::Rgb(52, 52, 52);
        // let input = Paragraph::new(format!("> {}", self.input))
        //     .fg(Color::White)
        //     .bg(color);

        frame.render_stateful_widget(ConversationView, conversation_area, &mut self.conversation);
        frame.render_widget(&self.input, input_area);
        frame.render_widget(self.status, status_area);
    }

    fn handle_session_event(&mut self, event: UiMessage) {
        match event {
            UiMessage::NewConversationItem(item) => {
                self.conversation.append_item(item);
                self.dirty = true;
            }
            UiMessage::UpdatedConversationItem(item) => {
                self.conversation.update_last_item(item);
                self.dirty = true;
            }
            UiMessage::GenerationFinished(usage) => {
                self.status.tokens = usage;
            }
        }
    }
}