aboutsummaryrefslogtreecommitdiff
path: root/ticket/src/tui.rs
diff options
context:
space:
mode:
Diffstat (limited to 'ticket/src/tui.rs')
-rw-r--r--ticket/src/tui.rs295
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);
}
}