Skip to content

Commit 8242912

Browse files
committed
add tests
1 parent a4fa980 commit 8242912

File tree

2 files changed

+180
-42
lines changed

2 files changed

+180
-42
lines changed

src/app_tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use crossterm::event::{KeyCode, KeyModifiers};
23
use expect_test::{Expect, expect};
34
use ratatui::{Terminal, backend::TestBackend};
45

@@ -36,3 +37,87 @@ fn test_render_empty_app() {
3637
"#]],
3738
);
3839
}
40+
41+
#[test]
42+
fn test_enter_key_behavior() {
43+
let mut app = App::new();
44+
45+
// Test that Enter inserts a newline
46+
app.enter_char('\n');
47+
assert_eq!(app.editor_buffer, "\n");
48+
assert_eq!(app.editor_cursor, 1);
49+
50+
// Test that Shift-Enter sends the message
51+
app.editor_buffer = "Hello".to_string();
52+
app.editor_cursor = 5;
53+
app.send_message();
54+
assert_eq!(app.editor_buffer, "");
55+
assert_eq!(app.editor_cursor, 0);
56+
assert_eq!(app.messages.len(), 1);
57+
assert!(matches!(app.messages[0], Message::User(ref text) if text == "Hello"));
58+
}
59+
60+
#[test]
61+
fn test_cursor_position_with_newlines() {
62+
let mut app = App::new();
63+
app.enter_char('a');
64+
app.enter_char('\n');
65+
app.enter_char('b');
66+
67+
let (x, y) = app.calculate_cursor_position();
68+
assert_eq!(x, 1);
69+
assert_eq!(y, 1);
70+
}
71+
72+
#[test]
73+
fn test_enter_tip_visibility() {
74+
let mut app = App::new();
75+
76+
// Initially, the tip is not shown
77+
assert!(!app.show_enter_tip);
78+
79+
// Pressing Enter shows the tip
80+
app.handle_key_code(KeyCode::Enter, KeyModifiers::NONE);
81+
assert!(app.show_enter_tip);
82+
83+
// Typing a character hides the tip
84+
app.handle_key_code(KeyCode::Char('a'), KeyModifiers::NONE);
85+
assert!(!app.show_enter_tip);
86+
87+
// Pressing Enter again shows the tip
88+
app.handle_key_code(KeyCode::Enter, KeyModifiers::NONE);
89+
assert!(app.show_enter_tip);
90+
91+
// Deleting a character hides the tip
92+
app.handle_key_code(KeyCode::Backspace, KeyModifiers::NONE);
93+
assert!(!app.show_enter_tip);
94+
95+
// Pressing Enter again shows the tip
96+
app.handle_key_code(KeyCode::Enter, KeyModifiers::NONE);
97+
assert!(app.show_enter_tip);
98+
99+
// Sending a message hides the tip
100+
app.handle_key_code(KeyCode::Enter, KeyModifiers::SHIFT);
101+
assert!(!app.show_enter_tip);
102+
}
103+
104+
#[test]
105+
fn test_render_enter_tip() {
106+
let mut app = App::new();
107+
app.handle_key_code(KeyCode::Enter, KeyModifiers::NONE);
108+
check(
109+
&app,
110+
expect![[r#"
111+
"┌Messages────────────────────────────────────────┐"
112+
"│ │"
113+
"│ │"
114+
"│ │"
115+
"│ │"
116+
"│ │"
117+
"└────────────────────────────────────────────────┘"
118+
"┌Editor──────────────────────────────────────────┐"
119+
"│ │"
120+
"└Alt- or opt-enter to send message───────────────┘"
121+
"#]],
122+
);
123+
}

src/main.rs

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use color_eyre::Result;
2-
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind};
3-
use ratatui::layout::{Constraint, Layout, Position};
2+
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
3+
use ratatui::layout::{Constraint, Layout, Position, Rect};
44
use ratatui::style::{Color, Style};
55
use ratatui::text::{Line, Span};
66
use ratatui::widgets::{Block, Paragraph};
@@ -51,6 +51,8 @@ struct App {
5151
editor_cursor: usize,
5252
/// Scroll position in editor
5353
editor_scroll: usize,
54+
/// Show a tip about how to send a message
55+
show_enter_tip: bool,
5456
}
5557

5658
impl App {
@@ -63,6 +65,7 @@ impl App {
6365
editor_buffer: String::new(),
6466
editor_cursor: 0,
6567
editor_scroll: 0,
68+
show_enter_tip: false,
6669
}
6770
}
6871

@@ -89,44 +92,18 @@ impl App {
8992
continue;
9093
}
9194

92-
match key.code {
93-
KeyCode::Char('q') | KeyCode::Char('c')
94-
if key
95-
.modifiers
96-
.contains(crossterm::event::KeyModifiers::CONTROL) =>
97-
{
98-
return Ok(());
99-
}
100-
// Temporary: simulate agent message for testing
101-
KeyCode::Char('r')
102-
if key
103-
.modifiers
104-
.contains(crossterm::event::KeyModifiers::CONTROL) =>
105-
{
106-
self.simulate_agent_response(&tx);
107-
}
108-
KeyCode::Char(c) => {
109-
self.enter_char(c);
110-
}
111-
KeyCode::Backspace => {
112-
self.delete_char();
113-
}
114-
KeyCode::Left => {
115-
self.move_cursor_left();
116-
}
117-
KeyCode::Right => {
118-
self.move_cursor_right();
119-
}
120-
KeyCode::Up => {
121-
self.scroll_messages_up();
122-
}
123-
KeyCode::Down => {
124-
self.scroll_messages_down();
125-
}
126-
KeyCode::Enter => {
127-
self.send_message();
128-
}
129-
_ => {}
95+
// Temporary: simulate agent message for testing
96+
if key.code == KeyCode::Char('r')
97+
&& key
98+
.modifiers
99+
.contains(crossterm::event::KeyModifiers::CONTROL)
100+
{
101+
self.simulate_agent_response(&tx);
102+
continue;
103+
}
104+
105+
if self.handle_key_code(key.code, key.modifiers) {
106+
return Ok(());
130107
}
131108
}
132109
UiEvent::AgentTurnBegin => {
@@ -149,6 +126,50 @@ impl App {
149126
Ok(())
150127
}
151128

129+
/// returns true if the app should quit
130+
fn handle_key_code(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
131+
self.show_enter_tip = false;
132+
133+
match code {
134+
KeyCode::Char('q') | KeyCode::Char('c')
135+
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) =>
136+
{
137+
return true;
138+
}
139+
KeyCode::Char(c) => {
140+
self.enter_char(c);
141+
}
142+
KeyCode::Backspace => {
143+
self.delete_char();
144+
}
145+
KeyCode::Left => {
146+
self.move_cursor_left();
147+
}
148+
KeyCode::Right => {
149+
self.move_cursor_right();
150+
}
151+
KeyCode::Up => {
152+
self.scroll_messages_up();
153+
}
154+
KeyCode::Down => {
155+
self.scroll_messages_down();
156+
}
157+
KeyCode::Enter => {
158+
if modifiers.contains(crossterm::event::KeyModifiers::SHIFT)
159+
|| modifiers.contains(crossterm::event::KeyModifiers::SUPER)
160+
|| modifiers.contains(crossterm::event::KeyModifiers::ALT)
161+
{
162+
self.send_message();
163+
} else {
164+
self.enter_char('\n');
165+
self.show_enter_tip = true;
166+
}
167+
}
168+
_ => {}
169+
}
170+
false
171+
}
172+
152173
async fn keyboard_task(tx: mpsc::UnboundedSender<UiEvent>) {
153174
let mut events = EventStream::new();
154175
while let Some(Ok(event)) = events.next().await {
@@ -214,10 +235,23 @@ impl App {
214235

215236
// Set cursor position
216237
#[expect(clippy::cast_possible_truncation)]
238+
let (cursor_x, cursor_y) = self.calculate_cursor_position();
217239
frame.set_cursor_position(Position::new(
218-
editor_area.x + self.editor_cursor as u16 + 1,
219-
editor_area.y + 1,
240+
editor_area.x + cursor_x as u16 + 1,
241+
editor_area.y + cursor_y as u16 + 1,
220242
));
243+
244+
if self.show_enter_tip {
245+
let tip_text = "Alt- or opt-enter to send message";
246+
let tip_area = Rect {
247+
x: editor_area.x + 1,
248+
y: editor_area.y + editor_area.height - 1,
249+
width: tip_text.len() as u16,
250+
height: 1,
251+
};
252+
let tip_paragraph = Paragraph::new(tip_text).style(Style::default().fg(Color::Yellow));
253+
frame.render_widget(tip_paragraph, tip_area);
254+
}
221255
}
222256

223257
// Editor methods (adapted from user-input example)
@@ -246,6 +280,7 @@ impl App {
246280
}
247281

248282
fn delete_char(&mut self) {
283+
self.show_enter_tip = false;
249284
let is_not_cursor_leftmost = self.editor_cursor != 0;
250285
if is_not_cursor_leftmost {
251286
let current_index = self.editor_cursor;
@@ -263,6 +298,23 @@ impl App {
263298
new_cursor_pos.clamp(0, self.editor_buffer.chars().count())
264299
}
265300

301+
fn calculate_cursor_position(&self) -> (usize, usize) {
302+
let mut x = 0;
303+
let mut y = 0;
304+
for (i, c) in self.editor_buffer.chars().enumerate() {
305+
if i == self.editor_cursor {
306+
break;
307+
}
308+
if c == '\n' {
309+
y += 1;
310+
x = 0;
311+
} else {
312+
x += 1;
313+
}
314+
}
315+
(x, y)
316+
}
317+
266318
// Message scrolling
267319
fn scroll_messages_up(&mut self) {
268320
self.message_scroll = self.message_scroll.saturating_sub(1);
@@ -274,6 +326,7 @@ impl App {
274326

275327
// Send user message to message history
276328
fn send_message(&mut self) {
329+
self.show_enter_tip = false;
277330
if !self.editor_buffer.is_empty() {
278331
self.messages
279332
.push(Message::User(self.editor_buffer.clone()));

0 commit comments

Comments
 (0)