feat: implement automatic scrolling

This commit is contained in:
2025-07-15 12:49:02 -04:00
parent 53fdd0a0f2
commit 66aa3bdd45

View File

@@ -1,5 +1,5 @@
use crossterm::{ use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, EventStream}, event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, EventStream},
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
}; };
@@ -22,6 +22,7 @@ struct App {
input: String, input: String,
username: String, username: String,
current_room: String, current_room: String,
messages_state: ListState,
} }
impl App { impl App {
@@ -31,11 +32,14 @@ impl App {
messages.push(("System".to_string(), format!("Welcome to the chat, {}!", username))); messages.push(("System".to_string(), format!("Welcome to the chat, {}!", username)));
messages.push(("System".to_string(), "To change your username, type /nick <new_username>".to_string())); messages.push(("System".to_string(), "To change your username, type /nick <new_username>".to_string()));
messages.push(("System".to_string(), format!("You are currently in room: {}. To change room, type /join <new_room_name>", default_room))); messages.push(("System".to_string(), format!("You are currently in room: {}. To change room, type /join <new_room_name>", default_room)));
let mut messages_state = ListState::default();
messages_state.select(Some(messages.len().saturating_sub(1)));
App { App {
messages, messages,
input: String::new(), input: String::new(),
username, username,
current_room: default_room, current_room: default_room,
messages_state,
} }
} }
} }
@@ -117,7 +121,7 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Re
let mut reader = EventStream::new(); let mut reader = EventStream::new();
loop { loop {
terminal.draw(|f| ui(f, &app))?; terminal.draw(|f| ui(f, &mut app))?;
tokio::select! { tokio::select! {
Some(Ok(event)) = reader.next() => { Some(Ok(event)) = reader.next() => {
@@ -130,6 +134,7 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Re
let new_username = message_text.split_whitespace().nth(1).unwrap_or(&app.username).to_string(); let new_username = message_text.split_whitespace().nth(1).unwrap_or(&app.username).to_string();
app.username = new_username.clone(); app.username = new_username.clone();
app.messages.push(("System".to_string(), format!("Username changed to: {}", app.username))); app.messages.push(("System".to_string(), format!("Username changed to: {}", app.username)));
app.messages_state.select(Some(app.messages.len().saturating_sub(1)));
let chat_message = ChatMessage { let chat_message = ChatMessage {
username: old_username, username: old_username,
@@ -156,6 +161,7 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Re
client_clone.subscribe(&format!("chat/{}", new_room), QoS::AtMostOnce).await.unwrap(); client_clone.subscribe(&format!("chat/{}", new_room), QoS::AtMostOnce).await.unwrap();
app.current_room = new_room.clone(); app.current_room = new_room.clone();
app.messages.push(("System".to_string(), format!("Changed room to: {}", new_room))); app.messages.push(("System".to_string(), format!("Changed room to: {}", new_room)));
app.messages_state.select(Some(app.messages.len().saturating_sub(1)));
// Send joining message to new room // Send joining message to new room
let joining_message = ChatMessage { let joining_message = ChatMessage {
@@ -166,6 +172,7 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Re
client_clone.publish(&format!("chat/{}", new_room), QoS::AtMostOnce, false, joining_payload.as_bytes()).await.unwrap(); client_clone.publish(&format!("chat/{}", new_room), QoS::AtMostOnce, false, joining_payload.as_bytes()).await.unwrap();
} else { } else {
app.messages.push(("System".to_string(), format!("Already in room: {}", new_room))); app.messages.push(("System".to_string(), format!("Already in room: {}", new_room)));
app.messages_state.select(Some(app.messages.len().saturating_sub(1)));
} }
} else { } else {
let chat_message = ChatMessage { let chat_message = ChatMessage {
@@ -194,6 +201,7 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Re
} }
Some((username, message)) = rx.recv() => { Some((username, message)) = rx.recv() => {
app.messages.push((username, message)); app.messages.push((username, message));
app.messages_state.select(Some(app.messages.len().saturating_sub(1)));
} }
else => { else => {
break; break;
@@ -204,7 +212,7 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Re
Ok(()) Ok(())
} }
fn ui(f: &mut Frame, app: &App) { fn ui(f: &mut Frame, app: &mut App) {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(1) .margin(1)
@@ -216,9 +224,9 @@ fn ui(f: &mut Frame, app: &App) {
.iter() .iter()
.map(|(username, message)| ListItem::new(format!("{}: {}", username, message))) .map(|(username, message)| ListItem::new(format!("{}: {}", username, message)))
.collect(); .collect();
let messages = List::new(messages) let messages_list = List::new(messages)
.block(Block::default().borders(Borders::ALL).title("Messages")); .block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[0]); f.render_stateful_widget(messages_list, chunks[0], &mut app.messages_state);
let input = Paragraph::new(app.input.as_str()) let input = Paragraph::new(app.input.as_str())
.style(Style::default()) .style(Style::default())