Skip to content

Commit 677fd42

Browse files
committed
implements scroll in chat window
1 parent 40dcc0a commit 677fd42

File tree

6 files changed

+120
-17
lines changed

6 files changed

+120
-17
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/chat-cli-ui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ eyre.workspace = true
2828
tokio-util.workspace = true
2929
futures.workspace = true
3030
rustyline.workspace = true
31+
strip-ansi-escapes.workspace = true
3132
ratatui = "0.29.0"
3233

3334
[target.'cfg(unix)'.dependencies]

crates/chat-cli-ui/src/ui/action.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,17 @@ pub enum Action {
1616
Error(String),
1717
Help,
1818
Input(InputEvent),
19+
Scroll(Scroll),
20+
}
21+
22+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23+
pub enum Scroll {
24+
Up(ScrollDistance),
25+
Down(ScrollDistance),
26+
}
27+
28+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29+
pub enum ScrollDistance {
30+
Message,
31+
Line(u16),
1932
}

crates/chat-cli-ui/src/ui/components/app.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ impl App {
156156
Action::ClearScreen => {},
157157
Action::Error(_) => {},
158158
Action::Help => {},
159+
Action::Scroll(_) => {},
159160
Action::Input(input_event) => {
160161
if let Err(e) = self.view_end.sender.send(input_event.clone()).await {
161162
error!("Error sending input event to control end: {:?}", e);

crates/chat-cli-ui/src/ui/components/chat_window.rs

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ use crate::protocol::{
1818
Event as SessionEvent,
1919
InputEvent,
2020
};
21-
use crate::ui::action::Action;
21+
use crate::ui::action::{
22+
Action,
23+
Scroll,
24+
ScrollDistance,
25+
};
2226

2327
#[derive(Debug, Clone)]
2428
struct Message {
@@ -43,6 +47,7 @@ pub struct ChatWindow {
4347
messages: Vec<Message>,
4448
current_message: Option<Message>,
4549
scroll_offset: u16,
50+
nearest_message_idx: usize,
4651
visible: bool,
4752
// TODO: update on resize
4853
pub dimension: (u16, u16),
@@ -108,7 +113,7 @@ impl Component for ChatWindow {
108113
Style::default().fg(Color::DarkGray),
109114
),
110115
Span::styled(format!("{}: ", prefix), style.bold()),
111-
Span::raw(&message.content),
116+
Span::raw(strip_ansi_escapes::strip_str(&message.content)),
112117
]));
113118
lines.push(Line::from("")); // Empty line between messages
114119
}
@@ -196,20 +201,63 @@ impl Component for ChatWindow {
196201
}
197202

198203
fn update(&mut self, action: Action) -> eyre::Result<Option<Action>> {
199-
if let Action::Input(input_event) = action {
200-
match input_event {
201-
InputEvent::Text(text) => {
202-
self.add_message(MessageRole::User, text);
203-
self.scroll_offset = self.messages.last().as_ref().map(|msg| msg.offset).unwrap_or_default();
204-
return Ok(Some(Action::Render));
205-
},
206-
InputEvent::Interrupt => {
207-
// Handle interrupt - could be used to cancel current streaming message
208-
if self.current_message.is_some() {
209-
self.finalize_current_message();
210-
return Ok(Some(Action::Render));
204+
if self.visible {
205+
match action {
206+
Action::Input(input_event) => {
207+
match input_event {
208+
InputEvent::Text(text) => {
209+
self.add_message(MessageRole::User, text);
210+
self.scroll_offset =
211+
self.messages.last().as_ref().map(|msg| msg.offset).unwrap_or_default();
212+
self.nearest_message_idx = self.messages.len();
213+
return Ok(Some(Action::Render));
214+
},
215+
InputEvent::Interrupt => {
216+
// Handle interrupt - could be used to cancel current streaming message
217+
if self.current_message.is_some() {
218+
self.finalize_current_message();
219+
return Ok(Some(Action::Render));
220+
}
221+
},
211222
}
212223
},
224+
Action::Scroll(scroll) => match scroll {
225+
Scroll::Up(scroll_distance) => match scroll_distance {
226+
ScrollDistance::Message => {
227+
if self.nearest_message_idx == 0 {
228+
return Ok(None);
229+
}
230+
self.nearest_message_idx -= 1;
231+
self.scroll_offset = self
232+
.messages
233+
.get(self.nearest_message_idx)
234+
.as_ref()
235+
.map(|msg| msg.offset)
236+
.unwrap_or_default();
237+
238+
return Ok(Some(Action::Render));
239+
},
240+
ScrollDistance::Line(_) => {},
241+
},
242+
Scroll::Down(scroll_distance) => match scroll_distance {
243+
ScrollDistance::Message => {
244+
if self.messages.is_empty() || self.nearest_message_idx == self.messages.len() - 1 {
245+
return Ok(None);
246+
}
247+
self.nearest_message_idx += 1;
248+
self.scroll_offset = self
249+
.messages
250+
.get(self.nearest_message_idx)
251+
.as_ref()
252+
.map(|msg| msg.offset)
253+
.unwrap_or_default();
254+
255+
return Ok(Some(Action::Render));
256+
},
257+
ScrollDistance::Line(_) => {},
258+
},
259+
},
260+
_ => {},
213261
}
214262
}
215263

crates/chat-cli-ui/src/ui/config.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
use std::collections::HashMap;
22

3-
use crossterm::event::KeyEvent;
3+
use crossterm::event::{
4+
KeyCode,
5+
KeyEvent,
6+
KeyEventKind,
7+
KeyEventState,
8+
KeyModifiers,
9+
};
410
use serde::{
511
Deserialize,
612
Deserializer,
713
Serialize,
814
};
915

10-
use super::action::Action;
16+
use super::action::{
17+
Action,
18+
Scroll,
19+
ScrollDistance,
20+
};
1121

1222
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1323
pub enum Mode {
@@ -21,9 +31,38 @@ pub struct Config {
2131
pub keybindings: KeyBindings,
2232
}
2333

24-
#[derive(Clone, Debug, Default)]
34+
#[derive(Clone, Debug)]
2535
pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
2636

37+
impl Default for KeyBindings {
38+
fn default() -> Self {
39+
let mut mapping = HashMap::<Vec<KeyEvent>, Action>::new();
40+
mapping.insert(
41+
vec![KeyEvent {
42+
code: KeyCode::Char('p'),
43+
modifiers: KeyModifiers::CONTROL,
44+
kind: KeyEventKind::Press,
45+
state: KeyEventState::NONE,
46+
}],
47+
Action::Scroll(Scroll::Up(ScrollDistance::Message)),
48+
);
49+
mapping.insert(
50+
vec![KeyEvent {
51+
code: KeyCode::Char('n'),
52+
modifiers: KeyModifiers::CONTROL,
53+
kind: KeyEventKind::Press,
54+
state: KeyEventState::NONE,
55+
}],
56+
Action::Scroll(Scroll::Down(ScrollDistance::Message)),
57+
);
58+
59+
let mut inner = HashMap::<Mode, _>::new();
60+
inner.insert(Default::default(), mapping);
61+
62+
KeyBindings(inner)
63+
}
64+
}
65+
2766
impl<'de> Deserialize<'de> for KeyBindings {
2867
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2968
where

0 commit comments

Comments
 (0)