rewrote parts to make it more async

This commit is contained in:
Thomas Eppers 2023-01-31 23:51:45 +01:00
parent c98558c41d
commit 87039541a5
12 changed files with 661 additions and 473 deletions

152
Cargo.lock generated
View File

@ -32,16 +32,16 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
@ -184,12 +184,6 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
[[package]]
name = "futures-io"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
[[package]]
name = "futures-sink"
version = "0.3.16"
@ -210,12 +204,9 @@ checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
dependencies = [
"autocfg",
"futures-core",
"futures-io",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@ -226,7 +217,7 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.10.0+wasi-snapshot-preview1",
]
[[package]]
@ -272,6 +263,15 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "http"
version = "0.2.4"
@ -393,9 +393,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.99"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "log"
@ -426,24 +426,14 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mio"
version = "0.7.13"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
"miow",
"ntapi",
"winapi",
]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
@ -464,15 +454,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.44"
@ -494,11 +475,11 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.13.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi",
"hermit-abi 0.2.6",
"libc",
]
@ -691,6 +672,7 @@ dependencies = [
"structopt",
"termion",
"thiserror",
"tokio",
"tui",
]
@ -845,9 +827,9 @@ checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
[[package]]
name = "socket2"
version = "0.4.1"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
dependencies = [
"libc",
"winapi",
@ -956,7 +938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
@ -977,9 +959,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.10.0"
version = "1.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb"
dependencies = [
"autocfg",
"bytes",
@ -988,7 +970,20 @@ dependencies = [
"mio",
"num_cpus",
"pin-project-lite",
"winapi",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -1139,6 +1134,12 @@ version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.75"
@ -1239,6 +1240,63 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winreg"
version = "0.7.0"

View File

@ -9,7 +9,7 @@ edition = "2021"
[dependencies]
serde = { version = "1.0.127", features = ["derive"] }
serde_json = "1.0.66"
reqwest = { version = "0.11.4", features = ["blocking", "json"] }
reqwest = { version = "0.11.4", features = ["json"] }
chrono = "0.4.19"
tui = "0.16"
termion = "1.5"
@ -18,6 +18,7 @@ lazy_static = "1.4.0"
structopt = "0.3.23"
thiserror = "1.0.32"
anyhow = "1.0.59"
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
[profile.release]
lto = "yes"

View File

@ -46,17 +46,17 @@ pub struct DockerHub {
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> {
pub async fn create_repo(repo: &str) -> Result<super::Repo, Error> {
let request = format!("https://hub.docker.com/v2/repositories/{}/tags", repo);
Self::with_url(&request)
Self::with_url(&request).await
}
/// fetches tag information from a url
pub fn with_url(url: &str) -> Result<super::Repo, Error> {
let response = reqwest::blocking::get(url)?;
pub async fn with_url(url: &str) -> Result<super::Repo, Error> {
let response = reqwest::get(url).await?;
//convert it to json
let tags = response.json::<Self>()?;
let tags = response.json::<Self>().await?;
if tags.results.is_empty() {
return Err(Error::NoTagsFound);
}

View File

@ -46,13 +46,14 @@ impl Tag {
}
}
#[derive(Clone)]
pub struct Repo {
tags: Vec<Tag>,
next_page: Option<String>,
}
impl Repo {
pub fn new(repo: &str) -> Result<Self, Error> {
pub async 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)),
@ -62,7 +63,7 @@ impl Repo {
};
if registry.unwrap_or_default().is_empty() {
dockerhub::DockerHub::create_repo(&repo)
dockerhub::DockerHub::create_repo(&repo).await
} else {
Err(Error::Converting(
"This registry is not supported yet".into(),
@ -70,18 +71,18 @@ impl Repo {
}
}
pub fn with_url(url: &str) -> Result<Self, Error> {
pub async fn with_url(url: &str) -> Result<Self, Error> {
//TODO fix for other registries
dockerhub::DockerHub::with_url(url)
dockerhub::DockerHub::with_url(url).await
}
pub fn get_tags(&self) -> &Vec<Tag> {
&self.tags
}
pub fn next_page(&self) -> Option<Self> {
pub async fn next_page(&self) -> Option<Self> {
if let Some(url) = &self.next_page {
match Self::with_url(url) {
match Self::with_url(url).await {
Ok(tags) => return Some(tags),
Err(e) => println!("Encountered error: {e}"),
}

View File

@ -1,225 +0,0 @@
use anyhow::Result;
use termion::event::Key;
use termion::raw::IntoRawMode;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal;
use std::{io, thread};
use crate::repository;
use crate::widget::info;
use crate::widget::repo_entry;
use crate::widget::service_switcher;
use crate::widget::tag_list;
use crate::Opt;
pub struct Ui {
state: State,
repo: crate::widget::repo_entry::RepoEntry,
tags: crate::widget::tag_list::TagList,
services: crate::widget::service_switcher::ServiceSwitcher,
details: crate::widget::details::Details,
info: crate::widget::info::Info,
}
#[derive(PartialEq, Clone)]
pub enum State {
EditRepo,
SelectTag,
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;
fn next(&mut self) -> Option<Self::Item> {
match self {
State::EditRepo => *self = State::SelectTag,
State::SelectTag => *self = State::SelectService,
State::SelectService => *self = State::EditRepo,
}
Some(self.clone())
}
}
impl Ui {
pub fn run(opt: &Opt, switcher: service_switcher::ServiceSwitcher) -> Result<()> {
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: switcher,
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()?;
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
//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(10),
Constraint::Length(3),
Constraint::Min(7),
Constraint::Length(2),
]
.as_ref(),
)
.split(rect.size());
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 == 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]);
})?;
//handle input
match receiver.try_recv() {
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_text("Saved compose file"),
},
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 => {
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(':');
repo.push_str(&tag);
ui.services.change_current_line(repo);
}
_ => (),
},
Ok(Key::Char(key)) => match ui.state {
State::SelectService => (),
State::EditRepo => {
ui.info.set_text("Editing Repository");
ui.repo.handle_input(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);
}
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 repository::check_repo(&s) {
Err(e) => {
ui.info.set_info(&format!("{}", e));
continue;
}
Ok(s) => s,
};
ui.repo.set(repo.to_string());
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
}
}
}
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 repository::check_repo(&s) {
Err(e) => {
ui.info.set_info(&format!("{}", e));
continue;
}
Ok(s) => s,
};
ui.repo.set(repo.to_string());
ui.tags = tag_list::TagList::with_repo_name(ui.repo.get());
}
}
}
State::SelectService => (),
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()?;
Ok(())
}
}

View File

@ -1,5 +1,5 @@
mod default;
mod no_yaml;
mod no_yaml_found;
mod yaml_found;
use anyhow::Result;
use termion::input::TermRead;
@ -13,8 +13,8 @@ use std::{io, thread};
pub fn create_ui(opt: &Opt) -> Result<()> {
let service_result = service_switcher::ServiceSwitcher::new(&opt.file);
match service_result {
None => no_yaml::NoYaml::run(opt),
Some(switcher) => default::Ui::run(opt, switcher),
Some(switcher) => yaml_found::Ui::run(opt, switcher),
_ => no_yaml_found::Ui::run(opt),
}?;
Ok(())

View File

@ -1,161 +0,0 @@
use anyhow::Result;
use termion::event::Key;
use termion::raw::IntoRawMode;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal;
use std::{io, thread};
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) -> Result<()> {
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()?;
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
//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]);
})?;
//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()?;
Ok(())
}
}

