Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fb"
version = "0.2.1"
version = "0.2.2"
edition = "2021"
license = "Apache-2.0"

Expand Down
15 changes: 11 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use rustyline::{config::Configurer, error::ReadlineError, Cmd, DefaultEditor, EventHandler, KeyCode, KeyEvent, Modifiers};
use std::io::IsTerminal;

mod args;
mod auth;
Expand Down Expand Up @@ -41,24 +42,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}

let is_tty = std::io::stdout().is_terminal() && std::io::stdin().is_terminal();

let mut rl = DefaultEditor::new()?;
let history_path = history_path()?;
rl.set_max_history_size(10_000)?;
if rl.load_history(&history_path).is_err() {
eprintln!("No previous history");
if is_tty {
eprintln!("No previous history");
}
} else if context.args.verbose {
eprintln!("Loaded history from {:?} and set max_history_size = 10'000", history_path)
}

rl.bind_sequence(KeyEvent(KeyCode::Char('o'), Modifiers::CTRL), EventHandler::Simple(Cmd::Newline));

if !context.args.concise {
if is_tty {
eprintln!("Press Ctrl+D to exit.");
}

let mut buffer: String = String::new();
loop {
let prompt = if !buffer.trim_start().is_empty() {
let prompt = if !is_tty {
// No prompt when stdout is not a terminal (e.g., piped)
""
} else if !buffer.trim_start().is_empty() {
// Continuation prompt (PROMPT2)
if let Some(custom_prompt) = &context.prompt2 {
custom_prompt.as_str()
Expand Down
7 changes: 3 additions & 4 deletions src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,11 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box<

if !context.args.concise {
let elapsed = format!("{:?}", elapsed / 100000 * 100000);
print!("Time: {elapsed}\n");
eprintln!("Time: {elapsed}");
if let Some(request_id) = maybe_request_id {
print!("Request Id: {request_id}\n");
eprintln!("Request Id: {request_id}");
}
// on stdout, on purpose
println!("")
eprintln!("")
}
}
};
Expand Down
14 changes: 7 additions & 7 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fs;
use std::io::stdout;
use std::io::stderr;
use std::io::Write;
use std::path::PathBuf;
use std::time::SystemTime;
Expand Down Expand Up @@ -46,20 +46,20 @@ pub fn format_remaining_time(time: SystemTime, maybe_more: String) -> Result<Str
pub async fn spin(token: CancellationToken) {
let spins = ['─', '\\', '|', '/'];
let mut it = 0;
print!("{}", spins[it]);
eprint!("{}", spins[it]);
it += 1;
let _ = stdout().flush();
let _ = stderr().flush();
loop {
select! {
_ = token.cancelled() => {
print!("\x08 \x08");
let _ = stdout().flush();
eprint!("\x08 \x08");
let _ = stderr().flush();
return;
}
_ = tokio::time::sleep(std::time::Duration::from_millis(200)) => {
print!("\x08{}", spins[it]);
eprint!("\x08{}", spins[it]);
it = (it + 1) % spins.len();
let _ = stdout().flush();
let _ = stderr().flush();
}
};
}
Expand Down
30 changes: 30 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::io::Write;
use std::process::Command;
use serde_json;

fn run_fb(args: &[&str]) -> (bool, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_fb"))
Expand Down Expand Up @@ -205,3 +206,32 @@ fn test_exiting() {
assert_eq!(lines.next().unwrap(), "42");
lines.next();
}

#[test]
fn test_json_output_fully_parseable() {
// Test that JSON output on stdout is fully parseable, even when stats are printed to stderr
let (success, stdout, stderr) = run_fb(&["--core", "-f", "JSONLines_Compact", "SELECT 42 AS value"]);

assert!(success);

// stderr should contain stats
assert!(stderr.contains("Time:"), "stderr should contain Time:");

// stdout should be valid JSON Lines - each non-empty line should be valid JSON
let trimmed_stdout = stdout.trim();
assert!(!trimmed_stdout.is_empty(), "stdout should not be empty");

for line in trimmed_stdout.lines() {
if line.trim().is_empty() {
continue;
}
let parsed: Result<serde_json::Value, _> = serde_json::from_str(line);
assert!(
parsed.is_ok(),
"Each line of stdout should be valid JSON, but got parse error: {:?}\nline was: {}\nfull stdout was: {}",
parsed.err(),
line,
stdout
);
}
}