aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Gattozzi <mgattozzi@gmail.com>2019-12-20 21:25:55 -0500
committerMichael Gattozzi <mgattozzi@gmail.com>2019-12-20 21:25:55 -0500
commit50badce3dd78dde6baab913ea6af74966af526d3 (patch)
tree5e9794305566c372e5df15243709c217bf697376
parent82faafde2c493a4a61fcf71e6c650552699053e7 (diff)
Upgrade tui to allow commenting from it
This is a fairly large overhaul of ticket but lays down the last bit of groundwork needed before an initial release. It handles input to write comments, refactors the code a bit to be cleaner and less computation heavy, and also adds instructions on the bottom for how to use the tui. This should be enough for people to start using it, though obviously there's more work to go, but it feels more usable than before.
-rw-r--r--ticket/src/actions.rs3
-rw-r--r--ticket/src/main.rs8
-rw-r--r--ticket/src/tui.rs295
3 files changed, 217 insertions, 89 deletions
diff --git a/ticket/src/actions.rs b/ticket/src/actions.rs
index d2ebf24..42f360b 100644
--- a/ticket/src/actions.rs
+++ b/ticket/src/actions.rs
@@ -119,8 +119,7 @@ pub fn uuid_v1() -> Result<Uuid> {
)?)
}
-#[allow(clippy::needless_pass_by_value)]
-pub fn save_ticket(ticket: Ticket) -> Result<()> {
+pub fn save_ticket(ticket: &Ticket) -> Result<()> {
fs::write(ticket_path(&ticket)?, toml::to_string_pretty(&ticket)?)?;
Ok(())
}
diff --git a/ticket/src/main.rs b/ticket/src/main.rs
index 2903db1..88e86cd 100644
--- a/ticket/src/main.rs
+++ b/ticket/src/main.rs
@@ -135,7 +135,7 @@ fn new() -> Result<()> {
version: Version::V1,
};
- save_ticket(t)
+ save_ticket(&t)
}
fn show(id: Uuid) -> Result<()> {
@@ -182,7 +182,7 @@ fn close(id: Uuid) -> Result<()> {
if ticket.id == id {
let path = ticket_path(&ticket)?;
ticket.status = Status::Closed;
- save_ticket(ticket)?;
+ save_ticket(&ticket)?;
fs::remove_file(path)?;
found = true;
break;
@@ -217,7 +217,7 @@ fn migrate() -> Result<()> {
t.number,
ticket_file_name(&ticket)
)))?;
- save_ticket(ticket)?;
+ save_ticket(&ticket)?;
// We need to make sure we get different times for each ticket
// Possible future migrations might not have this issue
thread::sleep(time::Duration::from_millis(1000));
@@ -237,7 +237,7 @@ fn comment(id: Uuid, message: String) -> Result<()> {
uuid_v1()?,
(user_config.uuid, Name(user_config.name), Comment(message)),
);
- save_ticket(ticket)?;
+ save_ticket(&ticket)?;
Ok(())
}
#[derive(Serialize, Deserialize, Debug)]
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);
}
}