Skip to content

Commit 69ca104

Browse files
committed
feat: implemented txs search bar
1 parent a161101 commit 69ca104

File tree

4 files changed

+156
-60
lines changed

4 files changed

+156
-60
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ tx3-lang = "0.2.0"
4646
tx3-cardano = "0.2.0"
4747
url = { version = "2.5.0", features = ["serde"] }
4848
utxorpc = "0.10.0"
49+
regex = "1.11.1"
4950

5051
# The profile that 'cargo dist' will build with
5152
[profile.dist]

src/explorer/mod.rs

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ impl App {
8080
.draw(|frame| self.draw(frame))
8181
.into_diagnostic()
8282
.context("rendering")?;
83+
8384
match self.events.next().await? {
8485
Event::Crossterm(event) => {
8586
if let crossterm::event::Event::Key(key_event) = event {
@@ -107,7 +108,7 @@ impl App {
107108

108109
if !self.should_show_help {
109110
match key.code {
110-
KeyCode::Char('q') | KeyCode::Esc => {
111+
KeyCode::Char('q') => {
111112
if self.should_show_help {
112113
self.should_show_help = false
113114
} else {
@@ -165,15 +166,7 @@ impl App {
165166
}
166167

167168
if let SelectedTab::Transactions(_) = &mut self.selected_tab {
168-
match key.code {
169-
KeyCode::Char('j') | KeyCode::Down => {
170-
self.transactions_tab_next_row();
171-
}
172-
KeyCode::Char('k') | KeyCode::Up => {
173-
self.transactions_tab_previous_row();
174-
}
175-
_ => {}
176-
}
169+
self.transactions_tab_state.handle_key(&key);
177170
}
178171
} else {
179172
match key.code {
@@ -200,6 +193,7 @@ impl App {
200193
.blocks_tab_state
201194
.scroll_state
202195
.content_length(self.chain.blocks.len() * 3 - 2);
196+
203197
self.selected_tab = match &self.selected_tab {
204198
SelectedTab::Blocks(_) => SelectedTab::Blocks(BlocksTab::from(&*self)),
205199
SelectedTab::Transactions(_) => {
@@ -284,40 +278,6 @@ impl App {
284278
self.blocks_tab_state.scroll_state = self.blocks_tab_state.scroll_state.position(i * 3);
285279
}
286280

287-
pub fn transactions_tab_next_row(&mut self) {
288-
let tx_count = self.chain.blocks.iter().map(|b| b.tx_count).sum::<usize>();
289-
let i = match self.transactions_tab_state.table_state.selected() {
290-
Some(i) => {
291-
if i >= tx_count - 1 {
292-
0
293-
} else {
294-
i + 1
295-
}
296-
}
297-
None => 0,
298-
};
299-
self.transactions_tab_state.table_state.select(Some(i));
300-
self.transactions_tab_state.scroll_state =
301-
self.transactions_tab_state.scroll_state.position(i * 3);
302-
}
303-
304-
pub fn transactions_tab_previous_row(&mut self) {
305-
let tx_count = self.chain.blocks.iter().map(|b| b.tx_count).sum::<usize>();
306-
let i = match self.transactions_tab_state.table_state.selected() {
307-
Some(i) => {
308-
if i == 0 {
309-
tx_count - 1
310-
} else {
311-
i - 1
312-
}
313-
}
314-
None => 0,
315-
};
316-
self.transactions_tab_state.table_state.select(Some(i));
317-
self.transactions_tab_state.scroll_state =
318-
self.transactions_tab_state.scroll_state.position(i * 3);
319-
}
320-
321281
fn draw(&mut self, frame: &mut Frame) {
322282
let [header_area, sparkline_area, inner_area, footer_area] = Layout::vertical([
323283
Constraint::Length(5), // Header

src/explorer/widgets/transactions_tab.rs

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,131 @@
1+
use crossterm::event::{KeyCode, KeyEvent};
12
use ratatui::{
23
buffer::Buffer,
3-
layout::{Constraint, Margin, Rect},
4+
layout::{Constraint, Layout, Margin, Rect},
45
style::{Color, Modifier, Style, Stylize},
56
text::Text,
67
widgets::{
7-
Block, Cell, HighlightSpacing, Row, Scrollbar, ScrollbarState, StatefulWidget, Table,
8-
TableState,
8+
Block, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarState, StatefulWidget,
9+
Table, TableState, Widget,
910
},
1011
};
12+
use regex::Regex;
1113
use utxorpc::spec::cardano;
1214

1315
use crate::explorer::{App, ChainBlock};
1416

1517
#[derive(Default)]
1618
pub struct TransactionsTabState {
17-
pub scroll_state: ScrollbarState,
18-
pub table_state: TableState,
19+
scroll_state: ScrollbarState,
20+
table_state: TableState,
21+
input: String,
22+
input_mode: InputMode,
23+
character_index: usize,
24+
}
25+
impl TransactionsTabState {
26+
fn byte_index(&self) -> usize {
27+
self.input
28+
.char_indices()
29+
.map(|(i, _)| i)
30+
.nth(self.character_index)
31+
.unwrap_or(self.input.len())
32+
}
33+
34+
pub fn enter_char(&mut self, new_char: char) {
35+
let index = self.byte_index();
36+
self.input.insert(index, new_char);
37+
self.move_cursor_right();
38+
}
39+
40+
pub fn move_cursor_left(&mut self) {
41+
let cursor_moved_left = self.character_index.saturating_sub(1);
42+
self.character_index = self.clamp_cursor(cursor_moved_left);
43+
}
44+
45+
pub fn move_cursor_right(&mut self) {
46+
let cursor_moved_right = self.character_index.saturating_add(1);
47+
self.character_index = self.clamp_cursor(cursor_moved_right);
48+
}
49+
50+
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
51+
new_cursor_pos.clamp(0, self.input.chars().count())
52+
}
53+
54+
pub fn delete_char(&mut self) {
55+
let is_not_cursor_leftmost = self.character_index != 0;
56+
if is_not_cursor_leftmost {
57+
let current_index = self.character_index;
58+
let from_left_to_current_index = current_index - 1;
59+
60+
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
61+
let after_char_to_delete = self.input.chars().skip(current_index);
62+
63+
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
64+
self.move_cursor_left();
65+
}
66+
}
67+
68+
pub fn next_row(&mut self) {
69+
let i = match self.table_state.selected() {
70+
Some(i) => i + 1,
71+
None => 0,
72+
};
73+
74+
self.table_state.select(Some(i));
75+
self.scroll_state = self.scroll_state.position(i * 3);
76+
}
77+
78+
pub fn previous_row(&mut self) {
79+
let i = match self.table_state.selected() {
80+
Some(i) => {
81+
if i == 0 {
82+
0
83+
} else {
84+
i - 1
85+
}
86+
}
87+
None => 0,
88+
};
89+
self.table_state.select(Some(i));
90+
self.scroll_state = self.scroll_state.position(i * 3);
91+
}
92+
93+
pub fn handle_key(&mut self, key: &KeyEvent) {
94+
match self.input_mode {
95+
InputMode::Normal => match key.code {
96+
KeyCode::Char('j') | KeyCode::Down => {
97+
self.next_row();
98+
}
99+
KeyCode::Char('k') | KeyCode::Up => {
100+
self.previous_row();
101+
}
102+
KeyCode::Char('e') => self.input_mode = InputMode::Editing,
103+
_ => {}
104+
},
105+
InputMode::Editing => match key.code {
106+
KeyCode::Char(to_insert) => self.enter_char(to_insert),
107+
KeyCode::Left => self.move_cursor_left(),
108+
KeyCode::Right => self.move_cursor_right(),
109+
KeyCode::Esc => self.input_mode = InputMode::Normal,
110+
KeyCode::Backspace => self.delete_char(),
111+
_ => {}
112+
},
113+
}
114+
}
19115
}
20116

21117
#[derive(Clone)]
22118
pub struct TransactionsTab {
23119
pub blocks: Vec<ChainBlock>,
24120
}
25121

122+
#[derive(Clone, Default, PartialEq, Eq)]
123+
pub enum InputMode {
124+
#[default]
125+
Normal,
126+
Editing,
127+
}
128+
26129
impl From<&App> for TransactionsTab {
27130
fn from(value: &App) -> Self {
28131
Self {
@@ -60,25 +163,58 @@ impl TxView {
60163

61164
impl StatefulWidget for TransactionsTab {
62165
type State = TransactionsTabState;
166+
63167
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
64168
where
65169
Self: Sized,
66170
{
171+
let [search_area, txs_area] =
172+
Layout::vertical([Constraint::Length(3), Constraint::Fill(1)]).areas(area);
173+
174+
let input = Paragraph::new(state.input.as_str())
175+
.style(match state.input_mode {
176+
InputMode::Normal => Style::default(),
177+
InputMode::Editing => Style::default().fg(Color::White),
178+
})
179+
.block(
180+
Block::bordered()
181+
.title(" Search ")
182+
.border_style(match state.input_mode {
183+
InputMode::Normal => Style::new().dark_gray(),
184+
InputMode::Editing => Style::new().white(),
185+
}),
186+
);
187+
188+
input.render(search_area, buf);
189+
67190
let header = ["Hash", "Slot", "Certs", "Assets", "Total Coin", "Datum"]
68191
.into_iter()
69192
.map(Cell::from)
70193
.collect::<Row>()
71194
.style(Style::default().fg(Color::Green).bold())
72195
.height(1);
196+
let mut txs: Vec<TxView> = self
197+
.blocks
198+
.iter()
199+
.flat_map(|chain_block| {
200+
if let Some(body) = &chain_block.body {
201+
return TxView::new(chain_block.slot, body);
202+
}
203+
Default::default()
204+
})
205+
.collect();
73206

74-
let txs = self.blocks.iter().flat_map(|chain_block| {
75-
if let Some(body) = &chain_block.body {
76-
return TxView::new(chain_block.slot, body);
77-
}
78-
Default::default()
79-
});
207+
if !state.input.is_empty() {
208+
let input_regex = Regex::new(&state.input).unwrap();
209+
txs = txs
210+
.into_iter()
211+
.filter(|tx| {
212+
input_regex.is_match(&tx.hash) || input_regex.is_match(&tx.slot.to_string())
213+
})
214+
.collect();
215+
}
80216

81-
let rows = txs.enumerate().map(|(i, tx)| {
217+
let rows = txs.iter().enumerate().map(|(i, tx)| {
82218
let color = match i % 2 {
83219
0 => Color::Black,
84220
_ => Color::Reset,
@@ -94,7 +230,6 @@ impl StatefulWidget for TransactionsTab {
94230
.style(Style::new().fg(Color::White).bg(color))
95231
.height(3)
96232
});
97-
98233
let bar = " █ ";
99234
let table = Table::new(
100235
rows,
@@ -113,11 +248,10 @@ impl StatefulWidget for TransactionsTab {
113248
.highlight_spacing(HighlightSpacing::Always)
114249
.block(Block::bordered());
115250

116-
StatefulWidget::render(table, area, buf, &mut state.table_state);
117-
251+
StatefulWidget::render(table, txs_area, buf, &mut state.table_state);
118252
StatefulWidget::render(
119253
Scrollbar::new(ratatui::widgets::ScrollbarOrientation::VerticalRight),
120-
area.inner(Margin {
254+
txs_area.inner(Margin {
121255
vertical: 1,
122256
horizontal: 1,
123257
}),

0 commit comments

Comments
 (0)