Compare commits
65 Commits
e66b75c021
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
8f0eb3db4f | ||
|
13350f872a | ||
|
c8915f828f | ||
|
88070489f3 | ||
|
d1ef5c6755 | ||
|
e98c5e7a12 | ||
|
6c83683a4a | ||
|
3bf2359392 | ||
|
bb5ea3a993 | ||
|
7d9cc21b6e | ||
|
e3865da563 | ||
|
881423e461 | ||
|
b6dbcd8eba | ||
|
04842af653 | ||
|
bd60e57fea | ||
|
e1d9cbe8c9 | ||
|
987606d264 | ||
|
b59c5f4ead | ||
|
e3c6e01036 | ||
|
a725161638 | ||
|
62f784fdb9 | ||
|
7720ed3102 | ||
|
6d40b03fbf | ||
|
246124d4d1 | ||
|
eec1836dd3 | ||
|
79577de0f9 | ||
|
72fd5ec46f | ||
|
a41197562b | ||
|
0ace41f545 | ||
|
cefe57b980 | ||
|
fe3f0579ad | ||
|
cb1a1c24b7 | ||
|
bc09317d4b | ||
|
c0d376c79f | ||
|
cdb6babd48 | ||
|
8a35904c79 | ||
|
c55ccf005b | ||
|
b6a234a833 | ||
|
2a0dee78a7 | ||
|
b67242d0ea | ||
|
7b8e613058 | ||
|
74006af796 | ||
|
d608fe6b50 | ||
|
3cfbc2a656 | ||
|
45f2bb64b0 | ||
|
a718f6f8fb | ||
|
c9a8c637c4 | ||
|
a82119f827 | ||
|
c3c653f7a6 | ||
|
47752720b4 | ||
|
a7f509b165 | ||
|
05a9669fec | ||
|
8f03f7c14b | ||
|
14658e9253 | ||
|
c8b2bf991c | ||
|
4bda6e47be | ||
|
70ff0905e2 | ||
|
d6a40ef5ab | ||
|
187a9cb749 | ||
|
cb78c58de1 | ||
|
0edfd45d38 | ||
|
0a844c42ee | ||
|
a068e3f192 | ||
|
7828d7c703 | ||
|
67486f0042 |
6
.gitignore
vendored
6
.gitignore
vendored
@ -1 +1,7 @@
|
||||
# build files
|
||||
/target
|
||||
|
||||
# test files
|
||||
docker-compose.yml
|
||||
docker-compose.yaml
|
||||
docker-compose.yml.yml
|
||||
|
37
.woodpecker.yml
Normal file
37
.woodpecker.yml
Normal file
@ -0,0 +1,37 @@
|
||||
# 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
148
Cargo.lock
generated
@ -11,6 +11,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
@ -72,6 +92,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.1"
|
||||
@ -213,6 +248,15 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
@ -527,6 +571,30 @@ version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.28"
|
||||
@ -536,20 +604,6 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "query-docker-tags"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"termion",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
@ -617,6 +671,21 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reel-moby"
|
||||
version = "1.2.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"structopt",
|
||||
"termion",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
@ -776,6 +845,36 @@ dependencies = [
|
||||
"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]]
|
||||
name = "syn"
|
||||
version = "1.0.74"
|
||||
@ -813,6 +912,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
@ -975,6 +1083,18 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
|
@ -1,8 +1,8 @@
|
||||
|
||||
[package]
|
||||
name = "query-docker-tags"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
name = "reel-moby"
|
||||
version = "1.2.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -15,6 +15,7 @@ tui = "0.16"
|
||||
termion = "1.5"
|
||||
regex = "1.5.4"
|
||||
lazy_static = "1.4.0"
|
||||
structopt = "0.3.23"
|
||||
|
||||
[profile.release]
|
||||
lto = "yes"
|
||||
|
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
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.
|
10
README.md
Normal file
10
README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# 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)
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
@ -1,5 +0,0 @@
|
||||
pub fn remove_last_char(input: &str) -> &str {
|
||||
let mut chars = input.chars();
|
||||
chars.next_back();
|
||||
chars.as_str()
|
||||
}
|
28
src/common/display_duration_ext.rs
Normal file
28
src/common/display_duration_ext.rs
Normal file
@ -0,0 +1,28 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
1
src/common/mod.rs
Normal file
1
src/common/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod display_duration_ext;
|
23
src/main.rs
23
src/main.rs
@ -1,9 +1,26 @@
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod common;
|
||||
mod repo;
|
||||
mod tags;
|
||||
mod repository;
|
||||
mod ui;
|
||||
mod widget;
|
||||
|
||||
fn main() {
|
||||
ui::Ui::run("enter a repository or select one from docker-compose.yml");
|
||||
/// 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() {
|
||||
//parse parameter
|
||||
let opt = Opt::from_args();
|
||||
ui::create_ui(&opt);
|
||||
}
|
||||
|
188
src/repo.rs
188
src/repo.rs
@ -2,24 +2,16 @@ use std::fmt;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
// use crate::common;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
// Conversion,
|
||||
// Empty,
|
||||
NoTagFound,
|
||||
// InvalidChar,
|
||||
MisformedInput,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
// Error::Conversion => write!(f, "Conversion error"),
|
||||
// Error::Empty => write!(f, "Input is empty"),
|
||||
Error::NoTagFound => write!(f, "Expected a tag"),
|
||||
// Error::InvalidChar => write!(f, "Invalid character found"),
|
||||
Error::MisformedInput => write!(f, "Unexpected input"),
|
||||
}
|
||||
}
|
||||
@ -33,18 +25,22 @@ pub enum Repo {
|
||||
}
|
||||
|
||||
/// check if yaml line matches and returns the split of repo string and rest
|
||||
pub fn match_yaml_image(input: &str) -> Option<(&str, &str)> {
|
||||
/// the first &str is the image tag
|
||||
/// 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! {
|
||||
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) {
|
||||
Some(caps) => caps,
|
||||
None => return None,
|
||||
None => return Err(Error::NoTagFound),
|
||||
};
|
||||
|
||||
Some((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()))
|
||||
Ok((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> {
|
||||
lazy_static::lazy_static! {
|
||||
static ref REGEX: Regex = Regex::new(r"^([a-z0-9\./[^:]]*):?([a-z0-9._\-]*)").unwrap();
|
||||
@ -67,17 +63,10 @@ pub fn split_tag_from_repo(input: &str) -> Result<(&str, &str), Error> {
|
||||
Ok((front, back))
|
||||
}
|
||||
|
||||
// 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()))
|
||||
// }
|
||||
|
||||
/// takes an identifier and changes it to a Repo enum
|
||||
pub fn split_repo_without_tag(repo: &str) -> Result<Repo, Error> {
|
||||
let repo = repo.trim();
|
||||
let split_repo: Vec<&str> = repo.split("/").collect();
|
||||
let split_repo: Vec<&str> = repo.split('/').collect();
|
||||
match split_repo.len() {
|
||||
1 => {
|
||||
let regex = regex::Regex::new(r"[a-z0-9]+").unwrap();
|
||||
@ -108,118 +97,85 @@ 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)]
|
||||
mod tests {
|
||||
use crate::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]
|
||||
fn test_split_repo_without_tag() {
|
||||
use crate::repo::split_repo_without_tag as test_fn;
|
||||
assert_eq!(test_fn(""), Err(Error::MisformedInput));
|
||||
assert_eq!(test_fn("NGINX"), Err(Error::MisformedInput));
|
||||
assert_eq!(test_fn("nginx"), Ok(Repo::Project("nginx".into())));
|
||||
assert_eq!(
|
||||
test_fn("library/nginx"),
|
||||
Ok(Repo::WithOrga("library".into(), "nginx".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
test_fn("ghcr.io/library/nginx"),
|
||||
let input: Vec<(&str, Result<Repo, Error>)> = vec![
|
||||
("", Err(Error::MisformedInput)),
|
||||
("NGINX", Err(Error::MisformedInput)),
|
||||
("nginx", Ok(Repo::Project("nginx".into()))),
|
||||
(
|
||||
"library/nginx",
|
||||
Ok(Repo::WithOrga("library".into(), "nginx".into())),
|
||||
),
|
||||
(
|
||||
"ghcr.io/library/nginx",
|
||||
Ok(Repo::WithServer(
|
||||
"ghcr.io".into(),
|
||||
"library".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]
|
||||
fn test_match_yaml_image() {
|
||||
use crate::repo::match_yaml_image as test_fn;
|
||||
assert_eq!(test_fn(""), None);
|
||||
assert_eq!(test_fn("version: '2'"), None);
|
||||
assert_eq!(test_fn("image: "), None);
|
||||
assert_eq!(test_fn(" image: "), None);
|
||||
assert_eq!(test_fn(" image: nginx"), Some((" image: ", "nginx")));
|
||||
assert_eq!(
|
||||
test_fn(" image: library/nginx"),
|
||||
Some((" image: ", "library/nginx"))
|
||||
);
|
||||
assert_eq!(
|
||||
test_fn(" image: ghcr.io/library/nginx"),
|
||||
Some((" image: ", "ghcr.io/library/nginx"))
|
||||
);
|
||||
assert_eq!(test_fn("# image: nginx"), None);
|
||||
assert_eq!(
|
||||
test_fn(" image: nginx #comment"),
|
||||
Some((" image: ", "nginx"))
|
||||
);
|
||||
let input: Vec<(&str, Result<(&str, &str), Error>)> = vec![
|
||||
("", Err(Error::NoTagFound)),
|
||||
("version: '2'", Err(Error::NoTagFound)),
|
||||
("image: ", Err(Error::NoTagFound)),
|
||||
(" image: ", Err(Error::NoTagFound)),
|
||||
(" image: nginx", Ok((" image: ", "nginx"))),
|
||||
(" image: library/nginx", Ok((" image: ", "library/nginx"))),
|
||||
(
|
||||
" image: gchr.io/library/nginx",
|
||||
Ok((" image: ", "gchr.io/library/nginx")),
|
||||
),
|
||||
(" image: nginx # comment", Ok((" image: ", "nginx"))),
|
||||
(" image: test-hyphen", Ok((" image: ", "test-hyphen"))),
|
||||
(" image: test.dot", Ok((" image: ", "test.dot"))),
|
||||
];
|
||||
|
||||
for i in input {
|
||||
assert_eq!(super::match_yaml_image(i.0), i.1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_tag_from_repo() {
|
||||
use crate::repo::split_tag_from_repo as test_fn;
|
||||
assert_eq!(test_fn("nginx"), Ok(("nginx", "")));
|
||||
assert_eq!(test_fn("library/nginx"), Ok(("library/nginx", "")));
|
||||
assert_eq!(
|
||||
test_fn("ghcr.io/library/nginx"),
|
||||
Ok(("ghcr.io/library/nginx", ""))
|
||||
);
|
||||
assert_eq!(test_fn("nginx:"), Ok(("nginx", "")));
|
||||
assert_eq!(test_fn("nginx:1"), Ok(("nginx", "1")));
|
||||
assert_eq!(test_fn("nginx:latest"), Ok(("nginx", "latest")));
|
||||
let input: Vec<(&str, Result<(&str, &str), super::Error>)> = vec![
|
||||
("nginx", Ok(("nginx", ""))),
|
||||
("library/nginx", Ok(("library/nginx", ""))),
|
||||
("ghcr.io/library/nginx", Ok(("ghcr.io/library/nginx", ""))),
|
||||
("nginx:", Ok(("nginx", ""))),
|
||||
("nginx:1", Ok(("nginx", "1"))),
|
||||
("nginx:latest", Ok(("nginx", "latest"))),
|
||||
("hy-phen:latest", Ok(("hy-phen", "latest"))),
|
||||
("test.dot:latest", Ok(("test.dot", "latest"))),
|
||||
(
|
||||
"woodpeckerci/woodpecker-server",
|
||||
Ok(("woodpeckerci/woodpecker-server", "")),
|
||||
),
|
||||
];
|
||||
|
||||
for i in input {
|
||||
assert_eq!(super::split_tag_from_repo(i.0), i.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
76
src/repository/dockerhub.rs
Normal file
76
src/repository/dockerhub.rs
Normal file
@ -0,0 +1,76 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
137
src/repository/mod.rs
Normal file
137
src/repository/mod.rs
Normal file
@ -0,0 +1,137 @@
|
||||
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
162
src/tags.rs
@ -1,162 +0,0 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
@ -1,23 +1,24 @@
|
||||
use std::sync::mpsc;
|
||||
use std::{io, thread};
|
||||
|
||||
use crate::Opt;
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
use termion::raw::IntoRawMode;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::repository;
|
||||
use crate::widget::info;
|
||||
use crate::widget::repo_entry;
|
||||
use crate::widget::service_switcher;
|
||||
use crate::widget::tag_list;
|
||||
|
||||
pub struct Ui<'a> {
|
||||
pub struct Ui {
|
||||
state: State,
|
||||
repo: crate::widget::repo_entry::RepoEntry,
|
||||
tags: crate::widget::tag_list::TagList,
|
||||
services: crate::widget::service_switcher::ServiceSwitcher<'a>,
|
||||
services: crate::widget::service_switcher::ServiceSwitcher,
|
||||
details: crate::widget::details::Details,
|
||||
info: crate::widget::info::Info,
|
||||
}
|
||||
|
||||
@ -28,6 +29,16 @@ pub enum State {
|
||||
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 {
|
||||
type Item = Self;
|
||||
|
||||
@ -41,23 +52,30 @@ impl std::iter::Iterator for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Ui<'_> {
|
||||
pub fn run(repo_id: &str) {
|
||||
impl Ui {
|
||||
pub fn run(opt: &Opt) {
|
||||
let repo_id = opt.repo.as_deref();
|
||||
|
||||
let mut ui = Ui {
|
||||
state: State::SelectService,
|
||||
repo: repo_entry::RepoEntry::new(repo_id),
|
||||
tags: tag_list::TagList::with_status("Tags are empty"),
|
||||
services: service_switcher::ServiceSwitcher::new(),
|
||||
services: service_switcher::ServiceSwitcher::new(&opt.file).unwrap(),
|
||||
details: crate::widget::details::Details::new(),
|
||||
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
|
||||
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 = ui.spawn_stdin_channel();
|
||||
let receiver = super::spawn_stdin_channel();
|
||||
|
||||
//core interaction loop
|
||||
'core: loop {
|
||||
@ -68,7 +86,7 @@ impl Ui<'_> {
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(9),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(7),
|
||||
Constraint::Length(2),
|
||||
@ -77,11 +95,16 @@ impl Ui<'_> {
|
||||
)
|
||||
.split(rect.size());
|
||||
|
||||
let (list, state) = ui.services.render(&ui.state);
|
||||
let (list, state) = ui.services.render(ui.state == State::SelectService);
|
||||
rect.render_stateful_widget(list, chunks[0], state);
|
||||
rect.render_widget(ui.repo.render(&ui.state), chunks[1]);
|
||||
let (list, state) = ui.tags.render(&ui.state);
|
||||
rect.render_stateful_widget(list, chunks[2], state);
|
||||
rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[1]);
|
||||
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[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]);
|
||||
})
|
||||
.unwrap();
|
||||
@ -91,67 +114,62 @@ impl Ui<'_> {
|
||||
Ok(Key::Ctrl('q')) => break 'core, //quit program without saving
|
||||
Ok(Key::Char('\t')) => {
|
||||
ui.state.next();
|
||||
()
|
||||
ui.info.set_info(&ui.state);
|
||||
}
|
||||
Ok(Key::Ctrl('s')) => match ui.services.save() {
|
||||
Err(e) => {
|
||||
ui.info.set_info(&format!("{}", e));
|
||||
continue;
|
||||
}
|
||||
Ok(_) => ui.info.set_info("Saved compose file"),
|
||||
Ok(_) => ui.info.set_text("Saved compose file"),
|
||||
},
|
||||
Ok(Key::Ctrl('r')) => {
|
||||
ui.repo.confirm();
|
||||
ui.tags = tag_list::TagList::with_repo(ui.repo.get());
|
||||
ui.tags = tag_list::TagList::with_repo_name(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 {
|
||||
State::EditRepo => {
|
||||
ui.repo.confirm();
|
||||
ui.tags = tag_list::TagList::with_repo(ui.repo.get());
|
||||
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
|
||||
}
|
||||
State::SelectTag => {
|
||||
let mut repo = ui.repo.get();
|
||||
let tag = match ui.tags.get_selected() {
|
||||
Err(tag_list::Error::NextPageSelected) => continue,
|
||||
Err(e) => {
|
||||
ui.info.set_info(&format!("{}", e));
|
||||
continue;
|
||||
}
|
||||
Ok(tag) => tag,
|
||||
};
|
||||
repo.push_str(":");
|
||||
repo.push(':');
|
||||
repo.push_str(&tag);
|
||||
ui.services.change_current_line(repo);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Ok(Key::Char(key)) => {
|
||||
if ui.state == State::EditRepo {
|
||||
ui.info.set_info("Editing Repository");
|
||||
Ok(Key::Char(key)) => match ui.state {
|
||||
State::SelectService => (),
|
||||
State::EditRepo => {
|
||||
ui.info.set_text("Editing Repository");
|
||||
ui.repo.handle_input(Key::Char(key));
|
||||
}
|
||||
ui.repo.handle_input(&ui.state, Key::Char(key));
|
||||
ui.tags.handle_input(&ui.state, Key::Char(key));
|
||||
State::SelectTag => (),
|
||||
},
|
||||
Ok(Key::Backspace) => match ui.state {
|
||||
State::SelectService => (),
|
||||
State::EditRepo => {
|
||||
ui.info.set_text("Editing Repository");
|
||||
ui.repo.handle_input(Key::Backspace);
|
||||
}
|
||||
Ok(Key::Backspace) => {
|
||||
if ui.state == State::EditRepo {
|
||||
ui.info.set_info("Editing Repository");
|
||||
}
|
||||
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() {
|
||||
State::SelectTag => (),
|
||||
},
|
||||
Ok(Key::Up) => match ui.state {
|
||||
State::SelectService if ui.services.find_previous_match() => {
|
||||
match ui.services.extract_repo() {
|
||||
Err(e) => ui.info.set_info(&format!("{}", e)),
|
||||
Ok(s) => {
|
||||
let repo = match crate::tags::Tags::check_repo(&s) {
|
||||
let repo = match repository::check_repo(&s) {
|
||||
Err(e) => {
|
||||
ui.info.set_info(&format!("{}", e));
|
||||
continue;
|
||||
@ -159,19 +177,23 @@ impl Ui<'_> {
|
||||
Ok(s) => s,
|
||||
};
|
||||
ui.repo.set(repo.to_string());
|
||||
ui.tags = tag_list::TagList::with_repo(ui.repo.get());
|
||||
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.tags.handle_input(&ui.state, Key::Up);
|
||||
ui.repo.handle_input(&ui.state, Key::Up);
|
||||
State::SelectService => (),
|
||||
State::EditRepo => (),
|
||||
State::SelectTag => {
|
||||
ui.tags.handle_input(Key::Up);
|
||||
ui.details = ui.tags.create_detail_widget();
|
||||
}
|
||||
},
|
||||
Ok(Key::Down) => match ui.state {
|
||||
State::SelectService if ui.services.find_next_match() => {
|
||||
match ui.services.extract_repo() {
|
||||
Err(e) => ui.info.set_info(&format!("{}", e)),
|
||||
Ok(s) => {
|
||||
let repo = match crate::tags::Tags::check_repo(&s) {
|
||||
let repo = match repository::check_repo(&s) {
|
||||
Err(e) => {
|
||||
ui.info.set_info(&format!("{}", e));
|
||||
continue;
|
||||
@ -179,19 +201,17 @@ impl Ui<'_> {
|
||||
Ok(s) => s,
|
||||
};
|
||||
ui.repo.set(repo.to_string());
|
||||
ui.tags = tag_list::TagList::with_repo(ui.repo.get());
|
||||
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
ui.tags.handle_input(&ui.state, Key::Down);
|
||||
ui.repo.handle_input(&ui.state, Key::Down);
|
||||
State::SelectService => (),
|
||||
State::EditRepo => (),
|
||||
State::SelectTag => {
|
||||
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);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@ -201,18 +221,4 @@ impl Ui<'_> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
32
src/ui/mod.rs
Normal file
32
src/ui/mod.rs
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
161
src/ui/no_yaml.rs
Normal file
161
src/ui/no_yaml.rs
Normal file
@ -0,0 +1,161 @@
|
||||
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();
|
||||
}
|
||||
}
|
61
src/widget/details.rs
Normal file
61
src/widget/details.rs
Normal file
@ -0,0 +1,61 @@
|
||||
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))
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ impl Info {
|
||||
Self {
|
||||
info: String::from(info),
|
||||
keys: String::from(
|
||||
"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",
|
||||
"Tab Cycle widgets C-s Save C-r Reload C-q Quit ↑ ↓ Select tags or image line Return Select current selection",
|
||||
),
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,13 @@ impl Info {
|
||||
.highlight_style(Style::default().bg(Color::Black))
|
||||
}
|
||||
|
||||
pub fn set_info(&mut self, info: &str) {
|
||||
/// set a text to display
|
||||
pub fn set_text(&mut self, info: &str) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod details;
|
||||
pub mod info;
|
||||
pub mod repo_entry;
|
||||
pub mod service_switcher;
|
||||
|
@ -3,20 +3,21 @@ use tui::layout::Alignment;
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Paragraph};
|
||||
|
||||
use crate::ui::State;
|
||||
|
||||
pub struct RepoEntry {
|
||||
text: String,
|
||||
old_text: String,
|
||||
changed: bool,
|
||||
default_text: bool,
|
||||
}
|
||||
|
||||
impl RepoEntry {
|
||||
pub fn new(text: &str) -> Self {
|
||||
pub fn new(text: Option<&str>) -> Self {
|
||||
let default_text = "enter a repository here or select one from file widget";
|
||||
Self {
|
||||
text: String::from(text),
|
||||
old_text: String::from(text),
|
||||
text: String::from(text.unwrap_or(default_text)),
|
||||
old_text: String::from(text.unwrap_or(default_text)),
|
||||
changed: false,
|
||||
default_text: text.is_none(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,13 +30,13 @@ impl RepoEntry {
|
||||
self.old_text = entry;
|
||||
}
|
||||
|
||||
pub fn render(&self, state: &crate::ui::State) -> Paragraph {
|
||||
pub fn render(&self, colored: bool) -> Paragraph {
|
||||
let title = match self.changed {
|
||||
true => "Repository*",
|
||||
false => "Repository",
|
||||
};
|
||||
|
||||
let border_style = if state == &crate::ui::State::EditRepo {
|
||||
let border_style = if colored {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
@ -52,19 +53,20 @@ impl RepoEntry {
|
||||
.alignment(Alignment::Left)
|
||||
}
|
||||
|
||||
pub fn handle_input(&mut self, state: &State, key: termion::event::Key) {
|
||||
if state != &State::EditRepo {
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn handle_input(&mut self, key: termion::event::Key) {
|
||||
match key {
|
||||
// Key::Char('\n') => self.confirm(), //handled in Ui
|
||||
Key::Char(c) => {
|
||||
self.text.push(c);
|
||||
self.changed = true;
|
||||
self.default_text = false;
|
||||
}
|
||||
Key::Backspace => {
|
||||
if self.default_text {
|
||||
self.text = String::new();
|
||||
} else {
|
||||
self.text.pop();
|
||||
}
|
||||
self.changed = true;
|
||||
}
|
||||
Key::Esc => {
|
||||
|
@ -3,12 +3,12 @@ use std::fs::File;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, List, ListState};
|
||||
|
||||
use crate::repo;
|
||||
use crate::ui::State;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
@ -25,19 +25,28 @@ impl fmt::Display for Error {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServiceSwitcher<'a> {
|
||||
pub struct ServiceSwitcher {
|
||||
list: Vec<String>,
|
||||
state: ListState,
|
||||
changed: bool,
|
||||
opened_file: &'a str,
|
||||
opened_file: PathBuf,
|
||||
}
|
||||
|
||||
impl ServiceSwitcher<'_> {
|
||||
pub fn new() -> Self {
|
||||
let file_list = vec!["docker-compose.yml", "docker-compose.yaml"];
|
||||
impl ServiceSwitcher {
|
||||
pub fn new(file: &Option<PathBuf>) -> Option<Self> {
|
||||
//gather possible filenames
|
||||
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 {
|
||||
let list = match File::open(file) {
|
||||
let list = match File::open(&file) {
|
||||
Err(_) => continue,
|
||||
Ok(file) => {
|
||||
let buf = BufReader::new(file);
|
||||
@ -47,33 +56,28 @@ impl ServiceSwitcher<'_> {
|
||||
}
|
||||
};
|
||||
|
||||
return Self {
|
||||
return Some(Self {
|
||||
list,
|
||||
state: ListState::default(),
|
||||
changed: false,
|
||||
opened_file: file,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
//could not find docker-compose file
|
||||
Self {
|
||||
list: vec![format!("No docker-compose file found")],
|
||||
state: ListState::default(),
|
||||
changed: false,
|
||||
opened_file: "No file",
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn render(&mut self, state: &State) -> (List, &mut ListState) {
|
||||
let border_style = if state == &State::SelectService {
|
||||
pub fn render(&mut self, colored: bool) -> (List, &mut ListState) {
|
||||
let border_style = if colored {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
};
|
||||
|
||||
let title = match &self.changed {
|
||||
true => format!("File: *{}*", self.opened_file),
|
||||
false => format!("File: {}", self.opened_file),
|
||||
true => format!("File: *{}*", &self.opened_file.display()),
|
||||
false => format!("File: {}", &self.opened_file.display()),
|
||||
};
|
||||
|
||||
let items: Vec<tui::widgets::ListItem> = self
|
||||
@ -102,10 +106,7 @@ impl ServiceSwitcher<'_> {
|
||||
|
||||
/// finds the next image tag in given file
|
||||
pub fn find_next_match(&mut self) -> bool {
|
||||
let current_line: usize = match self.state.selected() {
|
||||
None => 0,
|
||||
Some(i) => i,
|
||||
};
|
||||
let current_line: usize = self.state.selected().unwrap_or(0);
|
||||
|
||||
let mut i = (current_line + 1) % self.list.len();
|
||||
loop {
|
||||
@ -115,7 +116,7 @@ impl ServiceSwitcher<'_> {
|
||||
}
|
||||
|
||||
//check if line matches
|
||||
if repo::match_yaml_image(&self.list[i]).is_some() {
|
||||
if repo::match_yaml_image(&self.list[i]).is_ok() {
|
||||
self.state.select(Some(i));
|
||||
return true;
|
||||
}
|
||||
@ -129,10 +130,7 @@ impl ServiceSwitcher<'_> {
|
||||
|
||||
/// finds the previous image tag in given file
|
||||
pub fn find_previous_match(&mut self) -> bool {
|
||||
let current_line: usize = match self.state.selected() {
|
||||
None => 0,
|
||||
Some(i) => i,
|
||||
};
|
||||
let current_line: usize = self.state.selected().unwrap_or(0);
|
||||
|
||||
let mut i: usize = if current_line == 0 {
|
||||
self.list.len() - 1
|
||||
@ -147,7 +145,7 @@ impl ServiceSwitcher<'_> {
|
||||
}
|
||||
|
||||
//check if line matches
|
||||
if repo::match_yaml_image(&self.list[i]).is_some() {
|
||||
if repo::match_yaml_image(&self.list[i]).is_ok() {
|
||||
self.state.select(Some(i));
|
||||
return true;
|
||||
}
|
||||
@ -163,10 +161,10 @@ impl ServiceSwitcher<'_> {
|
||||
/// return the repository from currently selected row
|
||||
pub fn extract_repo(&self) -> Result<String, Error> {
|
||||
match self.state.selected() {
|
||||
None => return Err(Error::NoneSelected),
|
||||
None => Err(Error::NoneSelected),
|
||||
Some(i) => match repo::match_yaml_image(&self.list[i]) {
|
||||
None => return Err(Error::Parsing(String::from("Nothing found"))),
|
||||
Some((_, repo)) => return Ok(repo.to_string()),
|
||||
Err(_) => Err(Error::Parsing(String::from("Nothing found"))),
|
||||
Ok((_, repo)) => Ok(repo.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -176,8 +174,8 @@ impl ServiceSwitcher<'_> {
|
||||
match self.state.selected() {
|
||||
None => (),
|
||||
Some(i) => match repo::match_yaml_image(&self.list[i]) {
|
||||
None => return,
|
||||
Some((front, _)) => self.list[i] = format!("{}{}", front, repo_with_tag),
|
||||
Err(_) => return,
|
||||
Ok((front, _)) => self.list[i] = format!("{}{}", front, repo_with_tag),
|
||||
},
|
||||
}
|
||||
self.changed = true;
|
||||
@ -185,7 +183,7 @@ impl ServiceSwitcher<'_> {
|
||||
|
||||
/// save the currently opened file
|
||||
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 {
|
||||
file.write_all(line.as_bytes())?;
|
||||
file.write_all("\n".as_bytes())?;
|
||||
|
@ -4,127 +4,99 @@ use termion::event::Key;
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, List, ListState};
|
||||
|
||||
use crate::tags;
|
||||
use crate::ui::State;
|
||||
use crate::repository;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
NoneSelected,
|
||||
NoTags,
|
||||
NoNextPage,
|
||||
NoPrevPage,
|
||||
NextPageSelected,
|
||||
SelectedStatus,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::NoTags => write!(f, "There are no tags"),
|
||||
Error::NoneSelected => write!(f, "No tag selected"),
|
||||
Error::NoNextPage => write!(f, "No next page available"),
|
||||
Error::NoPrevPage => write!(f, "No previous page available"),
|
||||
Error::NextPageSelected => write!(f, "tried to get the next page"),
|
||||
Error::SelectedStatus => write!(f, "Status message was selected"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// used for creating a TagList
|
||||
pub enum Type {
|
||||
enum Line {
|
||||
Status(String),
|
||||
Repo(tags::Tags),
|
||||
Image(repository::Tag),
|
||||
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 {
|
||||
typ: Type,
|
||||
lines: Vec<Line>,
|
||||
state: ListState,
|
||||
tags: Option<repository::Repo>,
|
||||
}
|
||||
|
||||
impl TagList {
|
||||
fn new(typ: Type) -> Self {
|
||||
Self {
|
||||
typ,
|
||||
state: ListState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// create a TagList with a status message
|
||||
/// shows a text in the list and no tags
|
||||
pub fn with_status(status: &str) -> Self {
|
||||
Self::new(Type::Status(String::from(status)))
|
||||
}
|
||||
|
||||
/// 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)),
|
||||
Self {
|
||||
lines: vec![Line::Status(String::from(status))],
|
||||
state: ListState::default(),
|
||||
tags: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// display next page if possible
|
||||
pub fn next_page(&mut self) -> Result<(), Error> {
|
||||
match &self.typ {
|
||||
Type::Status(_) => (),
|
||||
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 repository if the input is valid
|
||||
pub fn with_repo_name(repo: String) -> Self {
|
||||
match repository::Repo::new(&repo) {
|
||||
Ok(tags) => Self::with_tags(tags),
|
||||
Err(_) => Self::with_status("input repo was not found"),
|
||||
}
|
||||
}
|
||||
|
||||
/// get the list of tag names
|
||||
pub fn get_names(&self) -> Result<Vec<String>, Error> {
|
||||
match &self.typ {
|
||||
Type::Status(_) => Err(Error::NoTags),
|
||||
Type::Repo(tags) => Ok(tags.results.iter().map(|r| r.tag_name.clone()).collect()),
|
||||
/// list the tags of the input
|
||||
fn with_tags(mut tags: repository::Repo) -> Self {
|
||||
let mut lines: Vec<Line> = tags
|
||||
.get_tags()
|
||||
.iter()
|
||||
.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),
|
||||
}
|
||||
}
|
||||
|
||||
/// get the selected tag or return an error
|
||||
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 {
|
||||
pub fn render(&mut self, colored: bool) -> (List, &mut ListState) {
|
||||
let border_style = if colored {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
};
|
||||
|
||||
let lines = match &self.typ {
|
||||
Type::Status(line) => vec![line.clone()],
|
||||
Type::Repo(_) => self.print_lines(),
|
||||
};
|
||||
|
||||
let items: Vec<tui::widgets::ListItem> = lines
|
||||
let items: Vec<tui::widgets::ListItem> = self
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| {
|
||||
tui::widgets::ListItem::new(l.clone())
|
||||
tui::widgets::ListItem::new(format!("{}", l))
|
||||
.style(Style::default().fg(Color::White).bg(Color::Black))
|
||||
})
|
||||
.collect();
|
||||
@ -144,36 +116,99 @@ impl TagList {
|
||||
(items, &mut self.state)
|
||||
}
|
||||
|
||||
pub fn handle_input(&mut self, state: &State, key: termion::event::Key) {
|
||||
if state != &State::SelectTag {
|
||||
return;
|
||||
pub fn create_detail_widget(&self) -> crate::widget::details::Details {
|
||||
use crate::widget::details::Details;
|
||||
|
||||
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 {
|
||||
Key::Down => self.next(),
|
||||
Key::Up => self.previous(),
|
||||
Key::Char('\n') => self.select(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// select next tag
|
||||
pub fn next(&mut self) {
|
||||
/// 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 if self.print_lines().len() > 0 => self.state.select(Some(0)),
|
||||
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(i) if i == self.print_lines().len() - 1 => self.state.select(Some(0)),
|
||||
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
|
||||
fn next(&mut self) {
|
||||
match self.state.selected() {
|
||||
None if !self.lines.is_empty() => self.state.select(Some(0)),
|
||||
None => (),
|
||||
Some(i) if i == self.lines.len() - 1 => self.state.select(Some(0)),
|
||||
Some(i) => self.state.select(Some(i + 1)),
|
||||
}
|
||||
}
|
||||
|
||||
/// select previous tag
|
||||
pub fn previous(&mut self) {
|
||||
fn previous(&mut self) {
|
||||
match self.state.selected() {
|
||||
None if self.print_lines().len() > 0 => {
|
||||
self.state.select(Some(self.print_lines().len()))
|
||||
}
|
||||
None if !self.lines.is_empty() => self.state.select(Some(self.lines.len())),
|
||||
None => (),
|
||||
Some(i) if i == 0 => self.state.select(Some(self.print_lines().len() - 1)),
|
||||
Some(i) if i == 0 => self.state.select(Some(self.lines.len() - 1)),
|
||||
Some(i) => self.state.select(Some(i - 1)),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user