Compare commits

..

No commits in common. "master" and "e66b75c0218869ed227b877a9150216a23e4f7f2" have entirely different histories.

24 changed files with 518 additions and 1051 deletions

6
.gitignore vendored
View File

@ -1,7 +1 @@
# build files
/target /target
# test files
docker-compose.yml
docker-compose.yaml
docker-compose.yml.yml

View File

@ -1,37 +0,0 @@
# https://woodpecker-ci.org/docs/usage/intro
pipeline:
build_and_test:
image: rust
commands:
- cargo test
- cargo build --release
gitea_on_release:
# http://plugins.drone.io/drone-plugins/drone-gitea-release/
image: plugins/gitea-release
files: target/release/reel-moby
secrets: [gitea_release_api_key, gitea_release_base_url]
when:
event: tag
tag: v*
github_on_release:
# http://plugins.drone.io/drone-plugins/drone-github-release/
image: plugins/github-release
files: target/release/reel-moby
secrets: [github_release_api_key]
when:
event: tag
tag: v*
notify_when_failure:
# http://plugins.drone.io/appleboy/drone-discord/
image: appleboy/drone-discord
secrets: [ discord_webhook_id, discord_webhook_token]
message: "build {{build.number}} or release failed. Fix me please."
when:
status: failure
# http://plugins.drone.io/drone-plugins/drone-github-release/

148
Cargo.lock generated
View File

