| Crates.io | gemchat |
| lib.rs | gemchat |
| version | 0.1.0 |
| created_at | 2026-01-22 21:04:41.640306+00 |
| updated_at | 2026-01-22 21:04:41.640306+00 |
| description | simple gemini chat built with ratatui |
| homepage | https://github.com/thelazydo/gemchat |
| repository | https://github.com/thelazydo/gemchat |
| max_upload_size | |
| id | 2062623 |
| size | 97,726 |
Run these commands in your terminal to create the project and add the necessary dependencies (ratatui and crossterm):
cargo init
cargo add ratatui crossterm
Replace the contents of src/main.rs with the following code.
This implementation features:
use std::{io, time::Duration};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
struct App {
/// Current value of the input box
input: String,
/// History of recorded messages
messages: Vec<String>,
}
impl Default for App {
fn default() -> App {
App {
input: String::new(),
messages: Vec::new(),
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Create app state
let app = App::default();
// Run the app
let res = run_app(&mut terminal, app);
// Restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
KeyCode::Char(c) => {
app.input.push(c);
}
KeyCode::Backspace => {
app.input.pop();
}
KeyCode::Esc => {
return Ok(());
}
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Min(1),
Constraint::Length(3),
]
.as_ref(),
)
.split(f.area());
// Messages List
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = Line::from(Span::raw(format!("{}: {}", i, m)));
ListItem::new(content)
})
.collect();
let messages = List::new(messages)
.block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[0]);
// Input Paragraph
let input = Paragraph::new(app.input.as_str())
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
}
Run the application with:
cargo run
Press Esc to exit the application.