aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar lancebord 2026-03-07 21:50:12 -0500
committerGravatar lancebord 2026-03-07 21:50:12 -0500
commit3c47dd2c4802cfe959a628ea55c17a61832a57b1 (patch)
tree82467378ffa7a3f3be6191434df556b7b91c3e5f
parentmake chatbot fill from bottom (diff)
added chat scrolling
-rw-r--r--src/main.rs4
-rw-r--r--src/tui/app.rs13
-rw-r--r--src/tui/ui.rs22
3 files changed, 30 insertions, 9 deletions
diff --git a/src/main.rs b/src/main.rs
index 9c3d631..23a44f8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -70,7 +70,7 @@ async fn run(
// neither blocks the other.
loop {
// Draw
- terminal.draw(|f| ui::draw(f, &app))?;
+ terminal.draw(|f| ui::draw(f, &mut app))?;
// Poll crossterm for keyboard input (non-blocking, 20ms timeout)
if event::poll(Duration::from_millis(20))? {
@@ -96,6 +96,8 @@ async fn run(
(_, KeyCode::Backspace) => app.input_backspace(),
(_, KeyCode::Left) => app.cursor_left(),
(_, KeyCode::Right) => app.cursor_right(),
+ (_, KeyCode::Up) => app.scroll_up(),
+ (_, KeyCode::Down) => app.scroll_down(),
(_, KeyCode::Home) => app.cursor = 0,
(_, KeyCode::End) => app.cursor = app.input.len(),
_ => {}
diff --git a/src/tui/app.rs b/src/tui/app.rs
index 82342c0..7786858 100644
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -23,6 +23,8 @@ pub struct AppState {
pub input: String,
/// Cursor position within `input` (byte index)
pub cursor: usize,
+ /// Line scroll offset (byte index)
+ pub scroll_offset: usize,
/// Status line text (connection state, errors, etc.)
pub status: String,
/// Whether we've fully registered
@@ -38,6 +40,7 @@ impl AppState {
members: Vec::new(),
input: String::new(),
cursor: 0,
+ scroll_offset: 0,
status: "Set nick with /nick to connect.".into(),
connected: false,
}
@@ -101,6 +104,16 @@ impl AppState {
}
}
+ /// Scroll up one line
+ pub fn scroll_up(&mut self) {
+ self.scroll_offset += 1;
+ }
+
+ /// Scroll down one line
+ pub fn scroll_down(&mut self) {
+ self.scroll_offset = self.scroll_offset.saturating_sub(1);
+ }
+
/// Take the current input, clear the box, return the text
pub fn take_input(&mut self) -> String {
self.cursor = 0;
diff --git a/src/tui/ui.rs b/src/tui/ui.rs
index 0f552f2..ecdc477 100644
--- a/src/tui/ui.rs
+++ b/src/tui/ui.rs
@@ -12,7 +12,7 @@ use unicode_width::UnicodeWidthStr;
// Dark terminal aesthetic: near-black background, cool grey chrome,
// amber accent for our own nick, cyan for others, muted green for system.
-pub fn draw(f: &mut Frame, state: &AppState) {
+pub fn draw(f: &mut Frame, state: &mut AppState) {
let area = f.area();
// Fill background
@@ -54,7 +54,7 @@ fn draw_titlebar(f: &mut Frame, area: Rect, state: &AppState) {
f.render_widget(Paragraph::new(title).style(Style::default()), area);
}
-fn draw_body(f: &mut Frame, area: Rect, state: &AppState) {
+fn draw_body(f: &mut Frame, area: Rect, state: &mut AppState) {
// Body: [chat (fill)] | [members (18)]
let cols = Layout::default()
.direction(Direction::Horizontal)
@@ -68,7 +68,7 @@ fn draw_body(f: &mut Frame, area: Rect, state: &AppState) {
draw_members_panel(f, cols[1], state);
}
-fn draw_center(f: &mut Frame, area: Rect, state: &AppState) {
+fn draw_center(f: &mut Frame, area: Rect, state: &mut AppState) {
// Centre column: chat log on top, input box on bottom
let rows = Layout::default()
.direction(Direction::Vertical)
@@ -82,38 +82,44 @@ fn draw_center(f: &mut Frame, area: Rect, state: &AppState) {
draw_input(f, rows[1], state);
}
-fn draw_chat_log(f: &mut Frame, area: Rect, state: &AppState) {
+fn draw_chat_log(f: &mut Frame, area: Rect, state: &mut AppState) {
let lines: Vec<Line> = state
.messages
.iter()
.map(|msg| render_chat_line(msg))
.collect();
+
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(Style::default().fg(Color::Green))
.style(Style::default());
+
let inner_width = area.width.saturating_sub(2) as usize;
let inner_height = area.height.saturating_sub(2) as usize;
let total_wrapped = count_wrapped_lines(&lines, inner_width);
- let (padded_lines, scroll) = if total_wrapped < inner_height {
- // Pad the top with empty lines to push content to the bottom
+ let (padded_lines, base_scroll) = if total_wrapped < inner_height {
let padding = inner_height - total_wrapped;
let mut padded = vec![Line::raw(""); padding];
padded.extend(lines);
(padded, 0u16)
} else {
- // Content overflows — scroll to keep the latest lines visible
let scroll = total_wrapped.saturating_sub(inner_height);
(lines, scroll as u16)
};
+ // Max scrollable lines upward from the natural bottom position
+ let max_offset = base_scroll as usize;
+
+ // Clamp the offset and write it back so app.rs stays in sync
+ state.scroll_offset = state.scroll_offset.clamp(0, max_offset);
+ let final_scroll = (base_scroll as i32 - state.scroll_offset as i32) as u16;
f.render_widget(
Paragraph::new(Text::from(padded_lines))
.block(block)
.wrap(Wrap { trim: false })
- .scroll((scroll, 0)),
+ .scroll((final_scroll, 0)),
area,
);
}