@ -11,26 +11,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.0.1" version = "1.0.1"
@ -92,21 +72,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.1" version = "0.9.1"
@ -248,15 +213,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -571,30 +527,6 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.28" version = "1.0.28"
@ -604,6 +536,20 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "query-docker-tags"
version = "0.1.0"
dependencies = [
"chrono",
"lazy_static",
"regex",
"reqwest",
"serde",
"serde_json",
"termion",
"tui",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
@ -671,21 +617,6 @@ dependencies = [
"redox_syscall", "redox_syscall",
] ]
[[package]]
name = "reel-moby"
version = "1.2.1"
dependencies = [
"chrono",
"lazy_static",
"regex",
"reqwest",
"serde",
"serde_json",
"structopt",
"termion",
"tui",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.4" version = "1.5.4"
@ -845,36 +776,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa"
dependencies = [
"clap",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.74" version = "1.0.74"
@ -912,15 +813,6 @@ dependencies = [
"redox_termios", "redox_termios",
] ]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.44" version = "0.1.44"
@ -1083,18 +975,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.0" version = "0.3.0"

View File

@ -1,8 +1,8 @@
[package] [package]
name = "reel-moby" name = "query-docker-tags"
version = "1.2.1" version = "0.1.0"
edition = "2021" edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -15,7 +15,6 @@ tui = "0.16"
termion = "1.5" termion = "1.5"
regex = "1.5.4" regex = "1.5.4"
lazy_static = "1.4.0" lazy_static = "1.4.0"
structopt = "0.3.23"
[profile.release] [profile.release]
lto = "yes" lto = "yes"

View File

@ -1,9 +0,0 @@
The MIT License (MIT)
Copyright © 2021 Thomas Eppers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,10 +0,0 @@
# reel-moby
search for new docker tags and update them in your docker-compose file
## Usage
Searches the current folder for a docker-compose.(yml|yaml) file and opens it when it found one. Then it is possible to select a image line. The program then shows the found repository and shows the latest tags. The tags can be scrolled and selected, which updates the opened file.
From that point save the file and pull the new image with `docker-compose up -d` or `docker-compse pull`.
![screenshot](./screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

5
src/common.rs Normal file
View File

@ -0,0 +1,5 @@
pub fn remove_last_char(input: &str) -> &str {
let mut chars = input.chars();
chars.next_back();
chars.as_str()
}

View File

@ -1,28 +0,0 @@
pub trait DisplayDurationExt {
/// displays a duration in a human readable form
fn display(&self) -> String;
}
impl DisplayDurationExt for chrono::Duration {
fn display(&self) -> String {
if self.num_weeks() == 52 {
format!("{} Year", (self.num_weeks() / 52) as i32)
} else if self.num_weeks() > 103 {
format!("{} Years", (self.num_weeks() / 52) as i32)
} else if self.num_days() == 1 {
format!("{} Day", self.num_days())
} else if self.num_days() > 1 {
format!("{} Days", self.num_days())
} else if self.num_hours() == 1 {
format!("{} Hour", self.num_hours())
} else if self.num_hours() > 1 {
format!("{} Hours", self.num_hours())
} else if self.num_minutes() == 1 {
format!("{} Minute", self.num_minutes())
} else if self.num_minutes() > 1 {
format!("{} Minutes", self.num_minutes())
} else {
format!("{} Seconds", self.num_seconds())
}
}
}

View File

@ -1 +0,0 @@
pub mod display_duration_ext;

View File

@ -1,26 +1,9 @@
use std::path::PathBuf;
use structopt::StructOpt;
mod common; mod common;
mod repo; mod repo;
mod repository; mod tags;
mod ui; mod ui;
mod widget; mod widget;
/// helps you searching or updating tags of your used docker images
#[derive(StructOpt, Debug)]
pub struct Opt {
/// A custom path to a docker-compose file
#[structopt(short, long, parse(from_os_str))]
file: Option<PathBuf>,
/// Give a Repository identifier, e.g. library/nginx
#[structopt(short, long, parse(from_str))]
repo: Option<String>,
}
fn main() { fn main() {
//parse parameter ui::Ui::run("enter a repository or select one from docker-compose.yml");
let opt = Opt::from_args();
ui::create_ui(&opt);
} }

View File

@ -2,16 +2,24 @@ use std::fmt;
use regex::Regex; use regex::Regex;
// use crate::common;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
// Conversion,
// Empty,
NoTagFound, NoTagFound,
// InvalidChar,
MisformedInput, MisformedInput,
} }
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
// Error::Conversion => write!(f, "Conversion error"),
// Error::Empty => write!(f, "Input is empty"),
Error::NoTagFound => write!(f, "Expected a tag"), Error::NoTagFound => write!(f, "Expected a tag"),
// Error::InvalidChar => write!(f, "Invalid character found"),
Error::MisformedInput => write!(f, "Unexpected input"), Error::MisformedInput => write!(f, "Unexpected input"),
} }
} }
@ -25,22 +33,18 @@ pub enum Repo {
} }
/// check if yaml line matches and returns the split of repo string and rest /// check if yaml line matches and returns the split of repo string and rest
/// the first &str is the image tag pub fn match_yaml_image(input: &str) -> Option<(&str, &str)> {
/// it will be used to not change the identation
/// the second &str will the the identifier for the image
pub fn match_yaml_image(input: &str) -> Result<(&str, &str), Error> {
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref REGEX: Regex = Regex::new(r"^( +image *: *)([a-z0-9\-\./:]+)").unwrap(); static ref REGEX: Regex = Regex::new(r"^( +image *: *)([a-z0-9\./:]+)").unwrap();
} }
let caps = match REGEX.captures(input) { let caps = match REGEX.captures(input) {
Some(caps) => caps, Some(caps) => caps,
None => return Err(Error::NoTagFound), None => return None,
}; };
Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())) Some((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()))
} }
/// takes the identifier and splits off the tag it exists
pub fn split_tag_from_repo(input: &str) -> Result<(&str, &str), Error> { pub fn split_tag_from_repo(input: &str) -> Result<(&str, &str), Error> {
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref REGEX: Regex = Regex::new(r"^([a-z0-9\./[^:]]*):?([a-z0-9._\-]*)").unwrap(); static ref REGEX: Regex = Regex::new(r"^([a-z0-9\./[^:]]*):?([a-z0-9._\-]*)").unwrap();
@ -63,10 +67,17 @@ pub fn split_tag_from_repo(input: &str) -> Result<(&str, &str), Error> {
Ok((front, back)) Ok((front, back))
} }
/// takes an identifier and changes it to a Repo enum // pub fn split_repo(repo: &str) -> Result<Repo, Error> {
// let split_tag: Vec<&str> = repo.split(":").collect();
// if split_tag.len() == 2 && split_tag[0].len() != 0 && split_tag[1].len() != 0 {
// //
// }
// Ok(Repo::Project("".into()))
// }
pub fn split_repo_without_tag(repo: &str) -> Result<Repo, Error> { pub fn split_repo_without_tag(repo: &str) -> Result<Repo, Error> {
let repo = repo.trim(); let repo = repo.trim();
let split_repo: Vec<&str> = repo.split('/').collect(); let split_repo: Vec<&str> = repo.split("/").collect();
match split_repo.len() { match split_repo.len() {
1 => { 1 => {
let regex = regex::Regex::new(r"[a-z0-9]+").unwrap(); let regex = regex::Regex::new(r"[a-z0-9]+").unwrap();
@ -97,85 +108,118 @@ pub fn split_repo_without_tag(repo: &str) -> Result<Repo, Error> {
} }
} }
pub fn split_tag(repo: &str) -> Result<(&str, &str), Error> {
let split_tag: Vec<&str> = repo.split(":").collect();
if split_tag.len() == 2 && split_tag[0].len() != 0 && split_tag[1].len() != 0 {
Ok((split_tag[0], split_tag[1]))
} else {
Err(Error::NoTagFound)
}
}
// pub fn extract(repo: &str) -> Result<(Option<&str>, Option<&str>, &str), Error> {
// if repo.len() == 0 {
// return Err(Error::Empty);
// }
// let regex = regex::Regex::new(r"([^/:]*?/)??([^/:]*?/)?([^/:]*):?(.*)?").unwrap();
// let caps = match regex.captures(repo) {
// None => return Err(Error::Conversion),
// Some(cap) => cap,
// };
// let server = match caps.get(1) {
// None => None,
// Some(cap) => Some(common::remove_last_char(cap.as_str())),
// };
// let orga = match caps.get(2) {
// None => None,
// Some(cap) => Some(common::remove_last_char(cap.as_str())),
// };
// Ok((server, orga, caps.get(3).unwrap().as_str()))
// }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::repo;
use crate::repo::{Error, Repo}; use crate::repo::{Error, Repo};
// #[test]
// fn test_repo_regex() {
// assert_eq!(repo::extract(""), Err(repo::Error::Empty));
// assert_eq!(
// repo::extract("ghcr.io/library/nginx"),
// Ok((Some("ghcr.io"), Some("library"), "nginx"))
// );
// assert_eq!(
// repo::extract("library/nginx"),
// Ok((None, Some("library"), "nginx"))
// );
// assert_eq!(repo::extract("nginx"), Ok((None, None, "nginx")));
// }
#[test]
fn split_tag() {
assert_eq!(repo::split_tag("nginx:v1"), Ok(("nginx", "v1")));
assert_eq!(repo::split_tag("dsfsdf"), Err(repo::Error::NoTagFound));
assert_eq!(repo::split_tag("nginx:"), Err(repo::Error::NoTagFound));
assert_eq!(repo::split_tag(":v1"), Err(repo::Error::NoTagFound));
assert_eq!(repo::split_tag(":"), Err(repo::Error::NoTagFound));
}
#[test] #[test]
fn test_split_repo_without_tag() { fn test_split_repo_without_tag() {
let input: Vec<(&str, Result<Repo, Error>)> = vec![ use crate::repo::split_repo_without_tag as test_fn;
("", Err(Error::MisformedInput)), assert_eq!(test_fn(""), Err(Error::MisformedInput));
("NGINX", Err(Error::MisformedInput)), assert_eq!(test_fn("NGINX"), Err(Error::MisformedInput));
("nginx", Ok(Repo::Project("nginx".into()))), assert_eq!(test_fn("nginx"), Ok(Repo::Project("nginx".into())));
( assert_eq!(
"library/nginx", test_fn("library/nginx"),
Ok(Repo::WithOrga("library".into(), "nginx".into())), Ok(Repo::WithOrga("library".into(), "nginx".into()))
), );
( assert_eq!(
"ghcr.io/library/nginx", test_fn("ghcr.io/library/nginx"),
Ok(Repo::WithServer( Ok(Repo::WithServer(
"ghcr.io".into(), "ghcr.io".into(),
"library".into(), "library".into(),
"nginx".into(), "nginx".into(),
)), ))
), );
(
"te-st/test-hypen",
Ok(Repo::WithOrga("te-st".into(), "test-hypen".into())),
),
(
"test/test.dot",
Ok(Repo::WithOrga("test".into(), "test.dot".into())),
),
];
for i in input {
assert_eq!(super::split_repo_without_tag(i.0), i.1);
}
} }
#[test] #[test]
fn test_match_yaml_image() { fn test_match_yaml_image() {
let input: Vec<(&str, Result<(&str, &str), Error>)> = vec![ use crate::repo::match_yaml_image as test_fn;
("", Err(Error::NoTagFound)), assert_eq!(test_fn(""), None);
("version: '2'", Err(Error::NoTagFound)), assert_eq!(test_fn("version: '2'"), None);
("image: ", Err(Error::NoTagFound)), assert_eq!(test_fn("image: "), None);
(" image: ", Err(Error::NoTagFound)), assert_eq!(test_fn(" image: "), None);
(" image: nginx", Ok((" image: ", "nginx"))), assert_eq!(test_fn(" image: nginx"), Some((" image: ", "nginx")));
(" image: library/nginx", Ok((" image: ", "library/nginx"))), assert_eq!(
( test_fn(" image: library/nginx"),
" image: gchr.io/library/nginx", Some((" image: ", "library/nginx"))
Ok((" image: ", "gchr.io/library/nginx")), );
), assert_eq!(
(" image: nginx # comment", Ok((" image: ", "nginx"))), test_fn(" image: ghcr.io/library/nginx"),
(" image: test-hyphen", Ok((" image: ", "test-hyphen"))), Some((" image: ", "ghcr.io/library/nginx"))
(" image: test.dot", Ok((" image: ", "test.dot"))), );
]; assert_eq!(test_fn("# image: nginx"), None);
assert_eq!(
for i in input { test_fn(" image: nginx #comment"),
assert_eq!(super::match_yaml_image(i.0), i.1); Some((" image: ", "nginx"))
} );
} }
#[test] #[test]
fn test_split_tag_from_repo() { fn test_split_tag_from_repo() {
let input: Vec<(&str, Result<(&str, &str), super::Error>)> = vec![ use crate::repo::split_tag_from_repo as test_fn;
("nginx", Ok(("nginx", ""))), assert_eq!(test_fn("nginx"), Ok(("nginx", "")));
("library/nginx", Ok(("library/nginx", ""))), assert_eq!(test_fn("library/nginx"), Ok(("library/nginx", "")));
("ghcr.io/library/nginx", Ok(("ghcr.io/library/nginx", ""))), assert_eq!(
("nginx:", Ok(("nginx", ""))), test_fn("ghcr.io/library/nginx"),
("nginx:1", Ok(("nginx", "1"))), Ok(("ghcr.io/library/nginx", ""))
("nginx:latest", Ok(("nginx", "latest"))), );
("hy-phen:latest", Ok(("hy-phen", "latest"))), assert_eq!(test_fn("nginx:"), Ok(("nginx", "")));
("test.dot:latest", Ok(("test.dot", "latest"))), assert_eq!(test_fn("nginx:1"), Ok(("nginx", "1")));
( assert_eq!(test_fn("nginx:latest"), Ok(("nginx", "latest")));
"woodpeckerci/woodpecker-server",
Ok(("woodpeckerci/woodpecker-server", "")),
),
];
for i in input {
assert_eq!(super::split_tag_from_repo(i.0), i.1);
}
} }
} }

View File

@ -1,76 +0,0 @@
use serde::Deserialize;
use crate::repository::Error;
#[derive(Deserialize, Debug, Clone)]
struct ImageDetails {
architecture: String,
os: String,
variant: Option<String>,
size: usize,
}
#[derive(Deserialize, Clone)]
pub struct Images {
images: Vec<ImageDetails>,
#[serde(rename(deserialize = "name"))]
tag_name: String,
last_updated: String,
}
impl Images {
pub fn convert(&self) -> super::Tag {
super::Tag {
name: self.tag_name.clone(),
last_updated: Some(self.last_updated.clone()),
details: self
.images
.iter()
.map(|d| super::TagDetails {
arch: Some(d.architecture.clone()),
variant: Some(d.variant.clone().unwrap_or_default()),
os: Some(d.os.clone()),
size: Some(d.size),
})
.collect(),
}
}
}
#[derive(Deserialize)]
pub struct DockerHub {
#[serde(rename(deserialize = "next"))]
next_page: Option<String>,
results: Vec<Images>,
}
impl DockerHub {
/// fetches tag information with a repository name in the form of organization/repository or library/repository in the case of official images from docker
pub fn create_repo(repo: &str) -> Result<super::Repo, Error> {
let request = format!("https://hub.docker.com/v2/repositories/{}/tags", repo);
Self::with_url(&request)
}
/// fetches tag information from a url
pub fn with_url(url: &str) -> Result<super::Repo, Error> {
let response = match reqwest::blocking::get(url) {
Ok(result) => result,
Err(e) => return Err(Error::Fetching(format!("reqwest error: {}", e))),
};
//convert it to json
let tags = match response.json::<Self>() {
Ok(result) => result,
Err(e) => return Err(Error::Converting(format!("invalid json: {}", e))),
};
if tags.results.is_empty() {
return Err(Error::NoTagsFound);
}
Ok(super::Repo {
tags: tags.results.iter().map(|t| t.convert()).collect(),
next_page: tags.next_page,
})
}
}

View File

@ -1,137 +0,0 @@
mod dockerhub;
use std::fmt;
use chrono::DateTime;
use crate::common::display_duration_ext::DisplayDurationExt;
use crate::repo;
#[derive(Debug, PartialEq)]
pub enum Error {
/// couldn't fetch json with reqwest
Fetching(String),
/// a serde error
Converting(String),
/// invalid repos show a valid json with 0 tags
NoTagsFound,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Fetching(s) => write!(f, "Fetching error: {}", s),
Error::Converting(s) => write!(f, "Converting error: {}", s),
Error::NoTagsFound => write!(f, "Given Repo has 0 tags. Is it valid?"),
}
}
}
#[derive(Clone, PartialEq)]
pub struct TagDetails {
pub arch: Option<String>,
pub variant: Option<String>,
pub os: Option<String>,
pub size: Option<usize>,
}
#[derive(Clone)]
pub struct Tag {
name: String,
details: Vec<TagDetails>,
last_updated: Option<String>,
}
impl Tag {
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_name_with_details(&self) -> String {
let dif = match &self.last_updated {
None => "".to_string(),
Some(last_updated) => {
let now = chrono::Utc::now();
let rfc3339 = DateTime::parse_from_rfc3339(last_updated).unwrap();
let dif = now - rfc3339.with_timezone(&chrono::Utc);
format!(", {} old", dif.display())
}
};
if dif.is_empty() {}
format!("{}{}", self.name, dif)
}
pub fn get_details(&self) -> &Vec<TagDetails> {
&self.details
}
}
pub struct Repo {
tags: Vec<Tag>,
next_page: Option<String>,
}
impl Repo {
pub fn new(repo: &str) -> Result<Self, Error> {
use crate::repo::Repo;
let (registry, repo) = match crate::repo::split_repo_without_tag(repo) {
Ok(Repo::WithServer(reg, org, pro)) => (Some(reg), format!("{}/{}", org, pro)),
Ok(Repo::WithOrga(org, pro)) => (None, format!("{}/{}", org, pro)),
Ok(Repo::Project(pro)) => (None, format!("library/{}", pro)),
Err(e) => return Err(Error::Converting(format!("{}", e))),
};
if registry.unwrap_or_default().is_empty() {
dockerhub::DockerHub::create_repo(&repo)
} else {
return Err(Error::Converting("This registry is not supported".into()));
}
}
pub fn with_url(url: &str) -> Result<Self, Error> {
//TODO fix for other registries
dockerhub::DockerHub::with_url(url)
}
pub fn get_tags(&self) -> &Vec<Tag> {
&self.tags
}
pub fn next_page(&self) -> Option<Self> {
match &self.next_page {
Some(url) => match Self::with_url(url) {
Ok(tags) => Some(tags),
Err(_) => None,
},
None => None,
}
}
}
/// checks the repo name and may add a prefix for official images
pub fn check_repo(name: &str) -> Result<String, Error> {
let repo = match repo::split_tag_from_repo(name) {
Err(e) => return Err(Error::Converting(format!("{}", e))),
Ok((name, _)) => name,
};
match repo::split_repo_without_tag(name) {
Ok(repo::Repo::Project(s)) => Ok(format!("library/{}", s)),
Ok(_) => Ok(repo.to_string()),
Err(e) => Err(Error::Converting(format!("{}", e))),
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_check_repo() {
assert_eq!(super::check_repo("nginx").unwrap(), "library/nginx");
assert_eq!(super::check_repo("library/nginx").unwrap(), "library/nginx");
assert_eq!(
super::check_repo("rocketchat/rocket.chat").unwrap(),
"rocketchat/rocket.chat"
);
}
}

162
src/tags.rs Normal file
View File

@ -0,0 +1,162 @@
use std::fmt;
use crate::repo;
use chrono::DateTime;
use serde::Deserialize;
#[derive(Deserialize)]
struct ImageDetails {
architecture: String,
os: String,
size: usize,
}
#[derive(Deserialize)]
pub struct Images {
images: Vec<ImageDetails>,
#[serde(rename(deserialize = "name"))]
pub tag_name: String,
last_updated: String,
}
#[derive(Deserialize)]
pub struct Tags {
count: usize,
#[serde(rename(deserialize = "next"))]
next_page: Option<String>,
#[serde(rename(deserialize = "previous"))]
prev_page: Option<String>,
pub results: Vec<Images>,
}
#[derive(Debug, PartialEq)]
pub enum Error {
/// repo string contains an illegal character
InvalidCharacter(char),
/// couldn't fetch json with reqwest
Fetching(String),
/// a serde error
Converting(String),
/// invalid repos show a valid json with 0 tags
NoTagsFound,
NoPrevPage,
NoNextPage,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidCharacter(c) => write!(f, "Invalid Character: {}", c),
Error::Fetching(s) => write!(f, "Fetching error: {}", s),
Error::Converting(s) => write!(f, "Converting error: {}", s),
Error::NoNextPage => write!(f, "No next page available"),
Error::NoPrevPage => write!(f, "No previous page available"),
Error::NoTagsFound => write!(f, "Given Repo has 0 tags. Is it valid?"),
}
}
}
impl Tags {
/// fetches tag information with a repository name in the form of organization/repository or library/repository in the case of official images from docker
pub fn new(repo: String) -> Result<Self, Error> {
let request = format!("https://hub.docker.com/v2/repositories/{}/tags", repo);
Self::with_url(&request)
}
/// fetches tag information from a url
fn with_url(url: &str) -> Result<Self, Error> {
let res = match reqwest::blocking::get(url) {
Ok(result) => result,
Err(e) => return Err(Error::Fetching(format!("reqwest error: {}", e))),
};
//convert it to json
let raw = res.text().unwrap();
let tags: Self = match serde_json::from_str(&raw) {
Ok(result) => result,
Err(e) => return Err(Error::Converting(format!("invalid json: {}", e))),
};
if tags.count == 0 {
return Err(Error::NoTagsFound);
}
Ok(tags)
}
/// checks the repo name and may add a prefix for official images
pub fn check_repo(name: &str) -> Result<String, Error> {
let repo = match repo::split_tag_from_repo(name) {
Err(e) => return Err(Error::Converting(format!("{}", e))),
Ok((name, _)) => name,
};
match repo::split_repo_without_tag(name) {
Ok(repo::Repo::Project(s)) => Ok(format!("library/{}", s)),
Ok(_) => Ok(repo.to_string()),
Err(e) => Err(Error::Converting(format!("{}", e))),
}
}
/// returns tags of next page
pub fn next_page(&self) -> Result<Self, Error> {
match &self.next_page {
Some(url) => Self::with_url(url),
None => Err(Error::NoNextPage),
}
}
/// returns tags of previous page
pub fn prev_page(&self) -> Result<Self, Error> {
match &self.prev_page {
Some(url) => Self::with_url(url),
None => Err(Error::NoPrevPage),
}
}
}
impl fmt::Display for Images {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let now = chrono::Utc::now();
let rfc3339 = DateTime::parse_from_rfc3339(&self.last_updated).unwrap();
let dif = now - rfc3339.with_timezone(&chrono::Utc);
write!(f, "{} vor {}", self.tag_name, format_time_nice(dif))
}
}
/// converts a given duration to a readable string
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())
}
}
#[cfg(test)]
mod tests {
use crate::tags::{Error, Tags};
#[test]
fn test_check_repo() {
assert_eq!(Tags::check_repo("nginx").unwrap(), "library/nginx");
assert_eq!(Tags::check_repo("library/nginx").unwrap(), "library/nginx");
assert_eq!(
Tags::check_repo("rocketchat/rocket.chat").unwrap(),
"rocketchat/rocket.chat"
);
}
}

