Inital Commit for VIVO-Temrinal

This commit is contained in:
2026-07-25 01:18:59 +02:00
commit 731c1fa34b
5 changed files with 3625 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+3268
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "VIVO-Terminal"
version = "0.1.0"
edition = "2024"
[dependencies]
color-eyre = "0.6.5"
ratatui = "0.30.2"
crossterm = "0.29.0"
tokio = { version = "1.53.1", features = ["full"] }
reqwest = { version = "0.13.4", features = ["json"] }
axum = "0.8.9"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
textwrap = "0.16.2"
+290
View File
@@ -0,0 +1,290 @@
mod rest;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crossterm::event;
use crossterm::event::{Event, KeyCode, KeyModifiers};
use ratatui::{DefaultTerminal, Frame};
use ratatui::layout::{Alignment, Constraint, Layout, Position};
use ratatui::prelude::Direction;
use ratatui::style::{Color, Style};
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap};
use crate::rest::REST;
use textwrap::wrap;
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init();
let result = App::new().run(&mut terminal);
Ok(())
}
pub struct App {
input: String,
should_quit: bool,
pos: usize,
messages: Arc<Mutex<Vec<String>>>,
rest: REST,
list_state: ListState,
reply_rx: mpsc::UnboundedReceiver<String>,
reply_tx: mpsc::UnboundedSender<String>,
}
impl App {
pub fn new() -> Self {
let (tx, rx) = mpsc::unbounded_channel();
App{
input: String::new(),
should_quit: false,
pos: 0,
messages: Arc::new(Mutex::new(Vec::new())),
rest: REST::new(),
list_state: ListState::default(),
reply_rx: rx,
reply_tx: tx,
}
}
fn run(&mut self, terminal: &mut DefaultTerminal) -> std::io::Result<()> {
while !self.should_quit {
terminal.draw(|frame| self.render(frame))?;
if crossterm::event::poll(Duration::from_millis(500))? {
self.handle_event()?;
}
self.drain_replies();
}
Ok(())
}
fn handle_event(&mut self) -> std::io::Result<()> {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char(c) => {
if(key.modifiers == KeyModifiers::CONTROL)
{
self.should_quit = true;
}
else {
self.enter_char(c)
}
},
KeyCode::Left => self.move_cursor_left(),
KeyCode::Right => self.move_cursor_right(),
KeyCode::Home => self.reset_cursor(),
KeyCode::End => self.pos = self.input.len() - 1,
KeyCode::Backspace => self.delete_char(),
KeyCode::Delete => self.delete_forward_char(),
KeyCode::Esc => self.should_quit = true,
KeyCode::Tab => self.should_quit = true,
KeyCode::Up => self.move_view_up(),
KeyCode::Down => self.move_view_down(),
KeyCode::Enter => {
let strMessage = self.input.clone();
self.push_message(strMessage.clone());
self.reset_cursor();
self.input = String::new();
let message = rest::Message::new(
strMessage.clone(),
"USER".parse().unwrap(),
false,
true,
false,
);
let tx = self.reply_tx.clone();
let mut rest = self.rest.clone();
tokio::spawn(async move {
match rest.send(message).await {
Ok(reply) => {
if(reply.role == "ASSISTANT")
{
let _ = tx.send(reply.message);
}
},
Err(e) => {
eprintln!("{}", e);
}
}
});
},
_ => {}
}
}
Ok(())
}
fn drain_replies(&mut self) {
while let Ok(msg) = self.reply_rx.try_recv() {
self.push_message(msg);
}
}
fn move_view_down(&mut self) {
let i = match self.list_state.selected() {
Some(i) => (i+1).min(self.messages.lock().unwrap().len().saturating_sub(1)),
None => 0,
};
self.list_state.select(Some(i));
}
fn move_view_up(&mut self) {
let i = match self.list_state.selected() {
Some(i) => i.saturating_sub(1),
None => self.messages.lock().unwrap().len().saturating_sub(1),
};
self.list_state.select(Some(i));
}
fn push_message(&mut self, message: String) {
let mut messages = self.messages.lock().unwrap();
messages.push(message);
let last = messages.len().saturating_sub(1);
self.list_state.select(Some(last));
}
fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.pos != 0;
if is_not_cursor_leftmost {
let current_index = self.pos;
let from_left_to_current_index = current_index - 1;
let before_chat_to_delete = self.input.chars().take(from_left_to_current_index);
let after_chat_to_delete = self.input.chars().skip(current_index);
self.input = before_chat_to_delete.chain(after_chat_to_delete).collect();
self.move_cursor_left();
}
}
fn delete_forward_char(&mut self) {
let is_not_the_rightmost = self.pos != self.input.len();
if is_not_the_rightmost {
let current_index = self.pos;
let from_left_to_current_index = current_index;
let before_chat_to_delete = self.input.chars().take(from_left_to_current_index);
let after_chat_to_delete = self.input.chars().skip(from_left_to_current_index+1);
self.input = before_chat_to_delete.chain(after_chat_to_delete).collect();
//self.move_cursor_left();
}
}
fn move_cursor_left(&mut self) {
let pos = self.pos.saturating_sub(1);
self.pos = self.clamp_cursor(pos);
}
fn move_cursor_right(&mut self) {
let pos = self.pos.saturating_add(1);
self.pos = self.clamp_cursor(pos);
}
fn clamp_cursor(&self, pos: usize) -> usize {
pos.clamp(0, self.input.len())
}
fn reset_cursor(&mut self) {
self.pos = 0;
}
fn enter_char(&mut self, new_char: char) {
let index = self.byte_index();
self.input.insert(index, new_char);
self.move_cursor_right();
}
fn byte_index(&self) -> usize {
self.input
.char_indices()
.map(|(index, _)| index)
.nth(self.pos)
.unwrap_or(self.input.len())
}
fn render(&mut self, frame: &mut Frame) {
let layout = Layout::vertical([
Constraint::Percentage(70),
Constraint::Min(3),
]);
let [chat_area, input_area] = frame.area().layout(&layout);
if chat_area.height < 10 {
// too small — render a warning instead of your normal UI
let warning = Paragraph::new("Terminal window too small!")
.alignment(Alignment::Center);
frame.render_widget(warning, frame.area());
return;
}
let messages = self.messages.lock().unwrap();
let items: Vec<ListItem> = messages
.iter()
.enumerate()
.map(|(i, msg)| {
let mut lines: Vec<Line>= wrap(msg.as_str(), chat_area.width.saturating_sub(2) as usize)
.iter()
.map(|s| Line::from(s.to_string()))
.collect();
if i != messages.len() - 1 {
// separator row — width matches the area you render into
let sep = "".repeat(chat_area.width as usize);
lines.push(Line::styled(sep, Style::default().fg(Color::DarkGray)));
}
ListItem::new(lines)
})
.collect();
let list = List::new(items)
.block(Block::default().borders(Borders::ALL).title("Messages"));
frame.render_stateful_widget(list, chat_area, &mut self.list_state);
let inner_width = input_area.width.saturating_sub(2) as usize;
let wrapped_lines = wrap(&self.input, inner_width);
frame.render_widget(
Paragraph::new(self.input.as_str())
.block(Block::new().borders(Borders::ALL))
.wrap(Wrap { trim: false }),
input_area,
);
let (cursor_line, cursor_col) = {
let mut remaining = self.pos;
let mut line_idx = 0;
let mut col = 0;
for (i, line) in wrapped_lines.iter().enumerate() {
let len = line.chars().count();
if remaining <= len {
line_idx = i;
col = remaining;
break;
}
remaining -= len;
line_idx = i + 1;
col = remaining; // handles case where pos is exactly at end of input
}
(line_idx, col)
};
frame.set_cursor_position(Position::new(
input_area.x + cursor_col as u16 + 1,
input_area.y + cursor_line as u16 + 1,
))
}
}
+51
View File
@@ -0,0 +1,51 @@
use reqwest::Client;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Message {
pub message: String,
pub role: String,
pub finished: bool,
pub no_voice: bool,
pub stream: bool,
}
impl Message {
pub fn new(message: String, role: String, finished: bool, no_voice: bool, stream: bool) -> Self {
Message {
message,
role,
finished,
no_voice,
stream,
}
}
}
#[derive(Clone)]
pub struct REST {
client: Client,
}
impl REST {
pub fn new() -> Self {
REST{
client: Client::new(),
}
}
pub async fn send(&mut self, mut message: Message) -> Result<Message, reqwest::Error> {
// enforcing this as the current REST API stack dose not support this.
message.stream = false;
let res = self.client
.post("http://localhost:8080/v1/message/send")
.json(&message)
.send()
.await?
.error_for_status()?
.json::<Message>()
.await?;
Ok(res)
}
}