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;
|
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
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/ui.rs
56
src/ui.rs
@ -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
|
||||||
let chunks = Layout::default()
|
.draw(|rect| {
|
||||||
.direction(Direction::Vertical)
|
let chunks = Layout::default()
|
||||||
// .margin(1)
|
.direction(Direction::Vertical)
|
||||||
.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> {
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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*",
|
||||||
|
@ -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)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user