View File

@ -1,24 +1,23 @@
use std::sync::mpsc;
use std::{io, thread}; use std::{io, thread};
use crate::Opt;
use termion::event::Key; use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode; use termion::raw::IntoRawMode;
use tui::backend::TermionBackend; use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout}; use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal; use tui::Terminal;
use crate::repository;
use crate::widget::info; use crate::widget::info;
use crate::widget::repo_entry; use crate::widget::repo_entry;
use crate::widget::service_switcher; use crate::widget::service_switcher;
use crate::widget::tag_list; use crate::widget::tag_list;
pub struct Ui { pub struct Ui<'a> {
state: State, state: State,
repo: crate::widget::repo_entry::RepoEntry, repo: crate::widget::repo_entry::RepoEntry,
tags: crate::widget::tag_list::TagList, tags: crate::widget::tag_list::TagList,
services: crate::widget::service_switcher::ServiceSwitcher, services: crate::widget::service_switcher::ServiceSwitcher<'a>,
details: crate::widget::details::Details,
info: crate::widget::info::Info, info: crate::widget::info::Info,
} }
@ -29,16 +28,6 @@ pub enum State {
SelectService, SelectService,
} }
impl std::fmt::Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
State::EditRepo => write!(f, "Edit repository"),
State::SelectTag => write!(f, "Select a tag"),
State::SelectService => write!(f, "Select a image"),
}
}
}
impl std::iter::Iterator for State { impl std::iter::Iterator for State {
type Item = Self; type Item = Self;
@ -52,30 +41,23 @@ impl std::iter::Iterator for State {
} }
} }
impl Ui { impl Ui<'_> {
pub fn run(opt: &Opt) { pub fn run(repo_id: &str) {
let repo_id = opt.repo.as_deref();
let mut ui = Ui { let mut ui = Ui {
state: State::SelectService, state: State::SelectService,
repo: repo_entry::RepoEntry::new(repo_id), repo: repo_entry::RepoEntry::new(repo_id),
tags: tag_list::TagList::with_status("Tags are empty"), tags: tag_list::TagList::with_status("Tags are empty"),
services: service_switcher::ServiceSwitcher::new(&opt.file).unwrap(), services: service_switcher::ServiceSwitcher::new(),
details: crate::widget::details::Details::new(),
info: info::Info::new("Select image of edit Repository"), info: info::Info::new("Select image of edit Repository"),
}; };
if opt.repo.is_none() {
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
}
//setup tui //setup tui
let stdout = io::stdout().into_raw_mode().unwrap(); let stdout = io::stdout().into_raw_mode().unwrap();
let backend = TermionBackend::new(stdout); let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
//setup input thread //setup input thread
let receiver = super::spawn_stdin_channel(); let receiver = ui.spawn_stdin_channel();
//core interaction loop //core interaction loop
'core: loop { 'core: loop {
@ -86,7 +68,7 @@ impl Ui {
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(
[ [
Constraint::Length(10), Constraint::Min(9),
Constraint::Length(3), Constraint::Length(3),
Constraint::Min(7), Constraint::Min(7),
Constraint::Length(2), Constraint::Length(2),
@ -95,16 +77,11 @@ impl Ui {
) )
.split(rect.size()); .split(rect.size());
let (list, state) = ui.services.render(ui.state == State::SelectService); let (list, state) = ui.services.render(&ui.state);
rect.render_stateful_widget(list, chunks[0], state); rect.render_stateful_widget(list, chunks[0], state);
rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[1]); rect.render_widget(ui.repo.render(&ui.state), chunks[1]);
let (list, state) = ui.tags.render(ui.state == State::SelectTag); let (list, state) = ui.tags.render(&ui.state);
let more_chunks = Layout::default() rect.render_stateful_widget(list, chunks[2], state);
.direction(Direction::Horizontal)
.constraints([Constraint::Min(15), Constraint::Length(28)].as_ref())
.split(chunks[2]);
rect.render_stateful_widget(list, more_chunks[0], state);
rect.render_widget(ui.details.render(), more_chunks[1]);
rect.render_widget(ui.info.render(), chunks[3]); rect.render_widget(ui.info.render(), chunks[3]);
}) })
.unwrap(); .unwrap();
@ -114,62 +91,67 @@ impl Ui {
Ok(Key::Ctrl('q')) => break 'core, //quit program without saving Ok(Key::Ctrl('q')) => break 'core, //quit program without saving
Ok(Key::Char('\t')) => { Ok(Key::Char('\t')) => {
ui.state.next(); ui.state.next();
ui.info.set_info(&ui.state); ()
} }
Ok(Key::Ctrl('s')) => match ui.services.save() { Ok(Key::Ctrl('s')) => match ui.services.save() {
Err(e) => { Err(e) => {
ui.info.set_info(&format!("{}", e)); ui.info.set_info(&format!("{}", e));
continue; continue;
} }
Ok(_) => ui.info.set_text("Saved compose file"), Ok(_) => ui.info.set_info("Saved compose file"),
}, },
Ok(Key::Ctrl('r')) => { Ok(Key::Ctrl('r')) => {
ui.repo.confirm(); ui.repo.confirm();
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); ui.tags = tag_list::TagList::with_repo(ui.repo.get());
} }
Ok(Key::Ctrl('n')) => match ui.tags.next_page() {
Err(e) => ui.info.set_info(&format!("{}", e)),
Ok(_) => (),
},
Ok(Key::Ctrl('p')) => match ui.tags.prev_page() {
Err(e) => ui.info.set_info(&format!("{}", e)),
Ok(_) => (),
},
Ok(Key::Char('\n')) => match ui.state { Ok(Key::Char('\n')) => match ui.state {
State::EditRepo => { State::EditRepo => {
ui.repo.confirm(); ui.repo.confirm();
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); ui.tags = tag_list::TagList::with_repo(ui.repo.get());
} }
State::SelectTag => { State::SelectTag => {
let mut repo = ui.repo.get(); let mut repo = ui.repo.get();
let tag = match ui.tags.get_selected() { let tag = match ui.tags.get_selected() {
Err(tag_list::Error::NextPageSelected) => continue,
Err(e) => { Err(e) => {
ui.info.set_info(&format!("{}", e)); ui.info.set_info(&format!("{}", e));
continue; continue;
} }
Ok(tag) => tag, Ok(tag) => tag,
}; };
repo.push(':'); repo.push_str(":");
repo.push_str(&tag); repo.push_str(&tag);
ui.services.change_current_line(repo); ui.services.change_current_line(repo);
} }
_ => (), _ => (),
}, },
Ok(Key::Char(key)) => match ui.state { Ok(Key::Char(key)) => {
State::SelectService => (), if ui.state == State::EditRepo {
State::EditRepo => { ui.info.set_info("Editing Repository");
ui.info.set_text("Editing Repository");
ui.repo.handle_input(Key::Char(key));
} }
State::SelectTag => (), ui.repo.handle_input(&ui.state, Key::Char(key));
}, ui.tags.handle_input(&ui.state, Key::Char(key));
Ok(Key::Backspace) => match ui.state {
State::SelectService => (),
State::EditRepo => {
ui.info.set_text("Editing Repository");
ui.repo.handle_input(Key::Backspace);
} }
State::SelectTag => (), Ok(Key::Backspace) => {
}, if ui.state == State::EditRepo {
Ok(Key::Up) => match ui.state { ui.info.set_info("Editing Repository");
State::SelectService if ui.services.find_previous_match() => { }
ui.repo.handle_input(&ui.state, Key::Backspace);
ui.tags.handle_input(&ui.state, Key::Backspace);
}
Ok(Key::Up) => {
if ui.state == State::SelectService && ui.services.find_previous_match() {
match ui.services.extract_repo() { match ui.services.extract_repo() {
Err(e) => ui.info.set_info(&format!("{}", e)), Err(e) => ui.info.set_info(&format!("{}", e)),
Ok(s) => { Ok(s) => {
let repo = match repository::check_repo(&s) { let repo = match crate::tags::Tags::check_repo(&s) {
Err(e) => { Err(e) => {
ui.info.set_info(&format!("{}", e)); ui.info.set_info(&format!("{}", e));
continue; continue;
@ -177,23 +159,19 @@ impl Ui {
Ok(s) => s, Ok(s) => s,
}; };
ui.repo.set(repo.to_string()); ui.repo.set(repo.to_string());
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); ui.tags = tag_list::TagList::with_repo(ui.repo.get());
} }
} }
} }
State::SelectService => (), ui.tags.handle_input(&ui.state, Key::Up);
State::EditRepo => (), ui.repo.handle_input(&ui.state, Key::Up);
State::SelectTag => {
ui.tags.handle_input(Key::Up);
ui.details = ui.tags.create_detail_widget();
} }
},
Ok(Key::Down) => match ui.state { Ok(Key::Down) => match ui.state {
State::SelectService if ui.services.find_next_match() => { State::SelectService if ui.services.find_next_match() => {
match ui.services.extract_repo() { match ui.services.extract_repo() {
Err(e) => ui.info.set_info(&format!("{}", e)), Err(e) => ui.info.set_info(&format!("{}", e)),
Ok(s) => { Ok(s) => {
let repo = match repository::check_repo(&s) { let repo = match crate::tags::Tags::check_repo(&s) {
Err(e) => { Err(e) => {
ui.info.set_info(&format!("{}", e)); ui.info.set_info(&format!("{}", e));
continue; continue;
@ -201,17 +179,19 @@ impl Ui {
Ok(s) => s, Ok(s) => s,
}; };
ui.repo.set(repo.to_string()); ui.repo.set(repo.to_string());
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); ui.tags = tag_list::TagList::with_repo(ui.repo.get());
} }
} }
} }
State::SelectService => (), _ => {
State::EditRepo => (), ui.tags.handle_input(&ui.state, Key::Down);
State::SelectTag => { ui.repo.handle_input(&ui.state, Key::Down);
ui.tags.handle_input(Key::Down);
ui.details = ui.tags.create_detail_widget();
} }
}, },
Ok(key) => {
ui.repo.handle_input(&ui.state, Key::Down);
ui.tags.handle_input(&ui.state, key);
}
_ => (), _ => (),
} }
@ -221,4 +201,18 @@ impl Ui {
terminal.clear().unwrap(); terminal.clear().unwrap();
} }
/// create a thread for catching input and send them to core loop
pub fn spawn_stdin_channel(&self) -> mpsc::Receiver<termion::event::Key> {
let (tx, rx) = mpsc::channel::<termion::event::Key>();
thread::spawn(move || loop {
let stdin = io::stdin();
for c in stdin.keys() {
tx.send(c.unwrap()).unwrap();
}
});
thread::sleep(std::time::Duration::from_millis(64));
rx
}
} }

