[feat] first version

This commit is contained in:
jay 2022-02-14 15:35:29 +08:00
parent a8d59e48d1
commit fa3a2e60cd
4 changed files with 602 additions and 30 deletions

204
Cargo.lock generated
View File

@ -2,6 +2,17 @@
# It is not intended for manual editing.
version = 3
[[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"
@ -20,6 +31,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "bumpalo"
version = "3.9.1"
@ -50,6 +67,58 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clipboard"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7"
dependencies = [
"clipboard-win",
"objc",
"objc-foundation",
"objc_id",
"x11-clipboard",
]
[[package]]
name = "clipboard-win"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b"
dependencies = [
"winapi",
]
[[package]]
name = "core-foundation"
version = "0.9.2"
@ -213,6 +282,12 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -380,6 +455,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "matches"
version = "0.1.9"
@ -457,6 +541,35 @@ dependencies = [
"libc",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "once_cell"
version = "1.9.0"
@ -496,6 +609,15 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -545,6 +667,30 @@ version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[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.36"
@ -576,12 +722,15 @@ dependencies = [
name = "registry-cli"
version = "0.1.0"
dependencies = [
"clap",
"clipboard",
"crossterm",
"reqwest",
"serde",
"serde_json",
"tokio",
"tui",
"url",
]
[[package]]
@ -769,6 +918,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.86"
@ -794,6 +949,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
[[package]]
name = "tinyvec"
version = "1.5.1"
@ -959,6 +1129,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
version = "0.3.0"
@ -1061,6 +1237,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -1075,3 +1260,22 @@ checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
dependencies = [
"winapi",
]
[[package]]
name = "x11-clipboard"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea"
dependencies = [
"xcb",
]
[[package]]
name = "xcb"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de"
dependencies = [
"libc",
"log",
]

View File

@ -12,4 +12,6 @@ serde = {version = "1.0.136", features = ["derive"]}
serde_json = "1.0.78"
tokio = { version = "1", features = ["full"] }
tui = "0.17.0"
clap = { version = "3.0.14", features = ["derive"] }
clipboard = "0.5.0"
url = "2.2.2"

View File

@ -1,31 +1,397 @@
use clap::Parser;
use clipboard::{ClipboardContext, ClipboardProvider};
use std::{
io, thread,
time::{Duration, Instant},
};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
Frame, Terminal,
};
mod registry;
fn main() {
println!("Hello, world!");
let r = registry::api::Registry {
endpoint: "https://docker.mtfos.xyz".to_string(),
user: "".to_string(),
password: "".to_string(),
};
// let images = r.list_images().unwrap();
// println!("images ::: {:?}", images);
// let tags = r.get_image_tags("mtfos/go-bot").unwrap();
// println!("tags ::: {:?}", tags);
let digest = match r.get_image_tag_manifest("demo/node-hello", "v1") {
Ok(digest) => digest,
Err(e) => panic!("digest err ::: {:?}", e),
};
println!("digest ::: {:?}", digest);
match r.delete_image_tag("demo/node-hello", &digest) {
Ok(_) => println!("deleted"),
Err(e) => println!("error ::: {:?}", e),
};
struct StatefulList<T> {
state: ListState,
items: Vec<T>,
}
impl<T> StatefulList<T> {
fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i))
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i <= 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => self.items.len() - 1,
};
self.state.select(Some(i))
}
fn unselect(&mut self) {
self.state.select(None)
}
fn selected(&self) -> Option<&T> {
self.state.selected().map(|i| &self.items[i])
}
}
enum PageState {
Images,
Tags,
}
enum LoadingState {
Init,
Loading,
Loaded,
}
struct App {
registry_uri: String,
username: String,
password: String,
registry: Option<registry::api::Registry>,
page_state: PageState,
loading_state: LoadingState,
selected_image: Option<String>,
images: Option<StatefulList<String>>,
tags: Option<StatefulList<String>>,
}
impl App {
fn new(uri: String, username: String, password: String) -> App {
let mut app = App {
registry_uri: uri,
username,
password,
registry: None,
page_state: PageState::Images,
loading_state: LoadingState::Init,
selected_image: None,
images: None,
tags: None,
};
let api = registry::api::Registry {
endpoint: app.registry_uri.to_string(),
user: app.username.to_string(),
password: app.password.to_string(),
};
app.registry = Some(api);
app
}
}
#[derive(Debug, Clone, Parser)]
#[clap(version = "0.1.0")]
struct Args {
#[clap(short, long)]
registry_uri: String,
#[clap(short, long, default_value = "")]
username: String,
#[clap(short, long, default_value = "")]
password: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if args.registry_uri.is_empty() {
println!("Please provide a registry URI");
return Ok(());
}
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let app = App::new(args.registry_uri, args.username, args.password);
let res = run_app(&mut terminal, app);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(e) = res {
println!("{}", e);
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
) -> Result<(), Box<dyn std::error::Error>> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
match app.loading_state {
LoadingState::Init => {
app.loading_state = LoadingState::Loading;
loading(&mut app);
}
LoadingState::Loading => {}
LoadingState::Loaded => {}
}
let timeout = Duration::from_millis(250)
.checked_sub(last_tick.elapsed())
.unwrap_or(Duration::from_millis(0));
if crossterm::event::poll(timeout)? {
let event = crossterm::event::read()?;
if let Event::Key(key) = event {
match key.code {
KeyCode::Char('q') => return Ok(()),
_ => {}
}
match app.page_state {
PageState::Images => match key.code {
KeyCode::Char('r') => {
// reload list of images
app.loading_state = LoadingState::Init;
}
KeyCode::Down => {
if let Some(ref mut images) = app.images {
images.next();
}
}
KeyCode::Up => {
if let Some(ref mut images) = app.images {
images.previous();
}
}
KeyCode::Enter => {
app.selected_image = app.images.as_ref().unwrap().selected().cloned();
app.page_state = PageState::Tags;
app.loading_state = LoadingState::Init;
}
_ => {}
},
PageState::Tags => match key.code {
KeyCode::Esc => {
app.selected_image = None;
app.tags = None;
app.page_state = PageState::Images;
}
KeyCode::Down => {
if let Some(ref mut tags) = app.tags {
tags.next();
}
}
KeyCode::Up => {
if let Some(ref mut tags) = app.tags {
tags.previous();
}
}
KeyCode::Char('c') => {
// copy docker image uri
if let Some(ref image) = app.selected_image {
if let Some(ref tags) = app.tags {
if let Some(ref tag) = tags.selected() {
let mut u = url::Url::parse(&app.registry_uri)?;
u.set_path(image);
let uri = u
.to_string()
.replace(&format!("{}://", u.scheme()).to_string(), "");
let uri_str = format!("{}:{}", uri, tag);
let mut clipboard: ClipboardContext =
ClipboardProvider::new()?;
clipboard.set_contents(uri_str)?;
}
}
}
}
_ => {}
},
}
}
}
if last_tick.elapsed() >= Duration::from_millis(250) {
last_tick = Instant::now();
}
}
}
fn loading(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
match app.page_state {
PageState::Images => {
if let Some(registry) = &app.registry {
let images = registry.list_images()?;
app.images = Some(StatefulList::with_items(images));
app.loading_state = LoadingState::Loaded;
if let Some(ref mut images) = app.images {
images.state.select(Some(0));
}
}
}
PageState::Tags => {
if let Some(ref image) = app.selected_image {
let image_tags = app.registry.as_ref().unwrap().get_image_tags(image)?;
app.tags = Some(StatefulList::with_items(image_tags.tags));
app.loading_state = LoadingState::Loaded;
if let Some(ref mut tags) = app.tags {
tags.state.select(Some(0));
}
}
}
}
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(5),
Constraint::Percentage(85),
]
.as_ref(),
)
.split(f.size());
// chunk 0 is imfomation
show_info(f, app, chunks[0]);
// chunk 1 is the loading state
match app.loading_state {
LoadingState::Init | LoadingState::Loaded => {
f.render_widget(Block::default().borders(Borders::ALL), chunks[1]);
}
LoadingState::Loading => {
let txt = Spans::from(vec![Span::styled(
"Loading...",
Style::default().fg(Color::Yellow),
)]);
let paragraph = Paragraph::new(txt).block(Block::default().borders(Borders::ALL));
f.render_widget(paragraph, chunks[1]);
}
}
match app.page_state {
PageState::Images => page_images(f, app, chunks[2]),
PageState::Tags => page_tags(f, app, chunks[2]),
}
}
fn show_info<B: Backend>(f: &mut Frame<B>, app: &mut App, area: Rect) {
let chunks = Layout::default()
.margin(1)
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
.split(area);
let mut texts = vec![Spans::from(vec![
Span::raw("Registry: "),
Span::raw(&app.registry_uri),
])];
if let Some(ref selected_image) = app.selected_image {
texts.push(Spans::from(vec![
Span::raw("Selected Image: "),
Span::raw(selected_image.to_owned()),
]));
}
let paragraph = Paragraph::new(texts).block(Block::default().borders(Borders::NONE));
f.render_widget(paragraph, chunks[0]);
}
fn page_images<B: Backend>(f: &mut Frame<B>, app: &mut App, area: Rect) {
if let Some(ref mut images) = app.images {
let items: Vec<ListItem> = images
.items
.iter()
.map(|i| {
let line = Spans::from(vec![Span::raw(i.as_str())]);
ListItem::new(line)
})
.collect();
let list = List::new(items)
.block(Block::default().borders(Borders::ALL))
.highlight_style(Style::default().bg(Color::LightGreen).fg(Color::Black));
f.render_stateful_widget(list, area, &mut images.state);
}
}
fn page_tags<B: Backend>(f: &mut Frame<B>, app: &mut App, area: Rect) {
if let Some(ref mut tags) = app.tags {
let items: Vec<ListItem> = tags
.items
.iter()
.map(|i| {
let line = Spans::from(vec![Span::raw(i.as_str())]);
ListItem::new(line)
})
.collect();
let list = List::new(items)
.block(Block::default().borders(Borders::ALL))
.highlight_style(Style::default().bg(Color::LightGreen).fg(Color::Black));
f.render_stateful_widget(list, area, &mut tags.state);
}
}

View File

@ -77,7 +77,7 @@ impl Registry {
let headers = resp.headers().to_owned();
println!("\n{:?}\n", headers);
// println!("\n{:?}\n", headers);
if !headers
.get("Content-Type")