first working state
This commit is contained in:
parent
3323423dcc
commit
0031c46d4b
79
src/main.rs
79
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<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() {
|
||||
//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();
|
||||
}
|
||||
|
94
src/tags.rs
Normal file
94
src/tags.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
42
src/ui.rs
42
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<Self, io::Error> {
|
||||
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| {
|
||||
terminal
|
||||
.draw(|rect| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
// .margin(1)
|
||||
.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);
|
||||
})?;
|
||||
})
|
||||
.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<termion::event::Key> {
|
||||
|
@ -6,56 +6,3 @@ pub mod tag_list;
|
||||
pub trait Widget {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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*",
|
||||
|
@ -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<String>,
|
||||
list: Vec<String>,
|
||||
state: ListState,
|
||||
}
|
||||
|
||||
impl TagList {
|
||||
pub fn new(items: Vec<String>) -> Self {
|
||||
Self {
|
||||
list: super::StatefulList::with_items(items),
|
||||
list: items,
|
||||
state: ListState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> (List, &mut ListState) {
|
||||
let items: Vec<tui::widgets::ListItem> = 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user