diff --git a/src/main.rs b/src/main.rs index d76ccf1..0ee5136 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,82 +1,7 @@ -use chrono::DateTime; -use serde::Deserialize; - +mod tags; mod ui; mod widget; -#[derive(Deserialize)] -struct Image { - architecture: String, - os: String, - size: i32, - last_pulled: String, - last_pushed: String, -} - -#[derive(Deserialize)] -struct Result { - images: Vec, - last_updater_username: String, - #[serde(rename(deserialize = "name"))] - tag_name: String, - last_updated: String, -} - -#[derive(Deserialize)] -struct Tags { - count: i32, - next_page: Option, - prev_page: Option, - results: Vec, -} - fn main() { - //docker hub exposes tags stored in json at the following url - //https://hub.docker.com/v2/repositories/rocketchat/rocket.chat/tags - - //TODO fill them dynamic instead of hardcoded - let group = "rocketchat"; - let repo = "rocket.chat"; - let request = format!( - "https://hub.docker.com/v2/repositories/{}/{}/tags", - group, repo - ); - - //get response - let res = reqwest::blocking::get(request).unwrap(); - - //convert it to json - let raw = res.text().unwrap(); - let tags: Tags = serde_json::from_str(&raw).unwrap(); - - let now = chrono::Utc::now(); - for result in tags.results { - let rfc3339 = DateTime::parse_from_rfc3339(&result.last_updated).unwrap(); - let dif = now - rfc3339.with_timezone(&chrono::Utc); - println!("{} vor {}", result.tag_name, format_time_nice(dif)); - } - - ui::Ui::new(); -} - -fn format_time_nice(time: chrono::Duration) -> String { - if time.num_weeks() == 52 { - format!("{} Jahr", (time.num_weeks() / 52) as i32) - } else if time.num_weeks() > 103 { - format!("{} Jahren", (time.num_weeks() / 52) as i32) - } else if time.num_days() == 1 { - format!("{} Tag", time.num_days()) - } else if time.num_days() > 1 { - format!("{} Tagen", time.num_days()) - } else if time.num_hours() == 1 { - format!("{} Stunde", time.num_hours()) - } else if time.num_hours() > 1 { - format!("{} Stunden", time.num_hours()) - } else if time.num_minutes() == 1 { - format!("{} Minute", time.num_minutes()) - } else if time.num_minutes() > 1 { - format!("{} Minuten", time.num_minutes()) - } else { - format!("{} Sekunden", time.num_seconds()) - } + ui::Ui::run(); } diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 0000000..2b4a1b2 --- /dev/null +++ b/src/tags.rs @@ -0,0 +1,94 @@ +use chrono::DateTime; +use serde::Deserialize; + +#[derive(Deserialize)] +struct ImageDetails { + architecture: String, + os: String, + size: i32, + last_pulled: String, + last_pushed: String, +} + +#[derive(Deserialize)] +struct Images { + images: Vec, + last_updater_username: String, + #[serde(rename(deserialize = "name"))] + tag_name: String, + last_updated: String, +} + +#[derive(Deserialize)] +struct TagList { + count: i32, + next_page: Option, + prev_page: Option, + results: Vec, +} + +pub enum Error { + InvalidCharacter(char), + Fetching(String), + Converting(String), +} + +pub struct Tags {} + +impl Tags { + pub fn get_tags(repo: String) -> Result, Error> { + let request = format!("https://hub.docker.com/v2/repositories/{}/tags", repo); + + //check for right set of characters + if request.bytes().any(|c| !c.is_ascii()) { + return Err(Error::InvalidCharacter('a')); + } + + //get response + let res = match reqwest::blocking::get(request) { + Ok(result) => result, + Err(_) => return Err(Error::Fetching(String::from("reqwest error"))), + }; + + //convert it to json + let raw = res.text().unwrap(); + let tags: TagList = match serde_json::from_str(&raw) { + Ok(result) => result, + Err(_) => return Err(Error::Converting(String::from("invalid json"))), + }; + + let now = chrono::Utc::now(); + + Ok(tags + .results + .iter() + .map(|r| { + let rfc3339 = DateTime::parse_from_rfc3339(&r.last_updated).unwrap(); + let dif = now - rfc3339.with_timezone(&chrono::Utc); + format!("{} vor {}", r.tag_name, Self::format_time_nice(dif)) + }) + .collect()) + } + + fn format_time_nice(time: chrono::Duration) -> String { + if time.num_weeks() == 52 { + format!("{} Jahr", (time.num_weeks() / 52) as i32) + } else if time.num_weeks() > 103 { + format!("{} Jahren", (time.num_weeks() / 52) as i32) + } else if time.num_days() == 1 { + format!("{} Tag", time.num_days()) + } else if time.num_days() > 1 { + format!("{} Tagen", time.num_days()) + } else if time.num_hours() == 1 { + format!("{} Stunde", time.num_hours()) + } else if time.num_hours() > 1 { + format!("{} Stunden", time.num_hours()) + } else if time.num_minutes() == 1 { + format!("{} Minute", time.num_minutes()) + } else if time.num_minutes() > 1 { + format!("{} Minuten", time.num_minutes()) + } else { + format!("{} Sekunden", time.num_seconds()) + } + } +} diff --git a/src/ui.rs b/src/ui.rs index 1641138..506d7ff 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,16 +1,16 @@ -use core::time::Duration; use std::sync::mpsc; use std::{io, thread}; -use termion::event::{Event, Key}; +use termion::event::Key; use termion::input::TermRead; use termion::raw::IntoRawMode; use tui::backend::TermionBackend; -use tui::layout::{Alignment, Constraint, Direction, Layout}; -use tui::style::{Color, Style}; -use tui::widgets::{Block, Borders, Paragraph}; +use tui::layout::{Constraint, Direction, Layout}; use tui::Terminal; +use crate::tags; +use crate::widget::repo_entry; +use crate::widget::tag_list; use crate::widget::Widget; pub struct Ui { @@ -26,22 +26,17 @@ pub enum State { } impl Ui { - pub fn new() -> Result { + pub fn run() { let mut ui = Ui { state: State::EditRepo, - repo: crate::widget::repo_entry::RepoEntry::new("This is a text"), - tags: crate::widget::tag_list::TagList::new(vec![ - String::from("first"), - String::from("second"), - String::from("third"), - String::from("sdfs"), - ]), + repo: repo_entry::RepoEntry::new("This is a text"), + tags: tag_list::TagList::new(vec![String::from("editing Repository")]), }; //setup tui - let stdout = io::stdout().into_raw_mode()?; + let stdout = io::stdout().into_raw_mode().unwrap(); let backend = TermionBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; + let mut terminal = Terminal::new(backend).unwrap(); //setup input thread let receiver = ui.spawn_stdin_channel(); @@ -49,17 +44,18 @@ impl Ui { //core interaction loop 'core: loop { //draw - terminal.draw(|rect| { - let chunks = Layout::default() - .direction(Direction::Vertical) - // .margin(1) - .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) - .split(rect.size()); + terminal + .draw(|rect| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) + .split(rect.size()); - rect.render_widget(ui.repo.render(), chunks[0]); - let (list, state) = ui.tags.render(); - rect.render_stateful_widget(list, chunks[1], state); - })?; + rect.render_widget(ui.repo.render(), chunks[0]); + let (list, state) = ui.tags.render(); + rect.render_stateful_widget(list, chunks[1], state); + }) + .unwrap(); //handle input match receiver.try_recv() { @@ -74,27 +70,31 @@ impl Ui { ui.state = State::SelectTag; ui.repo.confirm(); //TODO query tags and show them switch + match tags::Tags::get_tags(ui.repo.get()) { + Ok(lines) => ui.tags = tag_list::TagList::new(lines), + Err(_) => (), + } } } Ok(Key::Down) => { if ui.state == State::SelectTag { - //TODO select tag ui.tags.next(); } } Ok(Key::Up) => { if ui.state == State::SelectTag { - //TODO select tag ui.tags.previous(); } } Ok(Key::Backspace) => { ui.state = State::EditRepo; ui.repo.input(Key::Backspace); + ui.tags = tag_list::TagList::new(vec![String::from("editing Repository")]); } Ok(key) => { ui.state = State::EditRepo; ui.repo.input(key); + ui.tags = tag_list::TagList::new(vec![String::from("editing Repository")]); } _ => (), } @@ -102,8 +102,6 @@ impl Ui { //sleep for 64ms (15 fps) thread::sleep(std::time::Duration::from_millis(32)); } - - Ok(ui) } pub fn spawn_stdin_channel(&self) -> mpsc::Receiver { diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 05bb3a3..86ef6e1 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -6,56 +6,3 @@ pub mod tag_list; pub trait Widget { fn input(&mut self, event: termion::event::Key); } - -pub struct StatefulList { - pub state: ListState, - pub items: Vec, -} - -impl StatefulList { - pub fn new() -> StatefulList { - StatefulList { - state: ListState::default(), - items: Vec::new(), - } - } - - pub fn with_items(items: Vec) -> StatefulList { - StatefulList { - state: ListState::default(), - items, - } - } - - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.items.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.items.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } - - pub fn unselect(&mut self) { - self.state.select(None); - } -} diff --git a/src/widget/repo_entry.rs b/src/widget/repo_entry.rs index d31d888..86bcd40 100644 --- a/src/widget/repo_entry.rs +++ b/src/widget/repo_entry.rs @@ -18,6 +18,10 @@ impl RepoEntry { } } + pub fn get(&self) -> String { + self.text.clone() + } + pub fn render(&self) -> Paragraph { let title = match self.changed { true => "Repository*", diff --git a/src/widget/tag_list.rs b/src/widget/tag_list.rs index e95208f..37bf506 100644 --- a/src/widget/tag_list.rs +++ b/src/widget/tag_list.rs @@ -1,47 +1,54 @@ -use termion::event::Key; -use tui::layout::Alignment; use tui::style::{Color, Style}; use tui::widgets::{Block, Borders, List, ListState}; pub struct TagList { - list: super::StatefulList, + list: Vec, + state: ListState, } impl TagList { pub fn new(items: Vec) -> Self { Self { - list: super::StatefulList::with_items(items), + list: items, + state: ListState::default(), } } pub fn render(&mut self) -> (List, &mut ListState) { let items: Vec = self .list - .items .iter() - .map(|i| { - let lines = vec![tui::text::Spans::from(i.as_ref())]; - tui::widgets::ListItem::new(lines) + .map(|l| { + tui::widgets::ListItem::new(l.as_ref()) .style(Style::default().fg(Color::White).bg(Color::Black)) }) .collect(); // Create a List from all list items and highlight the currently selected one let items = List::new(items) - .block(Block::default().borders(Borders::ALL).title("Tags")) - .highlight_style( - Style::default().bg(Color::Black), // .add_modifier(tui::style::Modifier::BOLD), - ) - .highlight_symbol(">> "); + .block(Block::default().title("Tags").borders(Borders::ALL)) + .style(Style::default().fg(Color::White).bg(Color::Black)) + .highlight_style(Style::default().bg(Color::Black)) + .highlight_symbol(">>"); - (items, &mut self.list.state) + (items, &mut self.state) } pub fn next(&mut self) { - self.list.next(); + match self.state.selected() { + None if self.list.len() > 0 => self.state.select(Some(0)), + None => (), + Some(i) if i == self.list.len() - 1 => self.state.select(Some(0)), + Some(i) => self.state.select(Some(i + 1)), + } } pub fn previous(&mut self) { - self.list.previous(); + match self.state.selected() { + None if self.list.len() > 0 => self.state.select(Some(self.list.len())), + None => (), + Some(i) if i == 0 => self.state.select(Some(self.list.len() - 1)), + Some(i) => self.state.select(Some(i - 1)), + } } }