first working state

This commit is contained in:
Thomas Eppers 2021-08-25 12:23:15 +02:00
parent 3323423dcc
commit 0031c46d4b
6 changed files with 150 additions and 175 deletions

View File

@ -1,82 +1,7 @@
use chrono::DateTime; mod tags;
use serde::Deserialize;
mod ui; mod ui;
mod widget; mod widget;
#[derive(Deserialize)]
struct Image {
architecture: String,
os: String,
size: i32,
last_pulled: String,
last_pushed: String,
}
#[derive(Deserialize)]
struct Result {
images: Vec<Image>,
last_updater_username: String,
#[serde(rename(deserialize = "name"))]
tag_name: String,
last_updated: String,
}
#[derive(Deserialize)]
struct Tags {
count: i32,
next_page: Option<String>,
prev_page: Option<String>,
results: Vec<Result>,
}
fn main() { fn main() {
//docker hub exposes tags stored in json at the following url ui::Ui::run();
//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())
}
} }

94
src/tags.rs Normal file
View File

@ -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<ImageDetails>,
last_updater_username: String,
#[serde(rename(deserialize = "name"))]
tag_name: String,
last_updated: String,
}
#[derive(Deserialize)]
struct TagList {
count: i32,
next_page: Option<String>,
prev_page: Option<String>,
results: Vec<Images>,
}
pub enum Error {
InvalidCharacter(char),
Fetching(String),
Converting(String),
}
pub struct Tags {}
impl Tags {
pub fn get_tags(repo: String) -> Result<Vec<String>, 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())
}
}
}

View File