View File

@ -1,32 +0,0 @@
mod default;
mod no_yaml;
use std::sync::mpsc;
use std::{io, thread};
use crate::Opt;
use termion::input::TermRead;
use crate::widget::service_switcher;
pub fn create_ui(opt: &Opt) {
let service_result = service_switcher::ServiceSwitcher::new(&opt.file);
match service_result {
None => no_yaml::NoYaml::run(opt),
Some(_) => default::Ui::run(opt),
}
}
/// create a thread for catching input and send them to core loop
pub fn spawn_stdin_channel() -> mpsc::Receiver<termion::event::Key> {
let (tx, rx) = mpsc::channel::<termion::event::Key>();
thread::spawn(move || loop {
let stdin = io::stdin();
for c in stdin.keys() {
tx.send(c.unwrap()).unwrap();
}
});
thread::sleep(std::time::Duration::from_millis(64));
rx
}

View File

@ -1,161 +0,0 @@
use std::{io, thread};
use termion::event::Key;
use termion::raw::IntoRawMode;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal;
use crate::widget::details;
use crate::widget::info;
use crate::widget::repo_entry;
use crate::widget::tag_list;
use crate::Opt;
#[derive(PartialEq, Clone)]
pub enum State {
EditRepo,
SelectTag,
}
impl std::fmt::Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
State::EditRepo => write!(f, "Edit repository"),
State::SelectTag => write!(f, "Select a tag"),
}
}
}
impl std::iter::Iterator for State {
type Item = Self;
fn next(&mut self) -> Option<Self::Item> {
match self {
State::EditRepo => *self = State::SelectTag,
State::SelectTag => *self = State::EditRepo,
}
Some(self.clone())
}
}
pub struct NoYaml {
state: State,
repo: repo_entry::RepoEntry,
tags: tag_list::TagList,
details: details::Details,
info: info::Info,
}
impl NoYaml {
pub fn run(opt: &Opt) {
let repo_id = opt.repo.as_deref();
let mut ui = NoYaml {
state: State::EditRepo,
repo: repo_entry::RepoEntry::new(repo_id),
tags: tag_list::TagList::with_status("Tags are empty"),
details: details::Details::new(),
info: info::Info::new("could not find a docker-compose file"),
};
// load tags if a repository was given thorugh paramter
if opt.repo.is_none() {
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
}
//setup tui
let stdout = io::stdout().into_raw_mode().unwrap();
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
//setup input thread
let receiver = super::spawn_stdin_channel();
//core interaction loop
'core: loop {
//draw
terminal
.draw(|rect| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3),
Constraint::Min(7),
Constraint::Length(2),
]
.as_ref(),
)
.split(rect.size());
rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[0]);
let (list, state) = ui.tags.render(ui.state == State::SelectTag);
let more_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(15), Constraint::Length(28)].as_ref())
.split(chunks[1]);
rect.render_stateful_widget(list, more_chunks[0], state);
rect.render_widget(ui.details.render(), more_chunks[1]);
rect.render_widget(ui.info.render(), chunks[2]);
})
.unwrap();
//handle input
match receiver.try_recv() {
Ok(Key::Ctrl('q')) => break 'core,
Ok(Key::Char('\t')) => {
ui.state.next();
ui.info.set_info(&ui.state);
}
Ok(Key::Ctrl('r')) => {
ui.repo.confirm();
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
}
Ok(Key::Char('\n')) => match ui.state {
State::EditRepo => {
ui.repo.confirm();
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
}
State::SelectTag => ui.tags.handle_input(Key::Char('\n')),
},
Ok(Key::Char(key)) => match ui.state {
State::EditRepo => {
ui.info.set_text("Editing Repository");
ui.repo.handle_input(Key::Char(key));
}
State::SelectTag => {
ui.tags.handle_input(Key::Char(key));
}
},
Ok(Key::Backspace) => match ui.state {
State::EditRepo => {
ui.info.set_text("Editing Repository");
ui.repo.handle_input(Key::Backspace);
}
State::SelectTag => (),
},
Ok(Key::Up) => match ui.state {
State::EditRepo => (),
State::SelectTag => {
ui.tags.handle_input(Key::Up);
ui.details = ui.tags.create_detail_widget();
}
},
Ok(Key::Down) => match ui.state {
State::EditRepo => (),
State::SelectTag => {
ui.tags.handle_input(Key::Down);
ui.details = ui.tags.create_detail_widget();
}
},
_ => (),
}
//sleep for 32ms (30 fps)
thread::sleep(std::time::Duration::from_millis(32));
}
terminal.clear().unwrap();
}
}

