Skip to content
This repository was archived by the owner on Jul 24, 2026. It is now read-only.

Commit 4cbf722

Browse files
committed
Merge branch 'feature/tui'
2 parents 8133f09 + 77d9560 commit 4cbf722

9 files changed

Lines changed: 189 additions & 72 deletions

File tree

crates/tui/src/app/mod.rs

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ pub struct AppState {
8787
pub toast: Option<ToastState>,
8888
pub connection: ConnectionState,
8989
pub last_refresh: Option<DateTime<FixedOffset>>,
90-
pub base_url: String,
9190
pub last_flow_id: Option<uuid::Uuid>,
9291
pub default_wallet_id: Option<uuid::Uuid>,
9392
pub default_flow_id: Option<uuid::Uuid>,
@@ -128,7 +127,6 @@ impl App {
128127
toast: None,
129128
connection: ConnectionState::default(),
130129
last_refresh: None,
131-
base_url: config.base_url.clone(),
132130
last_flow_id: None,
133131
default_wallet_id: None,
134132
default_flow_id: None,
@@ -378,7 +376,7 @@ impl App {
378376
TransactionsMode::List | TransactionsMode::Detail
379377
)
380378
{
381-
self.transactions_select_prev();
379+
self.state.transactions.select_prev();
382380
if self.state.transactions.mode == TransactionsMode::Detail {
383381
self.open_transaction_detail().await?;
384382
}
@@ -440,7 +438,7 @@ impl App {
440438
TransactionsMode::List | TransactionsMode::Detail
441439
)
442440
{
443-
self.transactions_select_next();
441+
self.state.transactions.select_next();
444442
if self.state.transactions.mode == TransactionsMode::Detail {
445443
self.open_transaction_detail().await?;
446444
}
@@ -962,13 +960,13 @@ impl App {
962960
}
963961
'j' | 'J' => {
964962
if self.state.section == Section::Transactions {
965-
self.transactions_select_next();
963+
self.state.transactions.select_next();
966964
}
967965
return Ok(());
968966
}
969967
'k' | 'K' => {
970968
if self.state.section == Section::Transactions {
971-
self.transactions_select_prev();
969+
self.state.transactions.select_prev();
972970
}
973971
return Ok(());
974972
}
@@ -1577,21 +1575,6 @@ impl App {
15771575
self.state.flows.selected = self.state.flows.selected.saturating_sub(1);
15781576
}
15791577

1580-
fn transactions_select_next(&mut self) {
1581-
let len = transactions_visible_indices(&self.state).len();
1582-
if len == 0 {
1583-
return;
1584-
}
1585-
self.state.transactions.selected = (self.state.transactions.selected + 1).min(len - 1);
1586-
}
1587-
1588-
fn transactions_select_prev(&mut self) {
1589-
if transactions_visible_indices(&self.state).is_empty() {
1590-
return;
1591-
}
1592-
self.state.transactions.selected = self.state.transactions.selected.saturating_sub(1);
1593-
}
1594-
15951578
fn transactions_picker_next(&mut self) {
15961579
let len = self.transactions_picker_len();
15971580
if len == 0 {
@@ -4581,18 +4564,32 @@ impl TransactionsState {
45814564
}
45824565

45834566
fn select_next(&mut self) {
4584-
if self.items.is_empty() {
4567+
let visible_len = self.visible_len();
4568+
if visible_len == 0 {
45854569
return;
45864570
}
4587-
self.selected = (self.selected + 1).min(self.items.len() - 1);
4571+
self.selected = (self.selected + 1).min(visible_len - 1);
45884572
}
45894573

45904574
fn select_prev(&mut self) {
4591-
if self.items.is_empty() {
4575+
let visible_len = self.visible_len();
4576+
if visible_len == 0 {
45924577
return;
45934578
}
45944579
self.selected = self.selected.saturating_sub(1);
45954580
}
4581+
4582+
fn visible_len(&self) -> usize {
4583+
let query = normalize_query(self.search_query.as_str());
4584+
if query.is_empty() {
4585+
return self.items.len();
4586+
}
4587+
4588+
self.items
4589+
.iter()
4590+
.filter(|tx| transaction_matches_query(tx, query.as_str()))
4591+
.count()
4592+
}
45964593
}
45974594

45984595
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

crates/tui/src/ui/components/card.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ impl<'a> Card<'a> {
4848
.borders(Borders::ALL)
4949
.border_type(BorderType::Rounded)
5050
.border_style(Style::default().fg(border_color))
51+
.style(Style::default().bg(self.theme.surface_bright))
5152
}
5253

5354
/// Returns the inner area after accounting for borders.

crates/tui/src/ui/components/charts.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ pub fn render_bar_chart(
2626
.value_style(Style::default().fg(theme.text).add_modifier(Modifier::BOLD))
2727
.label_style(Style::default().fg(theme.dim));
2828

29-
let card = Card::new(title, theme);
30-
let inner = card.inner(area);
31-
card.render_frame(frame, area);
32-
frame.render_widget(chart, inner);
29+
if title.is_empty() {
30+
frame.render_widget(chart, area);
31+
} else {
32+
let card = Card::new(title, theme);
33+
let inner = card.inner(area);
34+
card.render_frame(frame, area);
35+
frame.render_widget(chart, inner);
36+
}
3337
}
3438

3539
/// Renders a sparkline (mini line chart) for trend visualization.
@@ -46,10 +50,14 @@ pub fn render_sparkline(
4650
.data(data)
4751
.style(Style::default().fg(theme.accent));
4852

49-
let card = Card::new(title, theme);
50-
let inner = card.inner(area);
51-
card.render_frame(frame, area);
52-
frame.render_widget(sparkline, inner);
53+
if title.is_empty() {
54+
frame.render_widget(sparkline, area);
55+
} else {
56+
let card = Card::new(title, theme);
57+
let inner = card.inner(area);
58+
card.render_frame(frame, area);
59+
frame.render_widget(sparkline, inner);
60+
}
5361
}
5462

5563
/// Renders an inline sparkline without borders (for embedding in other

crates/tui/src/ui/components/help_overlay.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use ratatui::{
88

99
use crate::{
1010
app::{AppState, Section, TransactionsMode},
11-
ui::{components::centered_rect, theme::Theme},
11+
ui::{
12+
components::{centered_rect, tabs},
13+
theme::Theme,
14+
},
1215
};
1316

1417
pub fn render(frame: &mut Frame<'_>, area: Rect, state: &AppState) {
@@ -54,6 +57,7 @@ fn help_lines(state: &AppState, theme: &Theme) -> Vec<Line<'static>> {
5457
Span::styled("?", Style::default().fg(theme.accent)),
5558
Span::raw(" help"),
5659
])];
60+
lines.push(Line::from(tabs::tab_shortcuts(theme)));
5761

5862
match state.section {
5963
Section::Home => {

crates/tui/src/ui/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ pub use theme::Theme;
2020

2121
pub fn render(frame: &mut Frame<'_>, state: &AppState) {
2222
let area = frame.area();
23+
let theme = Theme::default();
24+
frame.render_widget(
25+
ratatui::widgets::Block::default()
26+
.style(ratatui::style::Style::default().bg(theme.background)),
27+
area,
28+
);
2329
match state.screen {
2430
crate::app::Screen::Login => screens::login::render(frame, area, state),
2531
crate::app::Screen::Home => render_shell(frame, area, state),
@@ -43,6 +49,11 @@ fn render_shell(frame: &mut Frame<'_>, area: Rect, state: &AppState) {
4349

4450
// Content area
4551
let content_inner = layout[1];
52+
frame.render_widget(
53+
ratatui::widgets::Block::default()
54+
.style(ratatui::style::Style::default().bg(theme.surface)),
55+
content_inner,
56+
);
4657

4758
match state.section {
4859
crate::app::Section::Home => screens::home::render(frame, content_inner, state),

crates/tui/src/ui/screens/home.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,8 @@ fn render_stat_card(
122122
ratio: Option<(i64, i64)>,
123123
theme: &Theme,
124124
) {
125-
let card = Card::new(title, theme);
125+
let card = Card::new(title, theme).focused(true);
126126
let inner = card.inner(area);
127-
card.render_frame(frame, area);
128-
129127
let mut lines = vec![Line::from(Span::styled(
130128
value,
131129
value_style.add_modifier(Modifier::BOLD),
@@ -140,7 +138,7 @@ fn render_stat_card(
140138
)));
141139
}
142140

143-
frame.render_widget(Paragraph::new(lines), inner);
141+
card.render_with(frame, area, Paragraph::new(lines));
144142
}
145143

146144
fn render_wallets_flows(frame: &mut Frame<'_>, area: Rect, state: &AppState, theme: &Theme) {

crates/tui/src/ui/screens/stats.rs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fn render_month_summary(frame: &mut Frame<'_>, area: Rect, state: &AppState, the
8686
let (year, month) = state.stats.current_month;
8787
let month_name = month_name(month);
8888

89-
let card = Card::new("Month Summary", theme);
89+
let card = Card::new("Month Summary", theme).focused(true);
9090
let inner = card.inner(area);
9191
card.render_frame(frame, area);
9292

@@ -325,11 +325,18 @@ fn render_category_breakdown(frame: &mut Frame<'_>, area: Rect, state: &AppState
325325
.take(inner.height as usize)
326326
.map(|(category, amount)| {
327327
let pct = compute_percentage(*amount, total);
328+
let style = if pct >= 75 {
329+
BarStyle::Block
330+
} else if pct >= 25 {
331+
BarStyle::Line
332+
} else {
333+
BarStyle::Dot
334+
};
328335
let bar = ascii_bar_styled(
329336
amount.saturating_abs() as u64,
330337
total.saturating_abs() as u64,
331338
20,
332-
BarStyle::Line,
339+
style,
333340
);
334341

335342
Line::from(vec![
@@ -379,6 +386,27 @@ fn render_monthly_trend(frame: &mut Frame<'_>, area: Rect, state: &AppState, the
379386
.iter()
380387
.map(|(label, value)| (label.as_str(), (*value).max(0) as u64))
381388
.collect();
389+
let income_color = if income_data.is_empty() {
390+
theme.dim
391+
} else if income_data.last().map(|(_, v)| *v).unwrap_or(0)
392+
>= income_data.first().map(|(_, v)| *v).unwrap_or(0)
393+
{
394+
theme.positive
395+
} else {
396+
theme.warning
397+
};
398+
let income_title = Line::from(vec![
399+
Span::styled("Income (6m)", Style::default().fg(theme.text_muted)),
400+
Span::raw(" "),
401+
Span::styled(
402+
if income_color == theme.positive {
403+
"●"
404+
} else {
405+
"○"
406+
},
407+
Style::default().fg(income_color),
408+
),
409+
]);
382410
if income_data.is_empty() {
383411
let card = Card::new("Income (6m)", theme);
384412
let inner = card.inner(layout[0]);
@@ -392,13 +420,43 @@ fn render_monthly_trend(frame: &mut Frame<'_>, area: Rect, state: &AppState, the
392420
inner,
393421
);
394422
} else {
395-
render_bar_chart(frame, layout[0], "Income (6m)", &income_data, theme);
423+
let card = Card::new("Income (6m)", theme).focused(true);
424+
let inner = card.inner(layout[0]);
425+
card.render_frame(frame, layout[0]);
426+
frame.render_widget(Paragraph::new(income_title), Rect { height: 1, ..inner });
427+
let chart_area = Rect {
428+
y: inner.y + 1,
429+
height: inner.height.saturating_sub(1),
430+
..inner
431+
};
432+
render_bar_chart(frame, chart_area, "", &income_data, theme);
396433
}
397434

398435
let expense_data: Vec<(&str, u64)> = expense_trend
399436
.iter()
400437
.map(|(label, value)| (label.as_str(), (*value).max(0) as u64))
401438
.collect();
439+
let expense_color = if expense_data.is_empty() {
440+
theme.dim
441+
} else if expense_data.last().map(|(_, v)| *v).unwrap_or(0)
442+
>= expense_data.first().map(|(_, v)| *v).unwrap_or(0)
443+
{
444+
theme.warning
445+
} else {
446+
theme.positive
447+
};
448+
let expense_title = Line::from(vec![
449+
Span::styled("Expenses (6m)", Style::default().fg(theme.text_muted)),
450+
Span::raw(" "),
451+
Span::styled(
452+
if expense_color == theme.warning {
453+
"●"
454+
} else {
455+
"○"
456+
},
457+
Style::default().fg(expense_color),
458+
),
459+
]);
402460
if expense_data.is_empty() {
403461
let card = Card::new("Expenses (6m)", theme);
404462
let inner = card.inner(layout[1]);
@@ -412,7 +470,16 @@ fn render_monthly_trend(frame: &mut Frame<'_>, area: Rect, state: &AppState, the
412470
inner,
413471
);
414472
} else {
415-
render_bar_chart(frame, layout[1], "Expenses (6m)", &expense_data, theme);
473+
let card = Card::new("Expenses (6m)", theme).focused(true);
474+
let inner = card.inner(layout[1]);
475+
card.render_frame(frame, layout[1]);
476+
frame.render_widget(Paragraph::new(expense_title), Rect { height: 1, ..inner });
477+
let chart_area = Rect {
478+
y: inner.y + 1,
479+
height: inner.height.saturating_sub(1),
480+
..inner
481+
};
482+
render_bar_chart(frame, chart_area, "", &expense_data, theme);
416483
}
417484
}
418485

0 commit comments

Comments
 (0)