Skip to content

Commit 0333467

Browse files
authored
fix bug in baml-cli init (#2697)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Harden init UI and error handling with TTY detection, safe cleanup, and non-interactive fallbacks, and exclude an additional test in CI. > > - **CLI (init UI)**: > - Add TTY detection using `IsTerminal`; only initialize UI when `stdout` is a TTY and gracefully fallback if UI creation fails. > - Ensure terminal state cleanup: disable raw mode on errors and after error UI; keep content and exit alternate screen reliably. > - Non-interactive fallback: print steps, completion (✓), and failures (✗) to stdio/stderr when UI is unavailable. > - Refactor error display: `show_error` checks TTY and delegates to `show_error_ui`; fallback to plain stderr message if UI fails. > - **Tests**: > - Exclude `tests/providers/test_aws_video_request.py` in `integ-tests/python/run_tests.sh`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8a8324f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 0f5895e commit 0333467

File tree

2 files changed

+148
-82
lines changed

2 files changed

+148
-82
lines changed

engine/baml-runtime/src/cli/init_ui.rs

Lines changed: 147 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
io,
2+
io::{self, IsTerminal},
33
time::{Duration, Instant},
44
};
55

@@ -48,18 +48,29 @@ const DOT_ANIMATION: &[&str] = &["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐"
4848
impl InitUI {
4949
pub fn new() -> Result<Self> {
5050
enable_raw_mode()?;
51-
let mut stdout = io::stdout();
52-
execute!(stdout, EnterAlternateScreen)?;
53-
let backend = CrosstermBackend::new(stdout);
54-
let terminal = Terminal::new(backend)?;
5551

56-
Ok(Self {
57-
steps: Vec::new(),
58-
terminal,
59-
animation_state: 0,
60-
dot_animation_state: 0,
61-
last_update: Instant::now(),
62-
})
52+
// Try to create the UI, but clean up raw mode if anything fails
53+
let result = (|| {
54+
let mut stdout = io::stdout();
55+
execute!(stdout, EnterAlternateScreen)?;
56+
let backend = CrosstermBackend::new(stdout);
57+
let terminal = Terminal::new(backend)?;
58+
59+
Ok(Self {
60+
steps: Vec::new(),
61+
terminal,
62+
animation_state: 0,
63+
dot_animation_state: 0,
64+
last_update: Instant::now(),
65+
})
66+
})();
67+
68+
// If anything failed, disable raw mode before returning the error
69+
if result.is_err() {
70+
let _ = disable_raw_mode();
71+
}
72+
73+
result
6374
}
6475

6576
pub fn add_step(&mut self, message: String) {
@@ -232,17 +243,32 @@ pub struct InitUIContext {
232243

233244
impl InitUIContext {
234245
pub fn new(use_ui: bool) -> Result<Self> {
235-
let ui = if use_ui { Some(InitUI::new()?) } else { None };
246+
let ui = if use_ui {
247+
// Check if stdout is a TTY before attempting to create UI
248+
if io::stdout().is_terminal() {
249+
// Try to create the UI, but gracefully fallback if it fails
250+
InitUI::new().ok()
251+
} else {
252+
// Not a TTY, use non-interactive mode
253+
None
254+
}
255+
} else {
256+
None
257+
};
236258
Ok(Self {
237259
ui,
238260
current_step: 0,
239261
})
240262
}
241263

264+
#[allow(clippy::print_stdout)]
242265
pub fn add_step(&mut self, message: &str) {
243266
if let Some(ui) = &mut self.ui {
244267
ui.add_step(message.to_string());
245268
let _ = ui.render();
269+
} else {
270+
// Non-interactive mode: just print the step
271+
println!(" {}", message);
246272
}
247273
}
248274

@@ -259,29 +285,41 @@ impl InitUIContext {
259285
}
260286
}
261287

288+
#[allow(clippy::print_stdout)]
262289
pub fn complete_step(&mut self) {
263290
if let Some(ui) = &mut self.ui {
264291
ui.update_step(self.current_step, StepStatus::Completed);
265292
let _ = ui.render();
266293
// Add a small delay to show the completion animation
267294
std::thread::sleep(Duration::from_millis(300));
295+
} else {
296+
// Non-interactive mode: print completion
297+
println!(" ✓ Done");
268298
}
269299
self.current_step += 1;
270300
}
271301

302+
#[allow(clippy::print_stderr)]
272303
pub fn fail_step(&mut self) {
273304
if let Some(ui) = &mut self.ui {
274305
ui.update_step(self.current_step, StepStatus::Failed);
275306
let _ = ui.render();
307+
} else {
308+
// Non-interactive mode: print failure to stderr
309+
eprintln!(" ✗ Failed");
276310
}
277311
self.current_step += 1;
278312
}
279313

314+
#[allow(clippy::print_stdout)]
280315
pub fn add_completion_message(&mut self, message: &str) {
281316
if let Some(ui) = &mut self.ui {
282317
ui.add_step(message.to_string());
283318
ui.update_step(ui.steps.len() - 1, StepStatus::Completed);
284319
let _ = ui.render();
320+
} else {
321+
// Non-interactive mode: just print the message
322+
println!("\n{}", message);
285323
}
286324
}
287325

@@ -298,79 +336,106 @@ impl InitUIContext {
298336
}
299337
}
300338

339+
#[allow(clippy::print_stderr)]
301340
pub fn show_error(message: &str) -> Result<()> {
302-
enable_raw_mode()?;
303-
let mut stdout = io::stdout();
304-
execute!(stdout, EnterAlternateScreen)?;
305-
let backend = CrosstermBackend::new(stdout);
306-
let mut terminal = Terminal::new(backend)?;
307-
308-
terminal.draw(|f| {
309-
let area = f.area();
310-
311-
// Calculate popup size
312-
let popup_width = message.len().min(60) as u16 + 4;
313-
let popup_height = 7;
314-
315-
let popup_area = Rect {
316-
x: (area.width.saturating_sub(popup_width)) / 2,
317-
y: (area.height.saturating_sub(popup_height)) / 2,
318-
width: popup_width,
319-
height: popup_height,
320-
};
341+
// Check if we're in a TTY before attempting to create a fancy error UI
342+
if !io::stdout().is_terminal() {
343+
// Non-interactive mode: just print the error to stderr
344+
eprintln!("Error: {}", message);
345+
return Ok(());
346+
}
321347

322-
// Clear the area first
323-
f.render_widget(Clear, popup_area);
324-
325-
// Error box
326-
let error_block = Block::default()
327-
.title(" ⚠️ Error ")
328-
.borders(Borders::ALL)
329-
.border_style(Style::default().fg(Color::Red))
330-
.border_type(BorderType::Rounded)
331-
.style(Style::default().bg(Color::Black));
332-
333-
let inner = error_block.inner(popup_area);
334-
f.render_widget(error_block, popup_area);
335-
336-
// Error message
337-
let error_text = vec![
338-
Line::from(""),
339-
Line::from(vec![
340-
Span::raw(" "),
341-
Span::styled(
342-
message,
343-
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
344-
),
345-
]),
346-
Line::from(""),
347-
Line::from(vec![
348-
Span::raw(" "),
349-
Span::styled(
350-
"Press any key to exit",
351-
Style::default()
352-
.fg(Color::Gray)
353-
.add_modifier(Modifier::ITALIC),
354-
),
355-
]),
356-
];
357-
358-
let paragraph = Paragraph::new(error_text)
359-
.alignment(Alignment::Left)
360-
.wrap(Wrap { trim: true });
361-
362-
f.render_widget(paragraph, inner);
363-
})?;
364-
365-
// Wait for user input
366-
loop {
367-
if let Event::Key(_) = event::read()? {
368-
break;
348+
// Try to create the fancy error UI, but fallback gracefully if it fails
349+
match show_error_ui(message) {
350+
Ok(()) => Ok(()),
351+
Err(_) => {
352+
// Failed to create UI, fallback to simple error message
353+
eprintln!("Error: {}", message);
354+
Ok(())
369355
}
370356
}
357+
}
358+
359+
fn show_error_ui(message: &str) -> Result<()> {
360+
enable_raw_mode()?;
361+
362+
// Ensure cleanup happens regardless of success or failure
363+
let result = (|| {
364+
let mut stdout = io::stdout();
365+
execute!(stdout, EnterAlternateScreen)?;
366+
let backend = CrosstermBackend::new(stdout);
367+
let mut terminal = Terminal::new(backend)?;
368+
369+
terminal.draw(|f| {
370+
let area = f.area();
371+
372+
// Calculate popup size
373+
let popup_width = message.len().min(60) as u16 + 4;
374+
let popup_height = 7;
375+
376+
let popup_area = Rect {
377+
x: (area.width.saturating_sub(popup_width)) / 2,
378+
y: (area.height.saturating_sub(popup_height)) / 2,
379+
width: popup_width,
380+
height: popup_height,
381+
};
382+
383+
// Clear the area first
384+
f.render_widget(Clear, popup_area);
385+
386+
// Error box
387+
let error_block = Block::default()
388+
.title(" ⚠️ Error ")
389+
.borders(Borders::ALL)
390+
.border_style(Style::default().fg(Color::Red))
391+
.border_type(BorderType::Rounded)
392+
.style(Style::default().bg(Color::Black));
393+
394+
let inner = error_block.inner(popup_area);
395+
f.render_widget(error_block, popup_area);
396+
397+
// Error message
398+
let error_text = vec![
399+
Line::from(""),
400+
Line::from(vec![
401+
Span::raw(" "),
402+
Span::styled(
403+
message,
404+
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
405+
),
406+
]),
407+
Line::from(""),
408+
Line::from(vec![
409+
Span::raw(" "),
410+
Span::styled(
411+
"Press any key to exit",
412+
Style::default()
413+
.fg(Color::Gray)
414+
.add_modifier(Modifier::ITALIC),
415+
),
416+
]),
417+
];
418+
419+
let paragraph = Paragraph::new(error_text)
420+
.alignment(Alignment::Left)
421+
.wrap(Wrap { trim: true });
422+
423+
f.render_widget(paragraph, inner);
424+
})?;
425+
426+
// Wait for user input
427+
loop {
428+
if let Event::Key(_) = event::read()? {
429+
break;
430+
}
431+
}
432+
433+
Ok(())
434+
})();
371435

372-
disable_raw_mode()?;
373-
execute!(io::stdout(), LeaveAlternateScreen)?;
436+
// Always clean up terminal state
437+
let _ = disable_raw_mode();
438+
let _ = execute!(io::stdout(), LeaveAlternateScreen);
374439

375-
Ok(())
440+
result
376441
}

integ-tests/python/run_tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ uv run pytest "$@" \
2525
--ignore=tests/test_emit.py \
2626
--ignore=tests/test_timeouts.py \
2727
--ignore=tests/test_tracing.py \
28+
--ignore=tests/providers/test_aws_video_request.py \

0 commit comments

Comments
 (0)