View File

@ -1,61 +0,0 @@
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List};
use crate::repository;
pub struct Details {
details: Vec<repository::TagDetails>,
}
impl Details {
pub fn new() -> Self {
Self { details: vec![] }
}
pub fn with_list(details: &[crate::repository::TagDetails]) -> Self {
let mut detail = Self {
details: details.to_owned(),
};
detail.details.sort_by(|a, b| a.arch.cmp(&b.arch));
detail.details.dedup();
detail
}
pub fn get_details(&self) -> Vec<String> {
let mut lines = vec![format!("{:^10}|{:^6}|{:^6}", "ARCH", "OS", "SIZE")];
for d in &self.details {
lines.push(format!(
"{:^10}|{:^6}|{:^6}MB",
format!(
"{}{}",
d.arch.clone().unwrap_or_default(),
d.variant.clone().unwrap_or_default()
),
d.os.clone().unwrap_or_default(),
d.size.unwrap_or_default() / 1024 / 1024,
));
}
lines
}
pub fn render(&self) -> List {
let items: Vec<tui::widgets::ListItem> = self
.get_details()
.iter()
.map(|l| {
tui::widgets::ListItem::new(l.to_string())
.style(Style::default().fg(Color::White).bg(Color::Black))
})
.collect();
List::new(items)
.block(
Block::default()
.title("Details")
.borders(Borders::ALL)
.border_style(Style::default()),
)
.style(Style::default().fg(Color::White).bg(Color::Black))
}
}