@ -1,16 +1,16 @@
use core::time::Duration;
use std::sync::mpsc; use std::sync::mpsc;
use std::{io, thread}; use std::{io, thread};
use termion::event::{Event, Key}; use termion::event::Key;
use termion::input::TermRead; use termion::input::TermRead;
use termion::raw::IntoRawMode; use termion::raw::IntoRawMode;
use tui::backend::TermionBackend; use tui::backend::TermionBackend;
use tui::layout::{Alignment, Constraint, Direction, Layout}; use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Paragraph};
use tui::Terminal; use tui::Terminal;
use crate::tags;
use crate::widget::repo_entry;
use crate::widget::tag_list;
use crate::widget::Widget; use crate::widget::Widget;
pub struct Ui { pub struct Ui {
@ -26,22 +26,17 @@ pub enum State {
} }
impl Ui { impl Ui {
pub fn new() -> Result<Self, io::Error> { pub fn run() {
let mut ui = Ui { let mut ui = Ui {
state: State::EditRepo, state: State::EditRepo,
repo: crate::widget::repo_entry::RepoEntry::new("This is a text"), repo: repo_entry::RepoEntry::new("This is a text"),
tags: crate::widget::tag_list::TagList::new(vec![ tags: tag_list::TagList::new(vec![String::from("editing Repository")]),
String::from("first"),
String::from("second"),
String::from("third"),
String::from("sdfs"),
]),
}; };
//setup tui //setup tui
let stdout = io::stdout().into_raw_mode()?; let stdout = io::stdout().into_raw_mode().unwrap();
let backend = TermionBackend::new(stdout); let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend).unwrap();
//setup input thread //setup input thread
let receiver = ui.spawn_stdin_channel(); let receiver = ui.spawn_stdin_channel();
@ -49,17 +44,18 @@ impl Ui {
//core interaction loop //core interaction loop
'core: loop { 'core: loop {
//draw //draw
terminal.draw(|rect| { terminal
.draw(|rect| {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
// .margin(1)
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
.split(rect.size()); .split(rect.size());
rect.render_widget(ui.repo.render(), chunks[0]); rect.render_widget(ui.repo.render(), chunks[0]);
let (list, state) = ui.tags.render(); let (list, state) = ui.tags.render();
rect.render_stateful_widget(list, chunks[1], state); rect.render_stateful_widget(list, chunks[1], state);
})?; })
.unwrap();
//handle input //handle input
match receiver.try_recv() { match receiver.try_recv() {
@ -74,27 +70,31 @@ impl Ui {
ui.state = State::SelectTag; ui.state = State::SelectTag;
ui.repo.confirm(); ui.repo.confirm();
//TODO query tags and show them switch //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) => { Ok(Key::Down) => {
if ui.state == State::SelectTag { if ui.state == State::SelectTag {
//TODO select tag
ui.tags.next(); ui.tags.next();
} }
} }
Ok(Key::Up) => { Ok(Key::Up) => {
if ui.state == State::SelectTag { if ui.state == State::SelectTag {
//TODO select tag
ui.tags.previous(); ui.tags.previous();
} }
} }
Ok(Key::Backspace) => { Ok(Key::Backspace) => {
ui.state = State::EditRepo; ui.state = State::EditRepo;
ui.repo.input(Key::Backspace); ui.repo.input(Key::Backspace);
ui.tags = tag_list::TagList::new(vec![String::from("editing Repository")]);
} }
Ok(key) => { Ok(key) => {
ui.state = State::EditRepo; ui.state = State::EditRepo;
ui.repo.input(key); 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) //sleep for 64ms (15 fps)
thread::sleep(std::time::Duration::from_millis(32)); thread::sleep(std::time::Duration::from_millis(32));
} }
Ok(ui)
} }
pub fn spawn_stdin_channel(&self) -> mpsc::Receiver<termion::event::Key> { pub fn spawn_stdin_channel(&self) -> mpsc::Receiver<termion::event::Key> {

View File

@ -6,56 +6,3 @@ pub mod tag_list;
pub trait Widget { pub trait Widget {
fn input(&mut self, event: termion::event::Key); fn input(&mut self, event: termion::event::Key);
} }
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn new() -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items: Vec::new(),
}
}
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
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);
}
}

View File

@ -18,6 +18,10 @@ impl RepoEntry {
} }
} }
pub fn get(&self) -> String {
self.text.clone()
}
pub fn render(&self) -> Paragraph { pub fn render(&self) -> Paragraph {
let title = match self.changed { let title = match self.changed {
true => "Repository*", true => "Repository*",

View File

@ -1,47 +1,54 @@
use termion::event::Key;
use tui::layout::Alignment;
use tui::style::{Color, Style}; use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List, ListState}; use tui::widgets::{Block, Borders, List, ListState};
pub struct TagList { pub struct TagList {
list: super::StatefulList<String>, list: Vec<String>,
state: ListState,
} }
impl TagList { impl TagList {
pub fn new(items: Vec<String>) -> Self { pub fn new(items: Vec<String>) -> Self {
Self { Self {
list: super::StatefulList::with_items(items), list: items,
state: ListState::default(),
} }
} }
pub fn render(&mut self) -> (List, &mut ListState) { pub fn render(&mut self) -> (List, &mut ListState) {
let items: Vec<tui::widgets::ListItem> = self let items: Vec<tui::widgets::ListItem> = self
.list .list
.items
.iter() .iter()
.map(|i| { .map(|l| {
let lines = vec![tui::text::Spans::from(i.as_ref())]; tui::widgets::ListItem::new(l.as_ref())
tui::widgets::ListItem::new(lines)
.style(Style::default().fg(Color::White).bg(Color::Black)) .style(Style::default().fg(Color::White).bg(Color::Black))
}) })
.collect(); .collect();
// Create a List from all list items and highlight the currently selected one // Create a List from all list items and highlight the currently selected one
let items = List::new(items) let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("Tags")) .block(Block::default().title("Tags").borders(Borders::ALL))
.highlight_style( .style(Style::default().fg(Color::White).bg(Color::Black))
Style::default().bg(Color::Black), // .add_modifier(tui::style::Modifier::BOLD), .highlight_style(Style::default().bg(Color::Black))
) .highlight_symbol(">>");
.highlight_symbol(">> ");
(items, &mut self.list.state) (items, &mut self.state)
} }
pub fn next(&mut self) { 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) { 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)),
}
} }
} }