225
src/ui/no_yaml_found.rs Normal file
View File

@ -0,0 +1,225 @@
use anyhow::Result;
use termion::event::Key;
use termion::raw::IntoRawMode;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal;
use std::sync::{Arc, Mutex};
use std::{io, thread};
use crate::widget::async_tag_list;
use crate::widget::info;
use crate::widget::repo_entry;
use crate::Opt;
pub struct Ui {
state: State,
repo: repo_entry::RepoEntry,
tags: async_tag_list::TagList,
details: crate::widget::details::Details,
info: info::Info,
}
#[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 enum UiEvent {
NewRepo(String),
TagInput(termion::event::Key),
Quit,
}
impl Ui {
#[tokio::main]
pub async fn work_requests(ui: Arc<Mutex<Ui>>, event: std::sync::mpsc::Receiver<UiEvent>) {
loop {
match event.recv() {
Ok(UiEvent::Quit) => break,
Ok(UiEvent::NewRepo(name)) => {
let list = async_tag_list::TagList::with_repo_name(name).await;
let mut ui = ui.lock().unwrap();
ui.tags = list;
}
Ok(UiEvent::TagInput(key)) => {
let mut tags = {
let ui_data = ui.lock().unwrap();
ui_data.tags.clone()
};
tags.handle_input(key).await;
let mut ui = ui.lock().unwrap();
ui.tags = tags;
}
Err(e) => {
let mut ui = ui.lock().unwrap();
ui.info.set_info(&e);
}
};
}
}
pub fn run(opt: &Opt) -> Result<()> {
let repo_id = opt.repo.as_deref();
let ui = Arc::new(Mutex::new(Ui {
state: State::EditRepo,
repo: repo_entry::RepoEntry::new(repo_id),
tags: async_tag_list::TagList::with_status("no tags"),
details: crate::widget::details::Details::new(),
info: info::Info::new("Select image or edit Repository"),
}));
// spawn new thread that fetches information async
let (sender, receiver) = std::sync::mpsc::channel();
let ui_clone = ui.clone();
std::thread::spawn(move || {
Self::work_requests(ui_clone, receiver);
});
//setup tui
let stdout = io::stdout().into_raw_mode()?;
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
//setup input thread
let receiver = super::spawn_stdin_channel();
//core interaction loop
'core: loop {
let mut ui_data = ui.lock().unwrap();
//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_data.repo.render(ui_data.state == State::EditRepo),
chunks[0],
);
let render_state = ui_data.state == State::SelectTag;
let (tags, state) = ui_data.tags.render(render_state);
let more_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(15), Constraint::Length(28)].as_ref())
.split(chunks[1]);
rect.render_stateful_widget(tags, more_chunks[0], state);
rect.render_widget(ui_data.details.render(), more_chunks[1]);
rect.render_widget(ui_data.info.render(), chunks[2]);
})?;
//handle input
match receiver.try_recv() {
Ok(Key::Ctrl('q')) => {
sender.send(UiEvent::Quit)?;
break 'core; //quit program without saving
}
Ok(Key::Char('\t')) => {
ui_data.state.next();
let state = ui_data.state.clone();
ui_data.info.set_info(&state);
}
Ok(Key::Ctrl('r')) => {
ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap();
}
Ok(Key::Char('\n')) => match ui_data.state {
State::EditRepo => {
ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap();
}
State::SelectTag => {} // {
// let mut repo = ui_data.repo.get();
// let tag = match ui_data.tags.get_selected() {
// Err(async_tag_list::Error::NextPageSelected) => continue,
// Err(e) => {
// ui_data.info.set_info(&format!("{}", e));
// continue;
// }
// Ok(tag) => tag,
// };
// repo.push(':');
// repo.push_str(&tag);
// ui_data.services.change_current_line(repo);
// }
},
Ok(Key::Char(key)) => match ui_data.state {
State::EditRepo => {
ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Char(key));
}
State::SelectTag => {}
},
Ok(Key::Backspace) => match ui_data.state {
State::EditRepo => {
ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Backspace);
}
State::SelectTag => {}
},
Ok(Key::Up) => {
let state = ui_data.state.clone();
match state {
State::EditRepo => {}
State::SelectTag => {
sender.send(UiEvent::TagInput(Key::Up)).unwrap();
ui_data.details = ui_data.tags.create_detail_widget();
}
}
}
Ok(Key::Down) => {
let state = ui_data.state.clone();
match state {
State::EditRepo => {}
State::SelectTag => {
sender.send(UiEvent::TagInput(Key::Down)).unwrap();
ui_data.details = ui_data.tags.create_detail_widget();
}
}
}
_ => {}
}
// release lock of ui for other threads
drop(ui_data);
//sleep for 32ms (30 fps)
thread::sleep(std::time::Duration::from_millis(32));
}
terminal.clear()?;
Ok(())
}
}

