use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph, Wrap};
use super::app::{App, FIELD_DESCRIPTIONS, FIELD_LABELS, Screen, TestOutcome};
pub fn draw(frame: &mut Frame, app: &App) {
let area = frame.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(3),
])
.split(area);
draw_header(frame, app, chunks[0]);
match app.screen {
Screen::ProfileList => draw_profile_list(frame, app, chunks[1]),
Screen::ProfileForm => draw_profile_form(frame, app, chunks[1]),
Screen::Testing => draw_testing(frame, app, chunks[1]),
Screen::SaveConfirm => draw_save_confirm(frame, app, chunks[1]),
Screen::ConfirmDelete => draw_confirm_delete(frame, app, chunks[1]),
Screen::ClientPicker => draw_client_picker(frame, app, chunks[1]),
Screen::DiffReview => draw_diff_review(frame, app, chunks[1]),
Screen::Summary => draw_summary(frame, app, chunks[1]),
}
draw_status_line(frame, app, chunks[2]);
}
fn draw_header(frame: &mut Frame, app: &App, area: Rect) {
let total_profiles = app.profile_names.len();
let default_name = app
.config
.default_profile
.as_deref()
.unwrap_or("none");
let header_lines = vec![Line::from(vec![
Span::styled(" ⚡ NexQL MCP ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled("Setup & Profile Manager", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" │ ", Style::default().fg(Color::DarkGray)),
Span::styled("Config: ", Style::default().fg(Color::DarkGray)),
Span::styled(app.config_path.display().to_string(), Style::default().fg(Color::White)),
Span::styled(" │ ", Style::default().fg(Color::DarkGray)),
Span::styled("Profiles: ", Style::default().fg(Color::DarkGray)),
Span::styled(format!("{total_profiles}"), Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" │ ", Style::default().fg(Color::DarkGray)),
Span::styled("Default: ", Style::default().fg(Color::DarkGray)),
Span::styled(default_name, Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)),
])];
let header_widget = Paragraph::new(header_lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::DarkGray)),
);
frame.render_widget(header_widget, area);
}
fn draw_status_line(frame: &mut Frame, app: &App, area: Rect) {
let line = if let Some(msg) = &app.status {
Line::from(vec![
Span::styled(" ℹ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(msg, Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
])
} else {
match app.screen {
Screen::ProfileList => Line::from(vec![
Span::styled(" [Space]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Toggle ", Style::default().fg(Color::White)),
Span::styled("[N]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" New ", Style::default().fg(Color::White)),
Span::styled("[E/Enter]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Edit ", Style::default().fg(Color::White)),
Span::styled("[T]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Test ", Style::default().fg(Color::White)),
Span::styled("[W]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Wire Clients ", Style::default().fg(Color::White)),
Span::styled("[S]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Default ", Style::default().fg(Color::White)),
Span::styled("[D]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" Del ", Style::default().fg(Color::White)),
Span::styled("[Q]", Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)),
Span::styled(" Quit", Style::default().fg(Color::White)),
]),
Screen::ProfileForm => Line::from(vec![
Span::styled(" [Tab/Shift+Tab]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Move ", Style::default().fg(Color::White)),
Span::styled("[Type]", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(" Edit ", Style::default().fg(Color::White)),
Span::styled("[Enter]", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" Test & Continue ", Style::default().fg(Color::White)),
Span::styled("[Esc]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" Cancel", Style::default().fg(Color::White)),
]),
Screen::Testing => Line::from(vec![
Span::styled(" [Enter]", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" Continue ", Style::default().fg(Color::White)),
Span::styled("[R]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Retry ", Style::default().fg(Color::White)),
Span::styled("[Esc]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" Back", Style::default().fg(Color::White)),
]),
Screen::SaveConfirm => Line::from(vec![
Span::styled(" [Y]", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" Save Profile ", Style::default().fg(Color::White)),
Span::styled("[N/Esc]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" Back to Form", Style::default().fg(Color::White)),
]),
Screen::ConfirmDelete => Line::from(vec![
Span::styled(" [Y]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" Confirm Delete ", Style::default().fg(Color::White)),
Span::styled("[N/Esc]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Cancel", Style::default().fg(Color::White)),
]),
Screen::ClientPicker => Line::from(vec![
Span::styled(" [Space]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Toggle ", Style::default().fg(Color::White)),
Span::styled("[A]", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(" Select All ", Style::default().fg(Color::White)),
Span::styled("[Enter]", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" Review Diffs ", Style::default().fg(Color::White)),
Span::styled("[Esc]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" Back", Style::default().fg(Color::White)),
]),
Screen::DiffReview => Line::from(vec![
Span::styled(" [Y]", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" Apply ", Style::default().fg(Color::White)),
Span::styled("[S]", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(" Skip ", Style::default().fg(Color::White)),
Span::styled("[A]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Apply All ", Style::default().fg(Color::White)),
Span::styled("[Esc]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" Cancel", Style::default().fg(Color::White)),
]),
Screen::Summary => Line::from(vec![
Span::styled(" [Enter/Esc]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" Back to Profile Manager", Style::default().fg(Color::White)),
]),
}
};
let footer_widget = Paragraph::new(line).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::DarkGray)),
);
frame.render_widget(footer_widget, area);
}
fn draw_profile_list(frame: &mut Frame, app: &App, area: Rect) {
let panels = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(42), Constraint::Percentage(58)])
.split(area);
let items: Vec<ListItem> = if app.profile_names.is_empty() {
vec![ListItem::new(" No profiles yet — press 'n' to create one")]
} else {
app.profile_names
.iter()
.map(|name| {
let profile = &app.config.profiles[name];
let is_default = app.config.default_profile.as_deref() == Some(name.as_str());
let is_checked = app.checked_profiles.contains(name);
let check_badge = if is_checked { "[x] " } else { "[ ] " };
let check_style = if is_checked {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::DarkGray)
};
let prefix = if is_default { "★ " } else { " " };
let access = profile.access_mode.as_deref().unwrap_or("read");
let access_tag = match access {
"admin" => "[ADMIN]",
"write" => "[WRITE]",
_ => "[READ]",
};
let title_line = Line::from(vec![
Span::styled(prefix, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(check_badge, check_style),
Span::styled(name, Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled(access_tag, Style::default().fg(Color::Cyan)),
]);
ListItem::new(title_line)
})
.collect()
};
let mut state = ListState::default();
if !app.profile_names.is_empty() {
state.select(Some(app.selected));
}
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(format!(" Profiles ({}) ", app.profile_names.len())),
)
.highlight_style(
Style::default()
.bg(Color::Cyan)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
);
frame.render_stateful_widget(list, panels[0], &mut state);
draw_profile_inspector(frame, app, panels[1]);
}
fn draw_profile_inspector(frame: &mut Frame, app: &App, area: Rect) {
let inspector_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Cyan))
.title(" Profile Details Inspector ");
let Some(selected_name) = app.profile_names.get(app.selected) else {
let empty_msg = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled(" No profile selected.", Style::default().fg(Color::DarkGray))),
Line::from(""),
Line::from(Span::styled(" Press 'n' to launch the connection setup wizard.", Style::default().fg(Color::Yellow))),
])
.block(inspector_block);
frame.render_widget(empty_msg, area);
return;
};
let Some(profile) = app.config.profiles.get(selected_name) else {
return;
};
let is_default = app.config.default_profile.as_deref() == Some(selected_name.as_str());
let mut lines = Vec::new();
lines.push(Line::from(vec![
Span::styled(" Profile Name: ", Style::default().fg(Color::DarkGray)),
Span::styled(selected_name, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
if is_default {
Span::styled(" ★ (Default)", Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD))
} else {
Span::raw("")
},
]));
lines.push(Line::from(""));
let access = profile.access_mode.as_deref().unwrap_or("read");
let (access_label, access_color) = match access {
"admin" => ("ADMIN (Full DDL & Admin)", Color::Red),
"write" => ("WRITE (Read & Data Mutations)", Color::Yellow),
_ => ("READ-ONLY (Safe Querying)", Color::Green),
};
lines.push(Line::from(vec![
Span::styled(" Access Policy: ", Style::default().fg(Color::DarkGray)),
Span::styled(access_label, Style::default().fg(access_color).add_modifier(Modifier::BOLD)),
]));
if let Some(url) = &profile.url {
lines.push(Line::from(vec![
Span::styled(" Connection URL: ", Style::default().fg(Color::DarkGray)),
Span::styled(url, Style::default().fg(Color::Cyan)),
]));
} else {
lines.push(Line::from(vec![
Span::styled(" Host & Port: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!(
"{}:{}",
profile.host.as_deref().unwrap_or("127.0.0.1"),
profile.port.unwrap_or(5432)
),
Style::default().fg(Color::White),
),
]));
lines.push(Line::from(vec![
Span::styled(" Database Name: ", Style::default().fg(Color::DarkGray)),
Span::styled(
profile.dbname.as_deref().unwrap_or("(not set)"),
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![
Span::styled(" Database User: ", Style::default().fg(Color::DarkGray)),
Span::styled(
profile.user.as_deref().unwrap_or("(not set)"),
Style::default().fg(Color::White),
),
]));
}
lines.push(Line::from(vec![
Span::styled(" SSL Mode: ", Style::default().fg(Color::DarkGray)),
Span::styled(
profile.sslmode.as_deref().unwrap_or("prefer"),
Style::default().fg(Color::White),
),
]));
let pass_info = if profile.password.is_some() {
"●●●●●●●● (Stored in config)"
} else if let Some(cmd) = &profile.password_command {
cmd.as_str()
} else {
"None"
};
lines.push(Line::from(vec![
Span::styled(" Password Auth: ", Style::default().fg(Color::DarkGray)),
Span::styled(pass_info, Style::default().fg(Color::White)),
]));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(" Quick Actions:", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))));
lines.push(Line::from(Span::styled(" • Press [E] to modify connection fields", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(" • Press [T] to test database connection live", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(" • Press [W] to auto-wire into Cursor, Claude, Zed, VS Code", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(" • Press [S] to set as default profile", Style::default().fg(Color::DarkGray))));
let paragraph = Paragraph::new(lines).block(inspector_block);
frame.render_widget(paragraph, area);
}
fn draw_profile_form(frame: &mut Frame, app: &App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(12), Constraint::Length(4)])
.split(area);
let title = if app.form.name_editable {
" New Profile — Connection Wizard "
} else {
" Edit Profile "
};
let mut lines = Vec::with_capacity(FIELD_LABELS.len() + 3);
let name_focused = app.form.focused == 0;
let name_prefix = if name_focused { " ▶ " } else { " " };
let name_style = if name_focused {
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
lines.push(Line::from(vec![
Span::styled(name_prefix, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled("Profile Name: ", Style::default().fg(if name_focused { Color::Cyan } else { Color::DarkGray })),
Span::styled(&app.form.name, name_style),
if !app.form.name_editable {
Span::styled(" (read-only)", Style::default().fg(Color::DarkGray))
} else {
Span::raw("")
},
]));
lines.push(Line::from(""));
for (i, label) in FIELD_LABELS.iter().enumerate() {
let focused = app.form.focused == i + 1;
let prefix = if focused { " ▶ " } else { " " };
let label_style = Style::default().fg(if focused { Color::Cyan } else { Color::DarkGray });
let val_style = if focused {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
let val = &app.form.fields[i];
let display_val = if val.is_empty() {
match i {
1 => "(e.g., 127.0.0.1)",
2 => "(default: 5432)",
7 => "(default: prefer)",
8 => "(default: read)",
_ => "(optional)",
}
} else if i == 5 {
"●●●●●●●●"
} else {
val.as_str()
};
let val_span_style = if val.is_empty() {
Style::default().fg(Color::DarkGray)
} else {
val_style
};
lines.push(Line::from(vec![
Span::styled(prefix, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(format!("{label:<32} "), label_style),
Span::styled(display_val, val_span_style),
]));
}
let form_widget = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Cyan))
.title(title),
);
frame.render_widget(form_widget, chunks[0]);
let guide_idx = app.form.focused.min(FIELD_DESCRIPTIONS.len() - 1);
let help_text = FIELD_DESCRIPTIONS[guide_idx];
let help_widget = Paragraph::new(vec![
Line::from(vec![
Span::styled(" 💡 ", Style::default().fg(Color::Yellow)),
Span::styled(help_text, Style::default().fg(Color::White)),
])
])
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Yellow))
.title(" Field Guide "),
);
frame.render_widget(help_widget, chunks[1]);
let (label_len, text_len) = if app.form.focused == 0 {
(" Profile Name: ".len(), app.form.name.chars().count())
} else {
let i = app.form.focused - 1;
(
format!(" {:<32} ", FIELD_LABELS[i]).len(),
app.form.fields[i].chars().count(),
)
};
let line_idx = if app.form.focused == 0 {
0
} else {
app.form.focused + 1
};
let cursor_x = chunks[0].x + 1 + label_len as u16 + text_len as u16;
let cursor_y = chunks[0].y + 1 + line_idx as u16;
frame.set_cursor_position((cursor_x, cursor_y));
}
fn draw_testing(frame: &mut Frame, app: &App, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(" Connection Diagnostic Tester ");
let mut lines = Vec::new();
lines.push(Line::from(""));
match &app.test_outcome {
TestOutcome::Idle => {
lines.push(Line::from(Span::styled(" Ready to initiate connection test.", Style::default().fg(Color::DarkGray))));
}
TestOutcome::Running => {
lines.push(Line::from(vec![
Span::styled(" ⏳ ", Style::default().fg(Color::Yellow)),
Span::styled("Initiating PostgreSQL connection handshake...", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(" Resolving profile credentials and validating socket/TLS parameters...", Style::default().fg(Color::DarkGray))));
}
TestOutcome::Done(Ok(report)) => {
lines.push(Line::from(vec![
Span::styled(" ✔ ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled("CONNECTION SUCCESSFUL", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" • Handshake Latency: ", Style::default().fg(Color::DarkGray)),
Span::styled(format!("{:.1} ms", report.latency.as_secs_f64() * 1000.0), Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(vec![
Span::styled(" • Server Version: ", Style::default().fg(Color::DarkGray)),
Span::styled(&report.server_version, Style::default().fg(Color::White)),
]));
lines.push(Line::from(vec![
Span::styled(" • Superuser Status: ", Style::default().fg(Color::DarkGray)),
Span::styled(
if report.is_superuser { "Yes (Warning: elevated permissions)" } else { "No (Standard user)" },
Style::default().fg(if report.is_superuser { Color::Yellow } else { Color::Green }),
),
]));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(" Press [Enter] to proceed and save profile.", Style::default().fg(Color::Yellow))));
}
TestOutcome::Done(Err(e)) => {
lines.push(Line::from(vec![
Span::styled(" ✖ ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled("CONNECTION FAILED", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Error Message: ", Style::default().fg(Color::DarkGray)),
Span::styled(e, Style::default().fg(Color::Red)),
]));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(" Troubleshooting Tips:", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))));
lines.push(Line::from(Span::styled(" • Verify host & port are reachable from this machine.", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(" • Check database name, username, and password.", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(" • Ensure pg_hba.conf allows connections for this user.", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(" Press [R] to retry, or [Esc] to return to profile editor.", Style::default().fg(Color::Yellow))));
}
}
let widget = Paragraph::new(lines).block(block).wrap(Wrap { trim: false });
frame.render_widget(widget, area);
}
fn draw_save_confirm(frame: &mut Frame, app: &App, area: Rect) {
let rendered = app
.pending_profile
.as_ref()
.and_then(|p| toml::to_string_pretty(p).ok())
.unwrap_or_default();
let mut lines = Vec::new();
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Save profile ", Style::default().fg(Color::White)),
Span::styled(format!("'{}'", app.form.name), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(" to ", Style::default().fg(Color::White)),
Span::styled(app.config_path.display().to_string(), Style::default().fg(Color::Cyan)),
Span::styled("?", Style::default().fg(Color::White)),
]));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(" TOML Profile Configuration Preview:", Style::default().fg(Color::DarkGray))));
for line in rendered.lines() {
lines.push(Line::from(Span::styled(format!(" {line}"), Style::default().fg(Color::Green))));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Press ", Style::default().fg(Color::DarkGray)),
Span::styled("[Y / Enter]", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(" to confirm save, or ", Style::default().fg(Color::DarkGray)),
Span::styled("[N / Esc]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" to back to form.", Style::default().fg(Color::DarkGray)),
]));
let widget = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(" Save Profile Confirmation "),
);
frame.render_widget(widget, area);
}
fn draw_confirm_delete(frame: &mut Frame, app: &App, area: Rect) {
let name = app
.profile_names
.get(app.selected)
.cloned()
.unwrap_or_default();
let lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(" ⚠️ Are you sure you want to delete profile ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(format!("'{name}'"), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled("?", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
]),
Line::from(""),
Line::from(Span::styled(" This operation will remove the profile from configuration and cannot be undone.", Style::default().fg(Color::DarkGray))),
Line::from(""),
Line::from(vec![
Span::styled(" Press ", Style::default().fg(Color::DarkGray)),
Span::styled("[Y]", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(" to confirm delete, or ", Style::default().fg(Color::DarkGray)),
Span::styled("[N / Esc]", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" to cancel.", Style::default().fg(Color::DarkGray)),
]),
];
let widget = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Red))
.title(" Confirm Delete "),
);
frame.render_widget(widget, area);
}
fn draw_client_picker(frame: &mut Frame, app: &App, area: Rect) {
let items: Vec<ListItem> = app
.picker_items
.iter()
.map(|item| {
let checkbox = if item.selected { "[✓]" } else { "[ ]" };
let (kind_tag, kind_color) = if item.mergeable {
("[AUTO-MERGE]", Color::Green)
} else {
("[COPY-SNIPPET]", Color::Yellow)
};
let line = Line::from(vec![
Span::styled(
format!("{checkbox} "),
Style::default().fg(if item.selected { Color::Green } else { Color::DarkGray }),
),
Span::styled(format!("{:<30} ", item.display_name), Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
Span::styled(kind_tag, Style::default().fg(kind_color)),
]);
ListItem::new(line)
})
.collect();
let mut state = ListState::default();
state.select(Some(app.picker_idx));
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(" Wire Profile into AI Clients & Coding Tools "),
)
.highlight_style(
Style::default()
.bg(Color::Cyan)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
);
frame.render_stateful_widget(list, area, &mut state);
}
fn draw_diff_review(frame: &mut Frame, app: &App, area: Rect) {
let Some(entry) = app.diffs.get(app.diff_idx) else {
frame.render_widget(Paragraph::new("No configuration diffs to review."), area);
return;
};
let diff_raw = app.current_diff_text().unwrap_or_default();
let mut lines = Vec::new();
lines.push(Line::from(vec![
Span::styled(" Reviewing Diff ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(format!("({} of {}): ", app.diff_idx + 1, app.diffs.len()), Style::default().fg(Color::DarkGray)),
Span::styled(entry.display_name, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(vec![
Span::styled(" Target File: ", Style::default().fg(Color::DarkGray)),
Span::styled(entry.config_path.display().to_string(), Style::default().fg(Color::White)),
]));
lines.push(Line::from(""));
for line in diff_raw.lines() {
if line.starts_with('+') && !line.starts_with("+++") {
lines.push(Line::from(Span::styled(line, Style::default().fg(Color::Green))));
} else if line.starts_with('-') && !line.starts_with("---") {
lines.push(Line::from(Span::styled(line, Style::default().fg(Color::Red))));
} else if line.starts_with("@@") {
lines.push(Line::from(Span::styled(line, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))));
} else if line.starts_with("diff") || line.starts_with("---") || line.starts_with("+++") {
lines.push(Line::from(Span::styled(line, Style::default().fg(Color::DarkGray))));
} else {
lines.push(Line::from(Span::styled(line, Style::default().fg(Color::White))));
}
}
let widget = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(" Configuration Diff Inspector "),
);
frame.render_widget(widget, area);
}
fn draw_summary(frame: &mut Frame, app: &App, area: Rect) {
let mut lines = Vec::new();
lines.push(Line::from(""));
for entry in &app.summary {
if let Some(err) = &entry.error {
lines.push(Line::from(vec![
Span::styled(" ✖ ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::styled(format!("{}: ", entry.display_name), Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
Span::styled(err, Style::default().fg(Color::Red)),
]));
} else if entry.skipped {
lines.push(Line::from(vec![
Span::styled(" ➖ ", Style::default().fg(Color::DarkGray)),
Span::styled(format!("{} (skipped)", entry.display_name), Style::default().fg(Color::DarkGray)),
]));
} else if let Some(path) = &entry.path {
let backup_note = entry
.backup
.as_ref()
.map(|b| format!(" (backup: {})", b.display()))
.unwrap_or_default();
lines.push(Line::from(vec![
Span::styled(" ✔ ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(format!("{}: ", entry.display_name), Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
Span::styled(format!("Updated {}", path.display()), Style::default().fg(Color::Green)),
Span::styled(backup_note, Style::default().fg(Color::DarkGray)),
]));
} else if let Some(snippet) = &entry.snippet {
lines.push(Line::from(vec![
Span::styled(" 📋 ", Style::default().fg(Color::Yellow)),
Span::styled(format!("{} (Copy snippet below):", entry.display_name), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
]));
for line in snippet.lines() {
lines.push(Line::from(Span::styled(format!(" {line}"), Style::default().fg(Color::Cyan))));
}
}
lines.push(Line::from(""));
}
if lines.is_empty() {
lines.push(Line::from(Span::styled(" No actions performed.", Style::default().fg(Color::DarkGray))));
}
lines.push(Line::from(Span::styled(" ✨ Client integration complete! You can now use NexQL MCP in your configured AI tools.", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))));
let widget = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(" Integration Summary "),
);
frame.render_widget(widget, area);
}