[feat] first version
This commit is contained in:
+394
-28
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user