Add CSV import functionality #1
227
src/main.rs
227
src/main.rs
@ -295,28 +295,10 @@ fn draw(f: &mut Frame, app: &App) {
|
|||||||
draw_title(f, main_chunks[0], app);
|
draw_title(f, main_chunks[0], app);
|
||||||
draw_content(f, main_chunks[1], app);
|
draw_content(f, main_chunks[1], app);
|
||||||
draw_tile_bar(f, main_chunks[2], app);
|
draw_tile_bar(f, main_chunks[2], app);
|
||||||
|
draw_bottom_bar(f, main_chunks[3], app, is_cmd_mode);
|
||||||
if is_cmd_mode {
|
|
||||||
draw_command_bar(f, main_chunks[3], app);
|
|
||||||
} else {
|
|
||||||
draw_status(f, main_chunks[3], app);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overlays (rendered last so they appear on top)
|
// Overlays (rendered last so they appear on top)
|
||||||
if matches!(app.mode, AppMode::Help) {
|
draw_overlays(f, main_chunks[1], app);
|
||||||
f.render_widget(HelpWidget, size);
|
|
||||||
}
|
|
||||||
if matches!(app.mode, AppMode::ImportWizard) {
|
|
||||||
if let Some(wizard) = &app.wizard {
|
|
||||||
f.render_widget(ImportWizardWidget::new(wizard), size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if matches!(app.mode, AppMode::ExportPrompt { .. }) {
|
|
||||||
draw_export_prompt(f, size, app);
|
|
||||||
}
|
|
||||||
if app.is_empty_model() && matches!(app.mode, AppMode::Normal | AppMode::CommandMode { .. }) {
|
|
||||||
draw_welcome(f, main_chunks[1], app);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
||||||
@ -330,17 +312,8 @@ fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let title = format!(" improvise · {}{}{} ", app.model.name, file, dirty);
|
let title = format!(" improvise · {}{}{} ", app.model.name, file, dirty);
|
||||||
let right = " ?:help :q quit ";
|
let right = " ?:help :q quit ";
|
||||||
let pad = " ".repeat((area.width as usize).saturating_sub(title.len() + right.len()));
|
let line = fill_line(title, right, area.width);
|
||||||
let line = format!("{title}{pad}{right}");
|
f.render_widget(Paragraph::new(line).style(title_bar_style()), area);
|
||||||
f.render_widget(
|
|
||||||
Paragraph::new(line).style(
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::Black)
|
|
||||||
.bg(Color::Blue)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
||||||
@ -406,51 +379,22 @@ fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_status(f: &mut Frame, area: Rect, app: &App) {
|
||||||
let mode_badge = match &app.mode {
|
let mode_badge = mode_badge_text(&app.mode);
|
||||||
AppMode::Normal => "NORMAL",
|
let search_part = search_indicator(&app.search_query, app.search_mode);
|
||||||
AppMode::Editing { .. } => "INSERT",
|
|
||||||
AppMode::FormulaEdit { .. } => "FORMULA",
|
|
||||||
AppMode::FormulaPanel => "FORMULAS",
|
|
||||||
AppMode::CategoryPanel => "CATEGORIES",
|
|
||||||
AppMode::CategoryAdd { .. } => "NEW CATEGORY",
|
|
||||||
AppMode::ItemAdd { .. } => "ADD ITEMS",
|
|
||||||
AppMode::ViewPanel => "VIEWS",
|
|
||||||
AppMode::TileSelect { .. } => "TILES",
|
|
||||||
AppMode::ImportWizard => "IMPORT",
|
|
||||||
AppMode::ExportPrompt { .. } => "EXPORT",
|
|
||||||
AppMode::CommandMode { .. } => "COMMAND",
|
|
||||||
AppMode::Help => "HELP",
|
|
||||||
AppMode::Quit => "QUIT",
|
|
||||||
};
|
|
||||||
|
|
||||||
let search_part = if app.search_mode {
|
|
||||||
format!(" /{}▌", app.search_query)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let msg = if !app.status_msg.is_empty() {
|
let msg = if !app.status_msg.is_empty() {
|
||||||
app.status_msg.as_str()
|
app.status_msg.as_str()
|
||||||
} else {
|
} else {
|
||||||
app.hint_text()
|
app.hint_text()
|
||||||
};
|
};
|
||||||
|
|
||||||
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
|
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
|
||||||
let view_badge = format!(" {}{} ", app.model.active_view, yank_indicator);
|
let view_badge = format!(" {}{} ", app.model.active_view, yank_indicator);
|
||||||
|
|
||||||
let left = format!(" {mode_badge}{search_part} {msg}");
|
let left = format!(" {mode_badge}{search_part} {msg}");
|
||||||
let right = view_badge;
|
let line = fill_line(left, &view_badge, area.width);
|
||||||
let pad = " ".repeat((area.width as usize).saturating_sub(left.len() + right.len()));
|
f.render_widget(
|
||||||
let line = format!("{left}{pad}{right}");
|
Paragraph::new(line).style(status_bar_style(&app.mode)),
|
||||||
|
area,
|
||||||
let badge_style = match &app.mode {
|
);
|
||||||
AppMode::Editing { .. } => Style::default().fg(Color::Black).bg(Color::Green),
|
|
||||||
AppMode::CommandMode { .. } => Style::default().fg(Color::Black).bg(Color::Yellow),
|
|
||||||
AppMode::TileSelect { .. } => Style::default().fg(Color::Black).bg(Color::Magenta),
|
|
||||||
_ => Style::default().fg(Color::Black).bg(Color::DarkGray),
|
|
||||||
};
|
|
||||||
|
|
||||||
f.render_widget(Paragraph::new(line).style(badge_style), area);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_command_bar(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_command_bar(f: &mut Frame, area: Rect, app: &App) {
|
||||||
@ -460,10 +404,7 @@ fn draw_command_bar(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
""
|
""
|
||||||
};
|
};
|
||||||
let line = format!(":{buf}▌");
|
let line = format!(":{buf}▌");
|
||||||
f.render_widget(
|
f.render_widget(Paragraph::new(line).style(command_bar_style()), area);
|
||||||
Paragraph::new(line).style(Style::default().fg(Color::White).bg(Color::Black)),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) {
|
||||||
@ -472,18 +413,15 @@ fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
let popup_w = 64u16.min(area.width);
|
let popup = centered_popup(area, 64, 3);
|
||||||
let x = area.x + area.width.saturating_sub(popup_w) / 2;
|
f.render_widget(Clear, popup);
|
||||||
let y = area.y + area.height / 2;
|
|
||||||
let popup_area = Rect::new(x, y, popup_w, 3);
|
|
||||||
|
|
||||||
f.render_widget(Clear, popup_area);
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Yellow))
|
.border_style(Style::default().fg(Color::Yellow))
|
||||||
.title(" Export CSV — path (Esc cancel) ");
|
.title(" Export CSV — path (Esc cancel) ");
|
||||||
let inner = block.inner(popup_area);
|
let inner = block.inner(popup);
|
||||||
f.render_widget(block, popup_area);
|
f.render_widget(block, popup);
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(format!("{buf}▌")).style(Style::default().fg(Color::Green)),
|
Paragraph::new(format!("{buf}▌")).style(Style::default().fg(Color::Green)),
|
||||||
inner,
|
inner,
|
||||||
@ -491,12 +429,7 @@ fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) {
|
fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) {
|
||||||
let w = 58u16.min(area.width.saturating_sub(4));
|
let popup = centered_popup(area, 58, 20);
|
||||||
let h = 20u16.min(area.height.saturating_sub(2));
|
|
||||||
let x = area.x + area.width.saturating_sub(w) / 2;
|
|
||||||
let y = area.y + area.height.saturating_sub(h) / 2;
|
|
||||||
let popup = Rect::new(x, y, w, h);
|
|
||||||
|
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
@ -506,7 +439,35 @@ fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) {
|
|||||||
let inner = block.inner(popup);
|
let inner = block.inner(popup);
|
||||||
f.render_widget(block, popup);
|
f.render_widget(block, popup);
|
||||||
|
|
||||||
let lines: &[(&str, Style)] = &[
|
let lines = welcome_lines();
|
||||||
|
for (i, (text, style)) in lines.iter().enumerate() {
|
||||||
|
if i >= inner.height as usize {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(*text).style(*style),
|
||||||
|
Rect::new(
|
||||||
|
inner.x + 1,
|
||||||
|
inner.y + i as u16,
|
||||||
|
inner.width.saturating_sub(2),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drawing Helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn title_bar_style() -> Style {
|
||||||
|
Style::default().fg(Color::Black).bg(Color::Blue).add_modifier(Modifier::BOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_bar_style() -> Style {
|
||||||
|
Style::default().fg(Color::White).bg(Color::Black)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn welcome_lines() -> Vec<(&'static str, Style)> {
|
||||||
|
vec![
|
||||||
(
|
(
|
||||||
"Multi-dimensional data modeling — in your terminal.",
|
"Multi-dimensional data modeling — in your terminal.",
|
||||||
Style::default().fg(Color::White),
|
Style::default().fg(Color::White),
|
||||||
@ -514,9 +475,7 @@ fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) {
|
|||||||
("", Style::default()),
|
("", Style::default()),
|
||||||
(
|
(
|
||||||
"Getting started",
|
"Getting started",
|
||||||
Style::default()
|
Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD),
|
||||||
.fg(Color::Blue)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
),
|
),
|
||||||
("", Style::default()),
|
("", Style::default()),
|
||||||
(
|
(
|
||||||
@ -546,9 +505,7 @@ fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) {
|
|||||||
("", Style::default()),
|
("", Style::default()),
|
||||||
(
|
(
|
||||||
"Navigation",
|
"Navigation",
|
||||||
Style::default()
|
Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD),
|
||||||
.fg(Color::Blue)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
),
|
),
|
||||||
("", Style::default()),
|
("", Style::default()),
|
||||||
(
|
(
|
||||||
@ -569,20 +526,80 @@ fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) {
|
|||||||
Style::default(),
|
Style::default(),
|
||||||
),
|
),
|
||||||
(":q Quit", Style::default()),
|
(":q Quit", Style::default()),
|
||||||
];
|
]
|
||||||
|
}
|
||||||
|
|
||||||
for (i, (text, style)) in lines.iter().enumerate() {
|
fn fill_line(left: String, right: &str, width: u16) -> String {
|
||||||
if i >= inner.height as usize {
|
let pad = " ".repeat((width as usize).saturating_sub(left.len() + right.len()));
|
||||||
break;
|
format!("{left}{pad}{right}")
|
||||||
}
|
}
|
||||||
f.render_widget(
|
|
||||||
Paragraph::new(*text).style(*style),
|
fn status_bar_style(mode: &AppMode) -> Style {
|
||||||
Rect::new(
|
match mode {
|
||||||
inner.x + 1,
|
AppMode::Editing { .. } => Style::default().fg(Color::Black).bg(Color::Green),
|
||||||
inner.y + i as u16,
|
AppMode::CommandMode { .. } => Style::default().fg(Color::Black).bg(Color::Yellow),
|
||||||
inner.width.saturating_sub(2),
|
AppMode::TileSelect { .. } => Style::default().fg(Color::Black).bg(Color::Magenta),
|
||||||
1,
|
_ => Style::default().fg(Color::Black).bg(Color::DarkGray),
|
||||||
),
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
|
fn mode_badge_text(mode: &AppMode) -> &'static str {
|
||||||
|
match mode {
|
||||||
|
AppMode::Normal => "NORMAL",
|
||||||
|
AppMode::Editing { .. } => "INSERT",
|
||||||
|
AppMode::FormulaEdit { .. } => "FORMULA",
|
||||||
|
AppMode::FormulaPanel => "FORMULAS",
|
||||||
|
AppMode::CategoryPanel => "CATEGORIES",
|
||||||
|
AppMode::CategoryAdd { .. } => "NEW CATEGORY",
|
||||||
|
AppMode::ItemAdd { .. } => "ADD ITEMS",
|
||||||
|
AppMode::ViewPanel => "VIEWS",
|
||||||
|
AppMode::TileSelect { .. } => "TILES",
|
||||||
|
AppMode::ImportWizard => "IMPORT",
|
||||||
|
AppMode::ExportPrompt { .. } => "EXPORT",
|
||||||
|
AppMode::CommandMode { .. } => "COMMAND",
|
||||||
|
AppMode::Help => "HELP",
|
||||||
|
AppMode::Quit => "QUIT",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_indicator(query: &str, active: bool) -> String {
|
||||||
|
if active {
|
||||||
|
format!(" /{}▌", query)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centered_popup(area: Rect, width: u16, height: u16) -> Rect {
|
||||||
|
let w = width.min(area.width);
|
||||||
|
let h = height.min(area.height);
|
||||||
|
let x = area.x + area.width.saturating_sub(w) / 2;
|
||||||
|
let y = area.y + area.height.saturating_sub(h) / 2;
|
||||||
|
Rect::new(x, y, w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App, is_cmd_mode: bool) {
|
||||||
|
if is_cmd_mode {
|
||||||
|
draw_command_bar(f, area, app);
|
||||||
|
} else {
|
||||||
|
draw_status(f, area, app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_overlays(f: &mut Frame, content_area: Rect, app: &App) {
|
||||||
|
let size = f.area();
|
||||||
|
if matches!(app.mode, AppMode::Help) {
|
||||||
|
f.render_widget(HelpWidget, size);
|
||||||
|
}
|
||||||
|
if matches!(app.mode, AppMode::ImportWizard) {
|
||||||
|
if let Some(wizard) = &app.wizard {
|
||||||
|
f.render_widget(ImportWizardWidget::new(wizard), size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches!(app.mode, AppMode::ExportPrompt { .. }) {
|
||||||
|
draw_export_prompt(f, size, app);
|
||||||
|
}
|
||||||
|
if app.is_empty_model() && matches!(app.mode, AppMode::Normal | AppMode::CommandMode { .. }) {
|
||||||
|
draw_welcome(f, content_area, app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user