285
src/ui/yaml_found.rs Normal file
View File

@ -0,0 +1,285 @@
use anyhow::Result;
use termion::event::Key;
use termion::raw::IntoRawMode;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal;
use std::sync::{Arc, Mutex};
use std::{io, thread};
use crate::repository;
use crate::widget::async_tag_list;
use crate::widget::info;
use crate::widget::repo_entry;
use crate::widget::service_switcher;
use crate::Opt;
pub struct Ui {
state: State,
repo: repo_entry::RepoEntry,
tags: async_tag_list::TagList,
services: service_switcher::ServiceSwitcher,
details: crate::widget::details::Details,
info: info::Info,
}
#[derive(PartialEq, Clone)]
pub enum State {
EditRepo,
SelectTag,
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;
fn next(&mut self) -> Option<Self::Item> {
match self {
State::EditRepo => *self = State::SelectTag,
State::SelectTag => *self = State::SelectService,
State::SelectService => *self = State::EditRepo,
}
Some(self.clone())
}
}
pub enum UiEvent {
NewRepo(String),
TagInput(termion::event::Key),
Quit,
}
impl Ui {
#[tokio::main]
pub async fn work_requests(ui: Arc<Mutex<Ui>>, event: std::sync::mpsc::Receiver<UiEvent>) {
loop {
match event.recv() {
Ok(UiEvent::Quit) => break,
Ok(UiEvent::NewRepo(name)) => {
let list = async_tag_list::TagList::with_repo_name(name).await;
let mut ui = ui.lock().unwrap();
ui.tags = list;
}
Ok(UiEvent::TagInput(key)) => {
let mut tags = {
let ui_data = ui.lock().unwrap();
ui_data.tags.clone()
};
tags.handle_input(key).await;
let mut ui = ui.lock().unwrap();
ui.tags = tags;
}
Err(e) => {
let mut ui = ui.lock().unwrap();
ui.info.set_info(&e);
}
};
}
}
pub fn run(opt: &Opt, switcher: service_switcher::ServiceSwitcher) -> Result<()> {
let repo_id = opt.repo.as_deref();
let ui = Arc::new(Mutex::new(Ui {
state: State::SelectService,
repo: repo_entry::RepoEntry::new(repo_id),
tags: async_tag_list::TagList::with_status("no tags"),
services: switcher,
details: crate::widget::details::Details::new(),
info: info::Info::new("Select image or edit Repository"),
}));
// spawn new thread that fetches information async
let (sender, receiver) = std::sync::mpsc::channel();
let ui_clone = ui.clone();
std::thread::spawn(move || {
Self::work_requests(ui_clone, receiver);
});
//setup tui
let stdout = io::stdout().into_raw_mode()?;
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
//setup input thread
let receiver = super::spawn_stdin_channel();
//core interaction loop
'core: loop {
let mut ui_data = ui.lock().unwrap();
//draw
terminal.draw(|rect| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(10),
Constraint::Min(7),
Constraint::Length(2),
]
.as_ref(),
)
.split(rect.size());
let render_state = ui_data.state == State::SelectService;
let (file, state) = ui_data.services.render(render_state);
rect.render_stateful_widget(file, chunks[0], state);
let more_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(chunks[1]);
rect.render_widget(
ui_data.repo.render(ui_data.state == State::EditRepo),
more_chunks[0],
);
let render_state = ui_data.state == State::SelectTag;
let (tags, state) = ui_data.tags.render(render_state);
rect.render_stateful_widget(tags, more_chunks[1], state);
rect.render_widget(ui_data.details.render(), more_chunks[2]);
rect.render_widget(ui_data.info.render(), chunks[2]);
})?;
//handle input
match receiver.try_recv() {
Ok(Key::Ctrl('q')) => {
sender.send(UiEvent::Quit)?;
break 'core; //quit program without saving
}
Ok(Key::Char('\t')) => {
ui_data.state.next();
let state = ui_data.state.clone();
ui_data.info.set_info(&state);
}
Ok(Key::Ctrl('s')) => match ui_data.services.save() {
Err(e) => {
ui_data.info.set_info(&format!("{}", e));
continue;
}
Ok(_) => ui_data.info.set_text("Saved compose file"),
},
Ok(Key::Ctrl('r')) => {
ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap();
}
Ok(Key::Char('\n')) => match ui_data.state {
State::EditRepo => {
ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap();
}
State::SelectTag => {
let mut repo = ui_data.repo.get();
let tag = match ui_data.tags.get_selected() {
Err(async_tag_list::Error::NextPageSelected) => continue,
Err(e) => {
ui_data.info.set_info(&format!("{}", e));
continue;
}
Ok(tag) => tag,
};
repo.push(':');
repo.push_str(&tag);
ui_data.services.change_current_line(repo);
}
_ => (),
},
Ok(Key::Char(key)) => match ui_data.state {
State::SelectService => (),
State::EditRepo => {
ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Char(key));
}
State::SelectTag => (),
},
Ok(Key::Backspace) => match ui_data.state {
State::SelectService => (),
State::EditRepo => {
ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Backspace);
}
State::SelectTag => (),
},
Ok(Key::Up) => {
let state = ui_data.state.clone();
match state {
State::SelectService if ui_data.services.find_previous_match() => {
match ui_data.services.extract_repo() {
Err(e) => ui_data.info.set_info(&format!("{}", e)),
Ok(s) => {
let repo = match repository::check_repo(&s) {
Err(e) => {
ui_data.info.set_info(&format!("{}", e));
continue;
}
Ok(s) => s,
};
ui_data.repo.set(repo.to_string());
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap();
}
}
}
State::SelectService | State::EditRepo => (),
State::SelectTag => {
sender.send(UiEvent::TagInput(Key::Up)).unwrap();
ui_data.details = ui_data.tags.create_detail_widget();
}
}
}
Ok(Key::Down) => {
let state = ui_data.state.clone();
match state {
State::SelectService if ui_data.services.find_next_match() => {
match ui_data.services.extract_repo() {
Err(e) => ui_data.info.set_info(&format!("{}", e)),
Ok(s) => {
let repo = match repository::check_repo(&s) {
Err(e) => {
ui_data.info.set_info(&format!("{}", e));
continue;
}
Ok(s) => s,
};
ui_data.repo.set(repo.to_string());
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap();
}
}
}
State::SelectService => (),
State::EditRepo => (),
State::SelectTag => {
sender.send(UiEvent::TagInput(Key::Down)).unwrap();
ui_data.details = ui_data.tags.create_detail_widget();
}
}
}
_ => (),
}
drop(ui_data);
//sleep for 32ms (30 fps)
thread::sleep(std::time::Duration::from_millis(32));
}
terminal.clear()?;
Ok(())
}
}

