diff options
Diffstat (limited to 'ticket/src/tui.rs')
-rw-r--r-- | ticket/src/tui.rs | 295 |
1 files changed, 212 insertions, 83 deletions
diff --git a/ticket/src/tui.rs b/ticket/src/tui.rs index 85f60aa..a4fdd35 100644 --- a/ticket/src/tui.rs +++ b/ticket/src/tui.rs @@ -2,19 +2,27 @@ use crate::{ actions::{ get_closed_tickets, get_open_tickets, + save_ticket, + uuid_v1, }, + Comment, + Name, Status, Ticket, }; use anyhow::Result; +use configamajig::{ + get_user_config, + UserConfig, +}; use crossterm::{ - cursor::Hide, event::{ self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode, + KeyEvent, }, queue, terminal::*, @@ -23,19 +31,27 @@ use std::{ collections::BTreeMap, io::{ self, + BufWriter, Write, }, - sync::mpsc, + sync::mpsc::{ + self, + Receiver, + }, thread, time::Duration, }; use tui::{ - backend::CrosstermBackend, + backend::{ + Backend, + CrosstermBackend, + }, layout::{ Alignment, Constraint, Direction, Layout, + Rect, }, style::{ Color, @@ -52,6 +68,7 @@ use tui::{ Text, Widget, }, + Frame, Terminal, }; @@ -66,14 +83,12 @@ impl<'a> TabsState<'a> { } pub fn next(&mut self) { - self.index = (self.index + 1) % self.titles.len(); + self.index = (self.index + 1) % self.titles.len() } pub fn previous(&mut self) { if self.index > 0 { - self.index -= 1; - } else { - self.index = self.titles.len() - 1; + self.index = (self.index - 1) % self.titles.len() } } } @@ -83,13 +98,13 @@ pub enum Event<I> { } pub struct TicketState { - pub tickets: BTreeMap<String, Vec<Ticket>>, + pub tickets: BTreeMap<String, Vec<(Ticket, String)>>, pub index: usize, pub status: Status, } impl TicketState { - pub fn new(tickets: BTreeMap<String, Vec<Ticket>>) -> Self { + pub fn new(tickets: BTreeMap<String, Vec<(Ticket, String)>>) -> Self { Self { tickets, index: 0, @@ -105,14 +120,12 @@ impl TicketState { } pub fn next(&mut self) { - self.index = (self.index + 1) % self.len(); + self.index = (self.index + 1) % self.len() } pub fn previous(&mut self) { if self.index > 0 { - self.index -= 1; - } else { - self.index = self.len() - 1; + self.index = (self.index - 1) % self.len() } } } @@ -136,25 +149,40 @@ impl Default for Config { } } pub fn run() -> Result<()> { + let stdout = io::stdout(); + let mut lock = BufWriter::new(stdout.lock()); // Terminal initialization enable_raw_mode()?; - queue!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, Hide)?; - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend)?; + queue!(lock, EnterAlternateScreen, EnableMouseCapture)?; + let mut terminal = Terminal::new(CrosstermBackend::new(lock))?; + terminal.backend_mut().hide_cursor()?; + terminal.clear()?; // App let mut app = App { tabs: TabsState::new(vec!["Open", "Closed"]), tickets: { let mut map = BTreeMap::new(); - let _ = map.insert("Open".into(), get_open_tickets()?); - let _ = map.insert("Closed".into(), get_closed_tickets()?); + let _ = map.insert( + "Open".into(), + get_open_tickets()? + .into_iter() + .map(|i| (i, String::new())) + .collect(), + ); + let _ = map.insert( + "Closed".into(), + get_closed_tickets()? + .into_iter() + .map(|i| (i, String::new())) + .collect(), + ); TicketState::new(map) }, should_quit: false, }; - terminal.clear()?; + // Spawn event sender thread let (tx, rx) = mpsc::channel(); let _ = thread::spawn(move || { loop { @@ -169,13 +197,29 @@ pub fn run() -> Result<()> { } }); - // Main loop + // Cached Values + let user_config = get_user_config()?; + + // Main drawing and event receiving loop loop { + let status = match app.tickets.status { + Status::Open => "Open", + Status::Closed => "Closed", + }; + terminal.draw(|mut f| { let size = f.size(); let vertical = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .constraints( + [ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + Constraint::Length(3), + ] + .as_ref(), + ) .split(size); let horizontal = Layout::default() .direction(Direction::Horizontal) @@ -183,75 +227,105 @@ pub fn run() -> Result<()> { .constraints( [Constraint::Percentage(30), Constraint::Percentage(70)].as_ref(), ) - .split(size); + .split(Rect { + x: size.x, + y: size.y, + width: size.width, + height: size.height - 3, + }); + app.tabs(&mut f, vertical[0]); + app.table(status, &mut f, horizontal[0]); + app.description(status, &mut f, horizontal[1]); + app.comment(status, &mut f, vertical[2]); + App::instructions(&mut f, vertical[3]); + })?; - Tabs::default() - .block(Block::default().borders(Borders::ALL).title("Status")) - .titles(&app.tabs.titles) - .select(app.tabs.index) - .style(Style::default().fg(Color::Cyan)) - .highlight_style(Style::default().fg(Color::Yellow)) - .render(&mut f, vertical[0]); + handle_event(&rx, &mut app, &user_config, &status)?; - match app.tabs.index { - 0 => { - app.table("Open").render(&mut f, horizontal[0]); + if app.should_quit { + let open = app.tickets.tickets["Open"].iter(); + let closed = app.tickets.tickets["Closed"].iter(); + for t in open.chain(closed) { + save_ticket(&t.0)?; + } + break; + } + } - Paragraph::new(app.description("Open").iter()) - .block(Block::default().borders(Borders::ALL)) - .alignment(Alignment::Left) - .wrap(true) - .render(&mut f, horizontal[1]); - } - 1 => { - app.table("Closed").render(&mut f, horizontal[0]); + // Clean up terminal + queue!( + io::stdout().lock(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.backend_mut().show_cursor()?; + disable_raw_mode()?; - Paragraph::new(app.description("Closed").iter()) - .block(Block::default().borders(Borders::ALL)) - .alignment(Alignment::Left) - .wrap(true) - .render(&mut f, horizontal[1]); - } - _ => {} - } - })?; + Ok(()) +} - match rx.recv()? { - Event::Input(event) => match event.code { - KeyCode::Char('q') => { - app.should_quit = true; +fn handle_event( + rx: &Receiver<Event<KeyEvent>>, + app: &mut App, + user_config: &UserConfig, + status: &str, +) -> Result<()> { + match rx.recv()? { + Event::Input(event) => match event.code { + KeyCode::Esc => { + app.should_quit = true; + } + KeyCode::Right => { + if app.tabs.index == 0 { + app.tickets.status = Status::Closed; + app.tickets.index = 0; } - KeyCode::Right => { - if app.tabs.index == 0 { - app.tickets.status = Status::Closed; - app.tickets.index = 0; - } - app.tabs.next(); + app.tabs.next(); + } + KeyCode::Left => { + if app.tabs.index > 0 { + app.tickets.status = Status::Open; + app.tickets.index = 0; } - KeyCode::Left => { - if app.tabs.index != 0 { - app.tickets.status = Status::Open; - app.tickets.index = 0; - } - app.tabs.previous(); + app.tabs.previous(); + } + KeyCode::Up => app.tickets.previous(), + KeyCode::Down => app.tickets.next(), + KeyCode::Backspace => { + let _ = app.tickets.tickets.get_mut(status).unwrap()[app.tickets.index] + .1 + .pop(); + } + KeyCode::Char(c) => { + app.tickets.tickets.get_mut(status).unwrap()[app.tickets.index] + .1 + .push(c); + } + KeyCode::Enter => { + let ticket = + &mut app.tickets.tickets.get_mut(status).unwrap()[app.tickets.index]; + if !ticket.1.is_empty() { + let _ = ticket.0.comments.insert( + uuid_v1()?, + ( + user_config.uuid, + Name(user_config.name.clone()), + Comment(ticket.1.clone()), + ), + ); + ticket.1.clear(); } - KeyCode::Up => app.tickets.previous(), - KeyCode::Down => app.tickets.next(), - _ => {} - }, - Event::Tick => continue, - } - if app.should_quit { - break; - } + } + _ => {} + }, + Event::Tick => (), } - queue!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; - disable_raw_mode()?; Ok(()) } impl<'a> App<'a> { - fn table(&self, tab: &'a str) -> impl Widget + '_ { + #[inline] + fn table(&self, tab: &'a str, f: &mut Frame<impl Backend>, rect: Rect) { Table::new( ["Id", "Title"].iter(), self @@ -262,7 +336,8 @@ impl<'a> App<'a> { .iter() .enumerate() .map(move |(idx, i)| { - let data = vec![i.id.to_string(), i.title.to_string()].into_iter(); + let data = + vec![i.0.id.to_string(), i.0.title.to_string()].into_iter(); let normal_style = Style::default().fg(Color::Yellow); let selected_style = Style::default().fg(Color::White).modifier(Modifier::BOLD); @@ -278,9 +353,11 @@ impl<'a> App<'a> { .widths(&[Constraint::Percentage(30), Constraint::Percentage(70)]) .style(Style::default().fg(Color::White)) .column_spacing(1) + .render(f, rect) } - fn description(&self, tab: &'a str) -> Vec<Text> { + #[inline] + fn description(&self, tab: &'a str, f: &mut Frame<impl Backend>, rect: Rect) { let mut description = vec![]; for (idx, i) in self.tickets.tickets.get(tab).unwrap().iter().enumerate() { if idx == self.tickets.index { @@ -288,13 +365,15 @@ impl<'a> App<'a> { let header = Style::default().fg(Color::Red).modifier(Modifier::BOLD); let mut desc = vec![ Text::styled("Description\n-------------\n", header), - Text::raw(i.description.to_owned()), + Text::raw(i.0.description.to_owned()), ]; let name_style = Style::default().fg(Color::Cyan).modifier(Modifier::BOLD); - if i.comments.is_empty() { + if i.0.comments.is_empty() { + desc.push(Text::styled("\nComments\n--------\n", header)); + } else { desc.push(Text::styled("\nComments\n--------\n", header)); - for (_, name, comment) in i.comments.values() { + for (_, name, comment) in i.0.comments.values() { desc.push(Text::styled(format!("\n{}\n", name.0), name_style)); desc.push(Text::raw(format!("{}\n", comment.0))); } @@ -305,6 +384,56 @@ impl<'a> App<'a> { } } - description + Paragraph::new(description.iter()) + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Left) + .wrap(true) + .render(f, rect); + } + + #[inline] + fn comment(&self, tab: &'a str, f: &mut Frame<impl Backend>, rect: Rect) { + let (_, s) = &self.tickets.tickets.get(tab).unwrap()[self.tickets.index]; + let mut text = String::from("> "); + text.push_str(&s); + + Paragraph::new([Text::raw(text)].iter()) + .block(Block::default().borders(Borders::ALL).title("Comment")) + .alignment(Alignment::Left) + .wrap(true) + .render(f, rect); + } + + #[inline] + fn tabs(&self, f: &mut Frame<impl Backend>, rect: Rect) { + Tabs::default() + .block(Block::default().borders(Borders::ALL).title("Status")) + .titles(&self.tabs.titles) + .select(self.tabs.index) + .style(Style::default().fg(Color::Cyan)) + .highlight_style(Style::default().fg(Color::Yellow)) + .render(f, rect); + } + + #[inline] + fn instructions(f: &mut Frame<impl Backend>, rect: Rect) { + let blue = Style::default().fg(Color::Blue).modifier(Modifier::BOLD); + Paragraph::new( + [ + Text::Styled("[ESC] ".into(), blue), + Text::Raw("- Exit ".into()), + Text::Styled("[Enter] ".into(), blue), + Text::Raw("- Comment ".into()), + Text::Styled("[Char] ".into(), blue), + Text::Raw("- Write a comment ".into()), + Text::Styled("[Backspace] ".into(), blue), + Text::Raw("- Delete a character".into()), + ] + .iter(), + ) + .block(Block::default().borders(Borders::ALL).title("Instructions")) + .alignment(Alignment::Left) + .wrap(true) + .render(f, rect); } } |