Skip to content

Commit 6b6712f

Browse files
committed
feat: browse questions by topic tag tui
1 parent fd7462b commit 6b6712f

File tree

8 files changed

+205
-163
lines changed

8 files changed

+205
-163
lines changed

Cargo.lock

-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,4 @@ toml = "0.7.6"
1818
async-trait = "0.1.71"
1919
skim = "0.10.4"
2020
ratatui = { version = "0.21.0", features = ["all-widgets"] }
21-
anyhow = "1.0.72"
2221
crossterm = "0.26.1"
23-

src/app_ui/app.rs

+36-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{entities::question::Model as QuestionModel, deserializers::question::Question};
1+
use crate::entities::question::Model as QuestionModel;
22
use crate::entities::topic_tag::Model as TopicTagModel;
33
use std::{collections::HashMap, error};
44

@@ -12,49 +12,60 @@ pub type SS = (TopicTagModel, Vec<QuestionModel>);
1212
pub type TTReciever = tokio::sync::mpsc::Receiver<SS>;
1313
pub type TTSender = tokio::sync::mpsc::Sender<SS>;
1414

15+
#[derive(Debug)]
16+
pub enum Widget<'a> {
17+
QuestionList(&'a mut StatefulList<QuestionModel>),
18+
TopicTagList(&'a mut StatefulList<TopicTagModel>),
19+
}
20+
1521
/// Application.
1622
#[derive(Debug)]
17-
pub struct App {
23+
pub struct App<'a> {
1824
/// Is the application running?
1925
pub running: bool,
2026

21-
pub topic_tags_stateful: StatefulList<TopicTagModel>,
22-
23-
// multi select logic can be implemented
24-
pub questions_stateful: StatefulList<QuestionModel>,
27+
pub widgets: &'a mut Vec<Widget<'a>>,
2528

26-
pub questions_list: HashMap<String, Vec<QuestionModel>>,
29+
pub questions_list: &'a HashMap<String, Vec<QuestionModel>>,
2730

28-
pub questions_recv: TTReciever,
31+
pub widget_switcher: i32,
2932
}
3033

31-
impl App {
34+
impl<'a> App<'a> {
3235
/// Constructs a new instance of [`App`].
33-
pub fn new(rec: TTReciever) -> Self {
36+
pub fn new(
37+
wid: &'a mut Vec<Widget<'a>>,
38+
questions_list: &'a HashMap<String, Vec<QuestionModel>>,
39+
) -> Self {
3440
Self {
3541
running: true,
36-
topic_tags_stateful: StatefulList::with_items(vec![]),
37-
questions_stateful: StatefulList::with_items(vec![]),
38-
questions_list: HashMap::new(),
39-
questions_recv: rec,
42+
questions_list,
43+
widgets: wid,
44+
widget_switcher: 0,
4045
}
4146
}
4247

43-
/// Handles the tick event of the terminal.
44-
pub fn tick(&mut self) {
45-
for _ in 0..100 {
46-
if let Some((topic_tag, mut questions)) = self.questions_recv.blocking_recv() {
47-
if let Some(name) = &topic_tag.name {
48-
self.questions_list.entry(name.clone()).or_insert(vec![]).append(&mut questions);
49-
}
50-
self.topic_tags_stateful.add_item(topic_tag);
51-
}
52-
}
48+
pub fn next_widget(&mut self) {
49+
let a = self.widget_switcher + 1;
50+
let b = self.widgets.len() as i32;
51+
self.widget_switcher = ((a % b) + b) % b;
5352
}
5453

54+
pub fn prev_widget(&mut self) {
55+
let a = self.widget_switcher - 1;
56+
let b = self.widgets.len() as i32;
57+
self.widget_switcher = ((a % b) + b) % b;
58+
}
59+
60+
pub fn get_current_widget(&self) -> &Widget {
61+
&self.widgets[self.widget_switcher as usize]
62+
}
63+
64+
/// Handles the tick event of the terminal.
65+
pub fn tick(&mut self) {}
66+
5567
/// Set running to false to quit the application.
5668
pub fn quit(&mut self) {
5769
self.running = false;
5870
}
59-
6071
}

src/app_ui/handler.rs

+29-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
33

44
/// Handles the key events and updates the state of [`App`].
55
pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
6+
let curr_widget = &mut app.widgets[app.widget_switcher as usize];
67
match key_event.code {
78
// Exit application on `ESC` or `q`
89
KeyCode::Esc | KeyCode::Char('q') => {
@@ -15,21 +16,39 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
1516
}
1617
}
1718
// Counter handlers
18-
KeyCode::Up => {
19-
app.topic_tags_stateful.previous();
20-
}
21-
KeyCode::Down => {
22-
app.topic_tags_stateful.next() ;
23-
}
19+
KeyCode::Up => match curr_widget {
20+
super::app::Widget::QuestionList(ql) => ql.previous(),
21+
super::app::Widget::TopicTagList(tt) => tt.previous(),
22+
},
23+
KeyCode::Down => match curr_widget {
24+
super::app::Widget::QuestionList(ql) => ql.next(),
25+
super::app::Widget::TopicTagList(tt) => tt.next(),
26+
},
27+
KeyCode::Left => app.prev_widget(),
28+
KeyCode::Right => app.next_widget(),
2429
// Other handlers you could add here.
2530
_ => {}
2631
}
2732

2833
// post key event update the question list
29-
if let Some(selected_tt) = app.topic_tags_stateful.get_selected_item() {
30-
if let Some(ttname) = &selected_tt.name {
31-
let questions = app.questions_list.get(ttname).unwrap();
32-
app.questions_stateful.items = questions.clone();
34+
let mut name: Option<String> = None;
35+
36+
match &app.widgets[app.widget_switcher as usize] {
37+
super::app::Widget::TopicTagList(ttl) => {
38+
if let Some(selected_widget) = ttl.get_selected_item() {
39+
if let Some(n) = &selected_widget.name {
40+
name = Some(n.clone());
41+
}
42+
}
43+
}
44+
_ => {}
45+
}
46+
47+
for w in app.widgets.iter_mut() {
48+
if let super::app::Widget::QuestionList(ql) = w {
49+
if let Some(name) = &name {
50+
ql.items = app.questions_list.get(name).unwrap().clone();
51+
}
3352
}
3453
}
3554
Ok(())

src/app_ui/list.rs

+9-16
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@ pub struct StatefulList<T> {
77
}
88

99
impl<T> StatefulList<T> {
10-
1110
pub fn add_item(&mut self, item: T) {
1211
self.items.push(item)
1312
}
1413

1514
pub fn get_selected_item(&self) -> Option<&T> {
1615
match self.state.selected() {
17-
Some(i) => {
18-
Some(&self.items[i])
19-
}
16+
Some(i) => Some(&self.items[i]),
2017
None => None,
2118
}
2219
}
@@ -31,29 +28,25 @@ impl<T> StatefulList<T> {
3128
pub fn next(&mut self) {
3229
let i = match self.state.selected() {
3330
Some(i) => {
34-
if i >= self.items.len() - 1 {
35-
0
36-
} else {
37-
i + 1
38-
}
31+
let a = i as i32 + 1;
32+
let b = self.items.len() as i32;
33+
((a % b) + b) % b
3934
}
4035
None => 0,
4136
};
42-
self.state.select(Some(i));
37+
self.state.select(Some(i as usize));
4338
}
4439

4540
pub fn previous(&mut self) {
4641
let i = match self.state.selected() {
4742
Some(i) => {
48-
if i == 0 {
49-
self.items.len() - 1
50-
} else {
51-
i - 1
52-
}
43+
let a = i as i32 - 1;
44+
let b = self.items.len() as i32;
45+
((a % b) + b) % b
5346
}
5447
None => 0,
5548
};
56-
self.state.select(Some(i));
49+
self.state.select(Some(i as usize));
5750
}
5851

5952
pub fn unselect(&mut self) {

src/app_ui/ui.rs

+74-60
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,92 @@
1-
// use ratatui::{
2-
// backend::Backend,
3-
// layout::{Alignment, Layout},
4-
// style::{Color, Style},
5-
// widgets::{Block, BorderType, Borders, Paragraph},
6-
// Frame,
7-
// };
81
use ratatui::{
9-
backend::{Backend, CrosstermBackend},
10-
layout::{Constraint, Corner, Direction, Layout},
2+
backend::Backend,
3+
layout::{Constraint, Direction, Layout},
114
style::{Color, Modifier, Style},
12-
text::{Line, Span},
13-
widgets::{Block, Borders, List, ListItem, ListState},
14-
Frame, Terminal,
5+
text::Line,
6+
widgets::{Block, Borders, List, ListItem},
7+
Frame,
158
};
169

1710
use super::app::App;
1811

1912
/// Renders the user interface widgets.
20-
pub fn render<B: Backend>(app: &mut App, f: &mut Frame<'_, B>) {
13+
pub fn render<'a, B: Backend>(app: &'a mut App, f: &mut Frame<'_, B>) {
2114
// Create two chunks with equal horizontal screen space
2215
let chunks = Layout::default()
2316
.direction(Direction::Horizontal)
2417
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
2518
.split(f.size());
2619

2720
// Iterate through all elements in the `items` app and append some debug text to it.
28-
let items: Vec<ListItem> = app
29-
.topic_tags_stateful
30-
.items
31-
.iter()
32-
.map(|tt_model| {
33-
if let Some(name) = &tt_model.name {
34-
let lines = vec![Line::from(name.as_str())];
35-
ListItem::new(lines)
36-
} else {
37-
ListItem::new(vec![Line::from("")])
38-
}
39-
})
40-
.collect();
41-
42-
// Create a List from all list items and highlight the currently selected one
43-
let items = List::new(items)
44-
.block(Block::default().borders(Borders::ALL).title("List"))
45-
.highlight_style(
46-
Style::default()
47-
.bg(Color::White)
48-
.fg(Color::Black)
49-
.add_modifier(Modifier::BOLD),
50-
)
51-
.highlight_symbol(">> ");
21+
for (i, w) in app.widgets.iter_mut().enumerate() {
22+
let is_widget_active = app.widget_switcher as usize == i;
23+
let mut border_style = Style::default();
24+
if is_widget_active {
25+
border_style = border_style.fg(Color::Cyan);
26+
}
27+
match w {
28+
super::app::Widget::TopicTagList(ttl) => {
29+
let items: Vec<ListItem> = ttl
30+
.items
31+
.iter()
32+
.map(|tt_model| {
33+
if let Some(name) = &tt_model.name {
34+
let lines = vec![Line::from(name.as_str())];
35+
ListItem::new(lines)
36+
} else {
37+
ListItem::new(vec![Line::from("")])
38+
}
39+
})
40+
.collect();
5241

53-
// We can now render the item list
54-
f.render_stateful_widget(items, chunks[0], &mut app.topic_tags_stateful.state);
42+
// Create a List from all list items and highlight the currently selected one
43+
let items = List::new(items)
44+
.block(
45+
Block::default()
46+
.borders(Borders::ALL)
47+
.title("Tags")
48+
.border_style(border_style),
49+
)
50+
.highlight_style(
51+
Style::default()
52+
.bg(Color::White)
53+
.fg(Color::Black)
54+
.add_modifier(Modifier::BOLD),
55+
)
56+
.highlight_symbol(">> ");
5557

56-
let questions: Vec<ListItem> = app
57-
.questions_stateful
58-
.items
59-
.iter()
60-
.map(|question| {
61-
let lines = vec![Line::from(format!(
62-
"{}: {:?}",
63-
question.frontend_question_id, question.title
64-
))];
65-
ListItem::new(lines)
66-
})
67-
.collect();
58+
// We can now render the item list
59+
f.render_stateful_widget(items, chunks[0], &mut ttl.state);
60+
}
61+
super::app::Widget::QuestionList(ql) => {
62+
let questions: Vec<ListItem> = ql
63+
.items
64+
.iter()
65+
.map(|question| {
66+
let lines = vec![Line::from(format!(
67+
"{}: {:?}",
68+
question.frontend_question_id, question.title
69+
))];
70+
ListItem::new(lines)
71+
})
72+
.collect();
6873

69-
let items = List::new(questions)
70-
.block(Block::default().borders(Borders::ALL).title("Questions"))
71-
.highlight_style(
72-
Style::default()
73-
.bg(Color::LightGreen)
74-
.add_modifier(Modifier::BOLD),
75-
)
76-
.highlight_symbol(">> ");
77-
f.render_stateful_widget(items, chunks[1], &mut app.questions_stateful.state);
74+
let items = List::new(questions)
75+
.block(
76+
Block::default()
77+
.borders(Borders::ALL)
78+
.title("Questions")
79+
.border_style(border_style),
80+
)
81+
.highlight_style(
82+
Style::default()
83+
.bg(Color::White)
84+
.fg(Color::Black)
85+
.add_modifier(Modifier::BOLD),
86+
)
87+
.highlight_symbol(">> ");
88+
f.render_stateful_widget(items, chunks[1], &mut ql.state);
89+
}
90+
}
91+
}
7892
}

src/entities/mod.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@ pub mod question;
55
pub mod question_topic_tag;
66
pub mod topic_tag;
77

8-
// pub use question::Model as Question;
9-
// pub use question_topic_tag::Model as QuestionTopicTag;
10-
// pub use topic_tag::Model as TopicTag;
8+
pub type QuestionModel = question::Model;
9+
pub type TopicTagModel = topic_tag::Model;

0 commit comments

Comments
 (0)