View File

@ -22,6 +22,7 @@ impl fmt::Display for Error {
}
}
#[derive(Clone)]
enum Line {
Status(String),
Image(repository::Tag),
@ -38,6 +39,7 @@ impl fmt::Display for Line {
}
}
#[derive(Clone)]
pub struct TagList {
lines: Vec<Line>,
state: ListState,
@ -55,22 +57,22 @@ impl TagList {
}
/// 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),
pub async fn with_repo_name(repo: String) -> Self {
match repository::Repo::new(&repo).await {
Ok(tags) => Self::with_tags(tags).await,
Err(_) => Self::with_status("input repo was not found"),
}
}
/// list the tags of the input
fn with_tags(mut tags: repository::Repo) -> Self {
async 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() {
match tags.next_page().await {
None => (),
Some(new_tags) => {
lines.push(Line::NextPage(String::from("load more tags")));
@ -128,20 +130,20 @@ impl TagList {
}
}
pub fn handle_input(&mut self, key: termion::event::Key) {
pub async fn handle_input(&mut self, key: termion::event::Key) {
match key {
Key::Down => self.next(),
Key::Down => self.next().await,
Key::Up => self.previous(),
Key::Char('\n') => self.select(),
Key::Char('\n') => self.select().await,
_ => (),
}
}
/// loads new tags when matching line is selected
fn select(&mut self) {
async fn select(&mut self) {
if let Some(i) = self.state.selected() {
if let Line::NextPage(_) = &self.lines[i] {
self.load_next_page()
self.load_next_page().await
}
}
}
@ -152,18 +154,15 @@ impl TagList {
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)
}
Line::NextPage(_) => Err(Error::NextPageSelected),
},
}
}
/// load new tags from the next page
fn load_next_page(&mut self) {
async fn load_next_page(&mut self) {
match &self.tags {
Some(tags) => match tags.next_page() {
Some(tags) => match tags.next_page().await {
None => (),
Some(new_tags) => {
//load new tags object
@ -183,7 +182,7 @@ impl TagList {
}
//readd next page
match self.tags.as_ref().unwrap().next_page() {
match self.tags.as_ref().unwrap().next_page().await {
None => (),
Some(_) => self.lines.push(next_page.unwrap()),
}
@ -194,21 +193,26 @@ impl TagList {
}
/// select next tag
fn next(&mut self) {
async fn next(&mut self) {
if let Some(Line::Status(_)) = self.lines.get(0) {
return;
}
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) if i == self.lines.len() - 2 => self.load_next_page().await,
Some(i) => self.state.select(Some(i + 1)),
}
}
/// select previous tag
fn previous(&mut self) {
if let Some(Line::Status(_)) = self.lines.get(0) {
return;
}
match self.state.selected() {
None if !self.lines.is_empty() => self.state.select(Some(self.lines.len())),
None => (),
Some(i) if i == 0 => self.state.select(Some(self.lines.len() - 1)),
None => self.state.select(Some(0)),
Some(i) if i == 0 => self.state.select(Some(self.lines.len() - 2)),
Some(i) => self.state.select(Some(i - 1)),
}
}

View File

@ -1,5 +1,5 @@
pub mod async_tag_list;
pub mod details;
pub mod info;
pub mod repo_entry;
pub mod service_switcher;
pub mod tag_list;

View File

@ -12,7 +12,7 @@ pub struct RepoEntry {
impl RepoEntry {
pub fn new(text: Option<&str>) -> Self {
let default_text = "enter a repository here or select one from file widget";
let default_text = "edit me or select a repository";
Self {
text: String::from(text.unwrap_or(default_text)),
old_text: String::from(text.unwrap_or(default_text)),