Skip to content

Commit 4b7ccbf

Browse files
committed
partial #1403
1 parent 16de525 commit 4b7ccbf

File tree

7 files changed

+218
-2
lines changed

7 files changed

+218
-2
lines changed

crates/chat-cli/src/cli/chat/conversation_state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use super::tools::{
3838
ToolSpec,
3939
serde_value_to_document,
4040
};
41-
use crate::cli::chat::shared_writer::SharedWriter;
41+
use crate::cli::chat::util::shared_writer::SharedWriter;
4242
use crate::fig_api_client::model::{
4343
AssistantResponseMessage,
4444
ChatMessage,

crates/chat-cli/src/cli/chat/util/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
pub mod issue;
2+
pub mod shared_writer;
3+
pub mod ui;
24

35
use std::io::Write;
46
use std::time::Duration;
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use crossterm::style::{
2+
Color,
3+
Stylize,
4+
};
5+
use crossterm::terminal::{
6+
self,
7+
ClearType,
8+
};
9+
use crossterm::{
10+
cursor,
11+
execute,
12+
style,
13+
};
14+
use eyre::Result;
15+
use strip_ansi_escapes::strip_str;
16+
17+
use super::shared_writer::SharedWriter;
18+
19+
pub fn draw_box(
20+
mut output: SharedWriter,
21+
title: &str,
22+
content: &str,
23+
box_width: usize,
24+
border_color: Color,
25+
) -> Result<()> {
26+
let inner_width = box_width - 4; // account for │ and padding
27+
28+
// wrap the single line into multiple lines respecting inner width
29+
// Manually wrap the text by splitting at word boundaries
30+
let mut wrapped_lines = Vec::new();
31+
let mut line = String::new();
32+
33+
for word in content.split_whitespace() {
34+
if line.len() + word.len() < inner_width {
35+
if !line.is_empty() {
36+
line.push(' ');
37+
}
38+
line.push_str(word);
39+
} else {
40+
// Here we need to account for words that are too long as well
41+
if word.len() >= inner_width {
42+
let mut start = 0_usize;
43+
for (i, _) in word.chars().enumerate() {
44+
if i - start >= inner_width {
45+
wrapped_lines.push(word[start..i].to_string());
46+
start = i;
47+
}
48+
}
49+
wrapped_lines.push(word[start..].to_string());
50+
line = String::new();
51+
} else {
52+
wrapped_lines.push(line);
53+
line = word.to_string();
54+
}
55+
}
56+
}
57+
58+
if !line.is_empty() {
59+
wrapped_lines.push(line);
60+
}
61+
62+
let side_len = (box_width.saturating_sub(title.len())) / 2;
63+
let top_border = format!(
64+
"{} {} {}",
65+
style::style(format!("╭{}", "─".repeat(side_len - 2))).with(border_color),
66+
title,
67+
style::style(format!("{}╮", "─".repeat(box_width - side_len - title.len() - 2))).with(border_color)
68+
);
69+
70+
execute!(
71+
output,
72+
terminal::Clear(ClearType::CurrentLine),
73+
cursor::MoveToColumn(0),
74+
style::Print(format!("{top_border}\n")),
75+
)?;
76+
77+
// Top vertical padding
78+
let top_vertical_border = format!(
79+
"{}",
80+
style::style(format!("│{: <width$}│\n", "", width = box_width - 2)).with(border_color)
81+
);
82+
execute!(output, style::Print(top_vertical_border))?;
83+
84+
// Centered wrapped content
85+
for line in wrapped_lines {
86+
let visible_line_len = strip_str(&line).len();
87+
let left_pad = box_width.saturating_sub(4).saturating_sub(visible_line_len) / 2;
88+
89+
let content = format!(
90+
"{} {: <pad$}{}{: <rem$} {}",
91+
style::style("│").with(border_color),
92+
"",
93+
line,
94+
"",
95+
style::style("│").with(border_color),
96+
pad = left_pad,
97+
rem = box_width
98+
.saturating_sub(4)
99+
.saturating_sub(left_pad)
100+
.saturating_sub(visible_line_len),
101+
);
102+
execute!(output, style::Print(format!("{}\n", content)))?;
103+
}
104+
105+
// Bottom vertical padding
106+
execute!(
107+
output,
108+
style::Print(format!("│{: <width$}│\n", "", width = box_width - 2).with(border_color))
109+
)?;
110+
111+
// Bottom rounded corner line: ╰────────────╯
112+
let bottom = format!("╰{}╯", "─".repeat(box_width - 2)).with(border_color);
113+
execute!(output, style::Print(format!("{}\n", bottom)))?;
114+
Ok(())
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use std::sync::Arc;
120+
121+
use bstr::ByteSlice;
122+
use crossterm::style::Color;
123+
use shared_writer::TestWriterWithSink;
124+
125+
use crate::GREETING_BREAK_POINT;
126+
use crate::chat::util::shared_writer::{
127+
self,
128+
SharedWriter,
129+
};
130+
use crate::util::ui::draw_box;
131+
132+
#[tokio::test]
133+
async fn test_draw_tip_box() {
134+
let buf = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
135+
let test_writer = TestWriterWithSink { sink: buf.clone() };
136+
let output = SharedWriter::new(test_writer.clone());
137+
138+
// Test with a short tip
139+
let short_tip = "This is a short tip";
140+
draw_box(
141+
output.clone(),
142+
"Did you know?",
143+
short_tip,
144+
GREETING_BREAK_POINT,
145+
Color::DarkGrey,
146+
)
147+
.expect("Failed to draw tip box");
148+
149+
// Test with a longer tip that should wrap
150+
let long_tip = "This is a much longer tip that should wrap to multiple lines because it exceeds the inner width of the tip box which is calculated based on the GREETING_BREAK_POINT constant";
151+
draw_box(
152+
output.clone(),
153+
"Did you know?",
154+
long_tip,
155+
GREETING_BREAK_POINT,
156+
Color::DarkGrey,
157+
)
158+
.expect("Failed to draw tip box");
159+
160+
// Test with a long tip with two long words that should wrap
161+
let long_tip_with_one_long_word = {
162+
let mut s = "a".repeat(200);
163+
s.push(' ');
164+
s.push_str(&"a".repeat(200));
165+
s
166+
};
167+
draw_box(
168+
output.clone(),
169+
"Did you know?",
170+
long_tip_with_one_long_word.as_str(),
171+
GREETING_BREAK_POINT,
172+
Color::DarkGrey,
173+
)
174+
.expect("Failed to draw tip box");
175+
// Test with a long tip with two long words that should wrap
176+
let long_tip_with_two_long_words = "a".repeat(200);
177+
draw_box(
178+
output.clone(),
179+
"Did you know?",
180+
long_tip_with_two_long_words.as_str(),
181+
GREETING_BREAK_POINT,
182+
Color::DarkGrey,
183+
)
184+
.expect("Failed to draw tip box");
185+
186+
// Get the output and verify it contains expected formatting elements
187+
let content = test_writer.get_content();
188+
let output_str = content.to_str_lossy();
189+
190+
// Check for box drawing characters
191+
assert!(output_str.contains("╭"), "Output should contain top-left corner");
192+
assert!(output_str.contains("╮"), "Output should contain top-right corner");
193+
assert!(output_str.contains("│"), "Output should contain vertical lines");
194+
assert!(output_str.contains("╰"), "Output should contain bottom-left corner");
195+
assert!(output_str.contains("╯"), "Output should contain bottom-right corner");
196+
197+
// Check for the label
198+
assert!(
199+
output_str.contains("Did you know?"),
200+
"Output should contain the 'Did you know?' label"
201+
);
202+
203+
// Check that both tips are present
204+
assert!(output_str.contains(short_tip), "Output should contain the short tip");
205+
206+
// For the long tip, we check for substrings since it will be wrapped
207+
let long_tip_parts: Vec<&str> = long_tip.split_whitespace().collect();
208+
for part in long_tip_parts.iter().take(3) {
209+
assert!(output_str.contains(part), "Output should contain parts of the long tip");
210+
}
211+
}
212+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub const UPDATE_AVAILABLE_KEY: &str = "update.new-version-available";

crates/chat-cli/src/fig_settings/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod error;
2+
pub mod keys;
23
pub mod settings;
34
pub mod sqlite;
45
pub mod state;

crates/chat-cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fn main() -> Result<ExitCode> {
4040

4141
let multithread = matches!(
4242
std::env::args().nth(1).as_deref(),
43-
Some("init" | "_" | "internal" | "completion" | "hook")
43+
Some("init" | "_" | "internal" | "completion" | "hook" | "chat")
4444
);
4545

4646
let parsed = match cli::Cli::try_parse() {

0 commit comments

Comments
 (0)