Beautiful terminal output for database operations, with automatic adaptation between rich formatting for humans and plain text for AI coding agents.
- Introduction
- Quick Start
- Output Modes
- Theme Configuration
- Renderables Catalog
- Integration Patterns
- Agent Compatibility
- Troubleshooting
- API Reference
SQLModel Console transforms database operation output from plain text into beautiful, informative displays:
- Query results as formatted tables with type-based coloring
- Error messages as detailed panels with context and suggestions
- Progress indicators for long-running operations
- Schema visualization as trees and tables
- SQL syntax highlighting for query display
- Connection pool dashboards with health metrics
Use the console when:
- Humans are watching database operations (development, demos)
- You want rich error messages with full context
- Debugging complex queries with visual output
- Monitoring connection pool health
Don't use the console when:
- Running in CI/CD pipelines (auto-detected → plain mode)
- AI agents are parsing output (auto-detected → plain mode)
- You need minimal dependencies
- Output is being piped to files or other programs
Without console:
id|name|email
1|Alice|alice@example.com
2|Bob|bob@example.com
With console (rich mode):
╭────────────── Query Results ── 2 rows in 1.23ms ──────────────╮
│ id │ name │ email │
├────┼───────┼────────────────────┤
│ 1 │ Alice │ alice@example.com │
│ 2 │ Bob │ bob@example.com │
╰───────────────────────────────────────────────────────────────╯
In your Cargo.toml:
[dependencies]
sqlmodel-console = { path = "../crates/sqlmodel-console" }
# Or from crates.io (when published)
# sqlmodel-console = "0.1"use sqlmodel_console::{SqlModelConsole, OutputMode};
fn main() {
// Auto-detect mode (recommended)
let console = SqlModelConsole::new();
// Or force a specific mode
let plain_console = SqlModelConsole::with_mode(OutputMode::Plain);
let rich_console = SqlModelConsole::with_mode(OutputMode::Rich);
}use sqlmodel_console::SqlModelConsole;
use sqlmodel_console::renderables::QueryResultTable;
fn main() {
let console = SqlModelConsole::new();
// Create query results
let table = QueryResultTable::new()
.title("Users")
.columns(vec!["id", "name", "email"])
.row(vec!["1", "Alice", "alice@example.com"])
.row(vec!["2", "Bob", "bob@example.com"])
.timing_ms(1.23);
// Display in the detected mode
if console.is_rich() {
println!("{}", table.render_styled());
} else {
println!("{}", table.render_plain());
}
}use sqlmodel_console::SqlModelConsole;
use sqlmodel_console::renderables::{ErrorPanel, ErrorSeverity};
fn main() {
let console = SqlModelConsole::new();
let error = ErrorPanel::new(
"Connection timeout",
ErrorSeverity::Error
)
.detail("Host", "localhost:5432")
.detail("Timeout", "30 seconds")
.suggestion("Check if the database server is running")
.sql("SELECT * FROM users WHERE id = $1");
// Render based on mode
if console.is_rich() {
eprintln!("{}", error.render_styled());
} else {
eprintln!("{}", error.render_plain());
}
}| Mode | Description | Use Case |
|---|---|---|
| Rich | Colors, tables, panels, box-drawing | Interactive human terminals |
| Plain | No ANSI codes, machine-parseable | AI agents, CI, piped output |
| Json | Structured JSON output | Tool integrations, scripting |
The console automatically detects the appropriate mode:
use sqlmodel_console::OutputMode;
let mode = OutputMode::detect();
match mode {
OutputMode::Rich => println!("Interactive terminal detected"),
OutputMode::Plain => println!("Agent or non-TTY detected"),
OutputMode::Json => println!("JSON mode requested"),
}The detection follows this order (first match wins):
SQLMODEL_PLAIN=1→ Plain modeSQLMODEL_JSON=1→ JSON modeSQLMODEL_RICH=1→ Rich mode (overrides agent detection)NO_COLORpresent → Plain modeCI=true→ Plain modeTERM=dumb→ Plain mode- AI agent environment detected → Plain mode
- stdout is not a TTY → Plain mode
- Default → Rich mode
| Variable | Values | Effect |
|---|---|---|
SQLMODEL_PLAIN |
1, true, yes, on |
Force plain mode |
SQLMODEL_RICH |
1, true, yes, on |
Force rich mode |
SQLMODEL_JSON |
1, true, yes, on |
Force JSON mode |
NO_COLOR |
(any value) | Disable colors |
use sqlmodel_console::{SqlModelConsole, OutputMode};
// Force plain mode (useful for testing)
let console = SqlModelConsole::with_mode(OutputMode::Plain);
assert!(console.is_plain());
// Force rich mode (for demos)
let console = SqlModelConsole::with_mode(OutputMode::Rich);
assert!(console.is_rich());
// Force JSON mode
let console = SqlModelConsole::with_mode(OutputMode::Json);
assert!(console.is_json());use sqlmodel_console::OutputMode;
let mode = OutputMode::detect();
// Check capabilities
if mode.supports_ansi() {
// Use ANSI color codes
}
if mode.is_structured() {
// Output JSON
}
// Get mode as string
println!("Mode: {}", mode.as_str()); // "plain", "rich", or "json"SQLModel Console includes two built-in themes:
use sqlmodel_console::Theme;
// Dark theme (default) - Dracula-inspired colors
let dark = Theme::dark();
// Light theme - Optimized for light backgrounds
let light = Theme::light();The dark theme uses the Dracula color palette:
| Color | Purpose | RGB |
|---|---|---|
| Green | Success, strings | (80, 250, 123) |
| Red | Errors, operators | (255, 85, 85) |
| Yellow | Warnings, booleans | (241, 250, 140) |
| Cyan | Info, numbers | (139, 233, 253) |
| Magenta | Dates, SQL keywords | (255, 121, 198) |
| Purple | JSON, SQL numbers | (189, 147, 249) |
| Gray | Borders, comments, dim | (98, 114, 164) |
Customize any color in the theme:
use sqlmodel_console::Theme;
use sqlmodel_console::theme::ThemeColor;
// Start from a preset
let mut theme = Theme::dark();
// Customize specific colors
theme.success = ThemeColor::new((0, 255, 0), 46); // Brighter green
theme.error = ThemeColor::new((255, 0, 0), 196); // Pure red
theme.header = ThemeColor::new((255, 255, 0), 226); // Yellow headersEach color has RGB and ANSI-256 fallback:
use sqlmodel_console::theme::ThemeColor;
// Basic color
let red = ThemeColor::new((255, 0, 0), 196);
// Color with plain-mode marker (for NULL values, etc.)
let null_color = ThemeColor::with_marker(
(128, 128, 128), // RGB for rich mode
244, // ANSI-256 fallback
"NULL" // Plain mode marker
);use sqlmodel_console::{SqlModelConsole, Theme};
use sqlmodel_console::renderables::QueryResultTable;
// Console with custom theme
let console = SqlModelConsole::with_theme(Theme::light());
// Renderables can also use themes directly
let table = QueryResultTable::new()
.columns(vec!["name"])
.row(vec!["Alice"])
.theme(Theme::dark());use sqlmodel_console::Theme;
let theme = Theme::dark();
// Status colors
let _ = theme.success; // Success messages
let _ = theme.error; // Error messages
let _ = theme.warning; // Warnings
let _ = theme.info; // Informational
// SQL value type colors
let _ = theme.null_value; // NULL
let _ = theme.bool_value; // true/false
let _ = theme.number_value; // 42, 3.14
let _ = theme.string_value; // "text"
let _ = theme.date_value; // 2024-01-15
let _ = theme.binary_value; // [blob]
let _ = theme.json_value; // {"key": "value"}
let _ = theme.uuid_value; // 550e8400-...
// SQL syntax colors
let _ = theme.sql_keyword; // SELECT, FROM
let _ = theme.sql_string; // 'value'
let _ = theme.sql_number; // 42
let _ = theme.sql_comment; // -- comment
let _ = theme.sql_operator; // =, AND
let _ = theme.sql_identifier; // table_name
// UI element colors
let _ = theme.border; // Box borders
let _ = theme.header; // Table headers
let _ = theme.dim; // Secondary text
let _ = theme.highlight; // Emphasized textDisplay query results as formatted tables.
use sqlmodel_console::renderables::{QueryResultTable, PlainFormat};
let table = QueryResultTable::new()
.title("Query Results")
.columns(vec!["id", "name", "email"])
.row(vec!["1", "Alice", "alice@example.com"])
.row(vec!["2", "Bob", "bob@example.com"])
.timing_ms(12.34)
.max_width(80)
.max_rows(100)
.with_row_numbers();
// Styled output (rich mode)
println!("{}", table.render_styled());
// Plain output (default format: pipe-delimited)
println!("{}", table.render_plain());
// Alternative plain formats
println!("{}", table.render_plain_format(PlainFormat::Csv));
println!("{}", table.render_plain_format(PlainFormat::JsonLines));
println!("{}", table.render_plain_format(PlainFormat::JsonArray));Plain Output Formats:
| Format | Example |
|---|---|
| Pipe (default) | id|name|email |
| CSV | id,name,email |
| JSON Lines | {"id":1,"name":"Alice"} |
| JSON Array | [{"id":1,"name":"Alice"}] |
Display errors with context and suggestions.
use sqlmodel_console::renderables::{ErrorPanel, ErrorSeverity};
let error = ErrorPanel::new("Connection failed", ErrorSeverity::Error)
.detail("Host", "localhost:5432")
.detail("Database", "myapp")
.detail("User", "postgres")
.suggestion("Verify database credentials")
.suggestion("Check if PostgreSQL is running")
.sql("SELECT * FROM users WHERE id = $1");
// Styled output
eprintln!("{}", error.render_styled());
// Plain output
eprintln!("{}", error.render_plain());Error Severities:
| Severity | Icon | Usage |
|---|---|---|
Error |
X | Unrecoverable errors |
Warning |
! | Potential issues |
Info |
i | Informational messages |
Show progress for long-running operations.
use sqlmodel_console::renderables::OperationProgress;
// Create progress bar
let mut progress = OperationProgress::new("Inserting rows", 1000);
// Update progress
for i in 0..1000 {
progress.update(i + 1);
println!("\r{}", progress.render_styled());
}
// Mark complete
progress.complete();
println!("\n{}", progress.render_styled());Show activity for operations with unknown duration.
use sqlmodel_console::renderables::{IndeterminateSpinner, SpinnerStyle};
let spinner = IndeterminateSpinner::new("Connecting to database")
.style(SpinnerStyle::Dots);
// Advance animation
for _ in 0..10 {
let next = spinner.next();
println!("\r{}", next.render_styled());
std::thread::sleep(std::time::Duration::from_millis(100));
}Spinner Styles:
| Style | Frames |
|---|---|
Dots |
⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ |
Line |
|/-\ |
Simple |
◐◓◑◒ |
Track bulk operations (inserts, updates, deletes).
use sqlmodel_console::renderables::BatchOperationTracker;
let mut tracker = BatchOperationTracker::new("Bulk insert", 10000);
// Update with batch completions
for batch in 0..100 {
tracker.complete_batch(100); // 100 items per batch
println!("\r{}", tracker.render_styled());
}
tracker.finish();Syntax highlight SQL queries.
use sqlmodel_console::renderables::SqlHighlighter;
use sqlmodel_console::Theme;
let highlighter = SqlHighlighter::with_theme(Theme::dark());
let sql = "SELECT u.name, COUNT(*) FROM users u WHERE u.active = true GROUP BY u.name";
// Highlighted (rich mode)
println!("{}", highlighter.highlight(sql));
// Plain (no colors)
println!("{}", highlighter.plain(sql));
// Formatted with indentation
println!("{}", highlighter.format(sql));Display query execution time.
use sqlmodel_console::renderables::QueryTiming;
use std::time::Duration;
let timing = QueryTiming::new(Duration::from_millis(123));
println!("{}", timing.render_styled()); // "123.00ms"
println!("{}", timing.render_plain()); // "123.00ms"Show connection pool health.
use sqlmodel_console::renderables::{PoolStatusDisplay, PoolStatsProvider, PoolHealth};
// Implement PoolStatsProvider for your pool type
struct MyPoolStats {
total: usize,
active: usize,
idle: usize,
}
impl PoolStatsProvider for MyPoolStats {
fn total_connections(&self) -> usize { self.total }
fn active_connections(&self) -> usize { self.active }
fn idle_connections(&self) -> usize { self.idle }
fn health(&self) -> PoolHealth {
if self.idle > 0 { PoolHealth::Healthy }
else if self.active < self.total { PoolHealth::Degraded }
else { PoolHealth::Critical }
}
}
let stats = MyPoolStats { total: 10, active: 3, idle: 7 };
let display = PoolStatusDisplay::from_provider(&stats);
println!("{}", display.render_styled());Attach a console to each database session:
use sqlmodel_console::{SqlModelConsole, Theme};
struct Session {
console: SqlModelConsole,
// ... connection fields
}
impl Session {
pub fn new() -> Self {
Self {
console: SqlModelConsole::new(),
}
}
pub fn new_with_theme(theme: Theme) -> Self {
Self {
console: SqlModelConsole::with_theme(theme),
}
}
}Share a single console instance:
use sqlmodel_console::SqlModelConsole;
use std::sync::LazyLock;
static CONSOLE: LazyLock<SqlModelConsole> = LazyLock::new(SqlModelConsole::new);
fn main() {
CONSOLE.print("Hello from global console");
CONSOLE.success("Operation completed");
}Write functions that adapt to the output mode:
use sqlmodel_console::{SqlModelConsole, OutputMode};
use sqlmodel_console::renderables::QueryResultTable;
fn display_results(console: &SqlModelConsole, table: &QueryResultTable) {
match console.mode() {
OutputMode::Rich => {
println!("{}", table.render_styled());
}
OutputMode::Plain => {
println!("{}", table.render_plain());
}
OutputMode::Json => {
println!("{}", serde_json::to_string(&table.to_json()).unwrap());
}
}
}Keep semantic data on stdout, status on stderr:
use sqlmodel_console::SqlModelConsole;
let console = SqlModelConsole::new();
// Status messages → stderr (for humans)
console.status("Connecting to database...");
console.success("Connected!");
// Data → stdout (for agents to parse)
console.print("id|name|email");
console.print("1|Alice|alice@example.com");
// Errors → stderr
console.error("Query failed");For detailed information about AI agent compatibility, see the Agent Compatibility Guide.
- Auto-detection: Agents are detected via environment variables
- Stream separation: stdout = data, stderr = status
- Plain mode: No ANSI codes, parseable formats
- Override: Use
SQLMODEL_RICH=1to force rich mode
use sqlmodel_console::OutputMode;
if OutputMode::is_agent_environment() {
println!("Running under an AI coding agent");
}- Claude Code (
CLAUDE_CODE) - Codex CLI (
CODEX_CLI) - Cursor IDE (
CURSOR_SESSION) - Aider (
AIDER_MODEL) - GitHub Copilot (
GITHUB_COPILOT) - Continue.dev (
CONTINUE_SESSION) - And more...
Symptoms: Output appears as plain text even in terminal.
Causes:
- Running under detected AI agent
- Output is piped/redirected
NO_COLORenvironment variable is setTERM=dumb
Solutions:
# Force rich mode
SQLMODEL_RICH=1 cargo run
# Check what mode is detected
cargo run --example mode_checkSymptoms: See escape sequences like ^[[32m in output.
Causes:
- Agent environment not detected
- Rich mode forced in non-terminal context
Solutions:
# Force plain mode
SQLMODEL_PLAIN=1 cargo run
# Or set generic agent marker
AGENT_MODE=1 cargo runSymptoms: Status messages mixed with data.
Cause: Using print() for status messages.
Solution: Use the appropriate method:
// Data → stdout
console.print("id|name");
// Status → stderr
console.status("Processing...");
console.success("Done!");
console.error("Failed!");Symptoms: Slow output, especially with large result sets.
Solutions:
- Use
max_rows()to limit displayed rows - Use plain mode for bulk data
- Render once instead of per-row
let table = QueryResultTable::new()
.columns(cols)
.rows(all_rows) // Add all at once
.max_rows(100); // Limit display// Console management
pub struct SqlModelConsole { ... }
pub enum OutputMode { Plain, Rich, Json }
pub struct Theme { ... }
pub struct ThemeColor { ... }
// Traits
pub trait ConsoleAware { ... }
pub trait PoolStatsProvider { ... }impl SqlModelConsole {
// Creation
pub fn new() -> Self;
pub fn with_mode(mode: OutputMode) -> Self;
pub fn with_theme(theme: Theme) -> Self;
// Mode queries
pub fn mode(&self) -> OutputMode;
pub fn is_rich(&self) -> bool;
pub fn is_plain(&self) -> bool;
pub fn is_json(&self) -> bool;
// stdout output
pub fn print(&self, message: &str);
pub fn print_raw(&self, message: &str);
pub fn print_json<T: Serialize>(&self, value: &T) -> Result<...>;
// stderr output
pub fn status(&self, message: &str);
pub fn success(&self, message: &str);
pub fn error(&self, message: &str);
pub fn warning(&self, message: &str);
pub fn info(&self, message: &str);
pub fn rule(&self, title: Option<&str>);
}impl OutputMode {
pub fn detect() -> Self;
pub fn is_agent_environment() -> bool;
pub fn supports_ansi(&self) -> bool;
pub fn is_structured(&self) -> bool;
pub fn is_plain(&self) -> bool;
pub fn is_rich(&self) -> bool;
pub fn as_str(&self) -> &'static str;
}All renderables follow this pattern:
impl Renderable {
pub fn new(...) -> Self; // Create
pub fn render_styled(&self) -> String; // Rich output
pub fn render_plain(&self) -> String; // Plain output
}- Read the Agent Compatibility Guide for AI agent details
- Explore the
examples/directory for more code samples - Check the API documentation with
cargo doc --open