View File

@ -11,7 +11,7 @@ impl Info {
Self { Self {
info: String::from(info), info: String::from(info),
keys: String::from( keys: String::from(
"Tab Cycle widgets C-s Save C-r Reload C-q Quit ↑ ↓ Select tags or image line Return Select current selection", "Tab Cycle widgets C-s Save C-r Reload C-q Quit C-n Next page C-p Previous page ↑ ↓ Select tags or image line",
), ),
} }
} }
@ -27,13 +27,7 @@ impl Info {
.highlight_style(Style::default().bg(Color::Black)) .highlight_style(Style::default().bg(Color::Black))
} }
/// set a text to display pub fn set_info(&mut self, info: &str) {
pub fn set_text(&mut self, info: &str) {
self.info = String::from(info); self.info = String::from(info);
} }
/// print a text to display
pub fn set_info(&mut self, text: &dyn std::fmt::Display) {
self.info = format!("{}", text);
}
} }

View File

@ -1,4 +1,3 @@
pub mod details;
pub mod info; pub mod info;
pub mod repo_entry; pub mod repo_entry;
pub mod service_switcher; pub mod service_switcher;

View File

@ -3,21 +3,20 @@ use tui::layout::Alignment;
use tui::style::{Color, Style}; use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Paragraph}; use tui::widgets::{Block, Borders, Paragraph};
use crate::ui::State;
pub struct RepoEntry { pub struct RepoEntry {
text: String, text: String,
old_text: String, old_text: String,
changed: bool, changed: bool,
default_text: bool,
} }
impl RepoEntry { impl RepoEntry {
pub fn new(text: Option<&str>) -> Self { pub fn new(text: &str) -> Self {
let default_text = "enter a repository here or select one from file widget";
Self { Self {
text: String::from(text.unwrap_or(default_text)), text: String::from(text),
old_text: String::from(text.unwrap_or(default_text)), old_text: String::from(text),
changed: false, changed: false,
default_text: text.is_none(),
} }
} }
@ -30,13 +29,13 @@ impl RepoEntry {
self.old_text = entry; self.old_text = entry;
} }
pub fn render(&self, colored: bool) -> Paragraph { pub fn render(&self, state: &crate::ui::State) -> Paragraph {
let title = match self.changed { let title = match self.changed {
true => "Repository*", true => "Repository*",
false => "Repository", false => "Repository",
}; };
let border_style = if colored { let border_style = if state == &crate::ui::State::EditRepo {
Style::default().fg(Color::Green) Style::default().fg(Color::Green)
} else { } else {
Style::default().fg(Color::Gray) Style::default().fg(Color::Gray)
@ -53,20 +52,19 @@ impl RepoEntry {
.alignment(Alignment::Left) .alignment(Alignment::Left)
} }
pub fn handle_input(&mut self, key: termion::event::Key) { pub fn handle_input(&mut self, state: &State, key: termion::event::Key) {
if state != &State::EditRepo {
return;
}
match key { match key {
// Key::Char('\n') => self.confirm(), //handled in Ui // Key::Char('\n') => self.confirm(), //handled in Ui
Key::Char(c) => { Key::Char(c) => {
self.text.push(c); self.text.push(c);
self.changed = true; self.changed = true;
self.default_text = false;
} }
Key::Backspace => { Key::Backspace => {
if self.default_text {
self.text = String::new();
} else {
self.text.pop(); self.text.pop();
}
self.changed = true; self.changed = true;
} }
Key::Esc => { Key::Esc => {

View File

@ -3,12 +3,12 @@ use std::fs::File;
use std::io::BufRead; use std::io::BufRead;
use std::io::BufReader; use std::io::BufReader;
use std::io::Write; use std::io::Write;
use std::path::PathBuf;
use tui::style::{Color, Style}; use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List, ListState}; use tui::widgets::{Block, Borders, List, ListState};
use crate::repo; use crate::repo;
use crate::ui::State;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
@ -25,28 +25,19 @@ impl fmt::Display for Error {
} }
} }
pub struct ServiceSwitcher { pub struct ServiceSwitcher<'a> {
list: Vec<String>, list: Vec<String>,
state: ListState, state: ListState,
changed: bool, changed: bool,
opened_file: PathBuf, opened_file: &'a str,
} }
impl ServiceSwitcher { impl ServiceSwitcher<'_> {
pub fn new(file: &Option<PathBuf>) -> Option<Self> { pub fn new() -> Self {
//gather possible filenames let file_list = vec!["docker-compose.yml", "docker-compose.yaml"];
let mut file_list = vec![
PathBuf::from("docker-compose.yml"),
PathBuf::from("docker-compose.yaml"),
];
match &file {
None => (),
Some(file) => file_list.insert(0, file.clone()),
}
//try filenames
for file in file_list { for file in file_list {
let list = match File::open(&file) { let list = match File::open(file) {
Err(_) => continue, Err(_) => continue,
Ok(file) => { Ok(file) => {
let buf = BufReader::new(file); let buf = BufReader::new(file);
@ -56,28 +47,33 @@ impl ServiceSwitcher {
} }
}; };
return Some(Self { return Self {
list, list,
state: ListState::default(), state: ListState::default(),
changed: false, changed: false,
opened_file: file, opened_file: file,
}); };
} }
//could not find docker-compose file //could not find docker-compose file
None Self {
list: vec![format!("No docker-compose file found")],
state: ListState::default(),
changed: false,
opened_file: "No file",
}
} }
pub fn render(&mut self, colored: bool) -> (List, &mut ListState) { pub fn render(&mut self, state: &State) -> (List, &mut ListState) {
let border_style = if colored { let border_style = if state == &State::SelectService {
Style::default().fg(Color::Green) Style::default().fg(Color::Green)
} else { } else {
Style::default().fg(Color::Gray) Style::default().fg(Color::Gray)
}; };
let title = match &self.changed { let title = match &self.changed {
true => format!("File: *{}*", &self.opened_file.display()), true => format!("File: *{}*", self.opened_file),
false => format!("File: {}", &self.opened_file.display()), false => format!("File: {}", self.opened_file),
}; };
let items: Vec<tui::widgets::ListItem> = self let items: Vec<tui::widgets::ListItem> = self
@ -106,7 +102,10 @@ impl ServiceSwitcher {
/// finds the next image tag in given file /// finds the next image tag in given file
pub fn find_next_match(&mut self) -> bool { pub fn find_next_match(&mut self) -> bool {
let current_line: usize = self.state.selected().unwrap_or(0); let current_line: usize = match self.state.selected() {
None => 0,
Some(i) => i,
};
let mut i = (current_line + 1) % self.list.len(); let mut i = (current_line + 1) % self.list.len();
loop { loop {
@ -116,7 +115,7 @@ impl ServiceSwitcher {
} }
//check if line matches //check if line matches
if repo::match_yaml_image(&self.list[i]).is_ok() { if repo::match_yaml_image(&self.list[i]).is_some() {
self.state.select(Some(i)); self.state.select(Some(i));
return true; return true;
} }
@ -130,7 +129,10 @@ impl ServiceSwitcher {
/// finds the previous image tag in given file /// finds the previous image tag in given file
pub fn find_previous_match(&mut self) -> bool { pub fn find_previous_match(&mut self) -> bool {
let current_line: usize = self.state.selected().unwrap_or(0); let current_line: usize = match self.state.selected() {
None => 0,
Some(i) => i,
};
let mut i: usize = if current_line == 0 { let mut i: usize = if current_line == 0 {
self.list.len() - 1 self.list.len() - 1
@ -145,7 +147,7 @@ impl ServiceSwitcher {
} }
//check if line matches //check if line matches
if repo::match_yaml_image(&self.list[i]).is_ok() { if repo::match_yaml_image(&self.list[i]).is_some() {
self.state.select(Some(i)); self.state.select(Some(i));
return true; return true;
} }
@ -161,10 +163,10 @@ impl ServiceSwitcher {
/// return the repository from currently selected row /// return the repository from currently selected row
pub fn extract_repo(&self) -> Result<String, Error> { pub fn extract_repo(&self) -> Result<String, Error> {
match self.state.selected() { match self.state.selected() {
None => Err(Error::NoneSelected), None => return Err(Error::NoneSelected),
Some(i) => match repo::match_yaml_image(&self.list[i]) { Some(i) => match repo::match_yaml_image(&self.list[i]) {
Err(_) => Err(Error::Parsing(String::from("Nothing found"))), None => return Err(Error::Parsing(String::from("Nothing found"))),
Ok((_, repo)) => Ok(repo.to_string()), Some((_, repo)) => return Ok(repo.to_string()),
}, },
} }
} }
@ -174,8 +176,8 @@ impl ServiceSwitcher {
match self.state.selected() { match self.state.selected() {
None => (), None => (),
Some(i) => match repo::match_yaml_image(&self.list[i]) { Some(i) => match repo::match_yaml_image(&self.list[i]) {
Err(_) => return, None => return,
Ok((front, _)) => self.list[i] = format!("{}{}", front, repo_with_tag), Some((front, _)) => self.list[i] = format!("{}{}", front, repo_with_tag),
}, },
} }
self.changed = true; self.changed = true;
@ -183,7 +185,7 @@ impl ServiceSwitcher {
/// save the currently opened file /// save the currently opened file
pub fn save(&mut self) -> Result<(), std::io::Error> { pub fn save(&mut self) -> Result<(), std::io::Error> {
let mut file = File::create(&self.opened_file)?; let mut file = File::create(self.opened_file)?;
for line in &self.list { for line in &self.list {
file.write_all(line.as_bytes())?; file.write_all(line.as_bytes())?;
file.write_all("\n".as_bytes())?; file.write_all("\n".as_bytes())?;

View File

@ -4,99 +4,127 @@ use termion::event::Key;
use tui::style::{Color, Style}; use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List, ListState}; use tui::widgets::{Block, Borders, List, ListState};
use crate::repository; use crate::tags;
use crate::ui::State;
#[derive(Debug)]
pub enum Error { pub enum Error {
NoneSelected, NoneSelected,
NextPageSelected, NoTags,
SelectedStatus, NoNextPage,
NoPrevPage,
} }
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Error::NoTags => write!(f, "There are no tags"),
Error::NoneSelected => write!(f, "No tag selected"), Error::NoneSelected => write!(f, "No tag selected"),
Error::NextPageSelected => write!(f, "tried to get the next page"), Error::NoNextPage => write!(f, "No next page available"),
Error::SelectedStatus => write!(f, "Status message was selected"), Error::NoPrevPage => write!(f, "No previous page available"),
} }
} }
} }
enum Line { /// used for creating a TagList
pub enum Type {
Status(String), Status(String),
Image(repository::Tag), Repo(tags::Tags),
NextPage(String),
}
impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Line::Status(s) => write!(f, "{}", s),
Line::Image(i) => write!(f, "{}", i.get_name_with_details()),
Line::NextPage(s) => write!(f, "{}", s),
}
}
} }
pub struct TagList { pub struct TagList {
lines: Vec<Line>, typ: Type,
state: ListState, state: ListState,
tags: Option<repository::Repo>,
} }
impl TagList { impl TagList {
/// shows a text in the list and no tags fn new(typ: Type) -> Self {
Self {
typ,
state: ListState::default(),
}
}
/// create a TagList with a status message
pub fn with_status(status: &str) -> Self { pub fn with_status(status: &str) -> Self {
Self { Self::new(Type::Status(String::from(status)))
lines: vec![Line::Status(String::from(status))], }
state: ListState::default(),
tags: None, /// create a TagList
pub fn with_repo(name: String) -> Self {
match tags::Tags::new(name) {
Err(e) => Self::with_status(&format!("{}", e)),
Ok(tags) => Self::new(Type::Repo(tags)),
} }
} }
/// list the tags of the repository if the input is valid /// display next page if possible
pub fn with_repo_name(repo: String) -> Self { pub fn next_page(&mut self) -> Result<(), Error> {
match repository::Repo::new(&repo) { match &self.typ {
Ok(tags) => Self::with_tags(tags), Type::Status(_) => (),
Err(_) => Self::with_status("input repo was not found"), Type::Repo(tags) => match tags.next_page() {
Err(_) => return Err(Error::NoNextPage),
Ok(tags) => self.typ = Type::Repo(tags),
},
}
Ok(())
}
/// display previous page if possible
pub fn prev_page(&mut self) -> Result<(), Error> {
match &self.typ {
Type::Status(_) => (),
Type::Repo(tags) => match tags.prev_page() {
Err(_) => return Err(Error::NoPrevPage),
Ok(tags) => self.typ = Type::Repo(tags),
},
}
Ok(())
}
/// get a list of tag names with info
fn print_lines(&self) -> Vec<String> {
match &self.typ {
Type::Status(line) => vec![line.to_string()],
Type::Repo(tags) => tags.results.iter().map(|r| format!("{}", r)).collect(),
} }
} }
/// list the tags of the input /// get the list of tag names
fn with_tags(mut tags: repository::Repo) -> Self { pub fn get_names(&self) -> Result<Vec<String>, Error> {
let mut lines: Vec<Line> = tags match &self.typ {
.get_tags() Type::Status(_) => Err(Error::NoTags),
.iter() Type::Repo(tags) => Ok(tags.results.iter().map(|r| r.tag_name.clone()).collect()),
.map(|r| Line::Image(r.clone()))
.collect();
match tags.next_page() {
None => (),
Some(new_tags) => {
lines.push(Line::NextPage(String::from("load more tags")));
tags = new_tags;
}
};
Self {
lines,
state: ListState::default(),
tags: Some(tags),
} }
} }
pub fn render(&mut self, colored: bool) -> (List, &mut ListState) { /// get the selected tag or return an error
let border_style = if colored { pub fn get_selected(&self) -> Result<String, Error> {
match &self.typ {
Type::Status(_) => Err(Error::NoTags),
Type::Repo(_) => match self.state.selected() {
None => Err(Error::NoneSelected),
Some(i) => Ok(self.get_names().unwrap()[i].clone()),
},
}
}
pub fn render(&mut self, state: &State) -> (List, &mut ListState) {
let border_style = if state == &State::SelectTag {
Style::default().fg(Color::Green) Style::default().fg(Color::Green)
} else { } else {
Style::default().fg(Color::Gray) Style::default().fg(Color::Gray)
}; };
let items: Vec<tui::widgets::ListItem> = self let lines = match &self.typ {
.lines Type::Status(line) => vec![line.clone()],
Type::Repo(_) => self.print_lines(),
};
let items: Vec<tui::widgets::ListItem> = lines
.iter() .iter()
.map(|l| { .map(|l| {
tui::widgets::ListItem::new(format!("{}", l)) tui::widgets::ListItem::new(l.clone())
.style(Style::default().fg(Color::White).bg(Color::Black)) .style(Style::default().fg(Color::White).bg(Color::Black))
}) })
.collect(); .collect();
@ -116,99 +144,36 @@ impl TagList {
(items, &mut self.state) (items, &mut self.state)
} }
pub fn create_detail_widget(&self) -> crate::widget::details::Details { pub fn handle_input(&mut self, state: &State, key: termion::event::Key) {
use crate::widget::details::Details; if state != &State::SelectTag {
return;
match self.state.selected() {
None => Details::new(),
Some(i) => match &self.lines[i] {
Line::Image(t) => Details::with_list(t.get_details()),
_ => Details::new(),
},
}
} }
pub fn handle_input(&mut self, key: termion::event::Key) {
match key { match key {
Key::Down => self.next(), Key::Down => self.next(),
Key::Up => self.previous(), Key::Up => self.previous(),
Key::Char('\n') => self.select(),
_ => (), _ => (),
} }
} }
/// loads new tags when matching line is selected
fn select(&mut self) {
if let Some(i) = self.state.selected() {
if let Line::NextPage(_) = &self.lines[i] {
self.load_next_page()
}
}
}
pub fn get_selected(&mut self) -> Result<String, Error> {
match self.state.selected() {
None => Err(Error::NoneSelected),
Some(i) => match &self.lines[i] {
Line::Status(_) => Err(Error::SelectedStatus),
Line::Image(i) => Ok(i.get_name().to_string()),
Line::NextPage(_) => {
self.load_next_page();
Err(Error::NextPageSelected)
}
},
}
}
/// load new tags from the next page
fn load_next_page(&mut self) {
match &self.tags {
Some(tags) => match tags.next_page() {
None => (),
Some(new_tags) => {
//load new tags object
self.tags = Some(new_tags);
//remove "load next page"
let next_page = self.lines.pop();
//add tags
match &self.tags {
None => (),
Some(tags) => {
for image in tags.get_tags().iter() {
self.lines.push(Line::Image(image.clone()));
}
}
}
//readd next page
match self.tags.as_ref().unwrap().next_page() {
None => (),
Some(_) => self.lines.push(next_page.unwrap()),
}
}
},
None => (),
}
}
/// select next tag /// select next tag
fn next(&mut self) { pub fn next(&mut self) {
match self.state.selected() { match self.state.selected() {
None if !self.lines.is_empty() => self.state.select(Some(0)), None if self.print_lines().len() > 0 => self.state.select(Some(0)),
None => (), None => (),
Some(i) if i == self.lines.len() - 1 => self.state.select(Some(0)), Some(i) if i == self.print_lines().len() - 1 => self.state.select(Some(0)),
Some(i) => self.state.select(Some(i + 1)), Some(i) => self.state.select(Some(i + 1)),
} }
} }
/// select previous tag /// select previous tag
fn previous(&mut self) { pub fn previous(&mut self) {
match self.state.selected() { match self.state.selected() {
None if !self.lines.is_empty() => self.state.select(Some(self.lines.len())), None if self.print_lines().len() > 0 => {
self.state.select(Some(self.print_lines().len()))
}
None => (), None => (),
Some(i) if i == 0 => self.state.select(Some(self.lines.len() - 1)), Some(i) if i == 0 => self.state.select(Some(self.print_lines().len() - 1)),
Some(i) => self.state.select(Some(i - 1)), Some(i) => self.state.select(Some(i - 1)),
} }
} }