Last active
May 21, 2023 08:57
-
-
Save tomekowal/77c2dac3e9b874a209f6f1570b3d7ba4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crossterm::{ | |
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, | |
execute, | |
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, | |
}; | |
use std::{ | |
error::Error, | |
fmt, | |
io, | |
env | |
}; | |
use strum_macros::EnumIter; | |
use strum::IntoEnumIterator; | |
use ratatui::{ | |
backend::{Backend, CrosstermBackend}, | |
layout::Alignment, | |
widgets::{Block, BorderType, Borders, List, ListItem}, | |
Frame, Terminal, | |
}; | |
#[derive(EnumIter, Copy, Clone, PartialEq, Eq, Hash)] | |
enum Tier { | |
Dev, | |
Int, | |
Prod, | |
Local | |
} | |
#[derive(EnumIter, Copy, Clone, PartialEq, Eq, Hash)] | |
enum Service { | |
PosExample, | |
Acquibase, | |
SchemeServices, | |
EmpsaBridgeSimulatorAcquirer, | |
EmpsaBridge, | |
EmpsaBridgeSimulatorIssuer, | |
} | |
pub trait ServiceLink { | |
fn aws_prefix(&self) -> &str; | |
fn local_prefix(&self) -> &str; | |
fn domain(&self) -> &str; | |
fn path_suffix(&self) -> &str; | |
} | |
impl Selectable for Action { | |
fn shortcut(&self) -> char { | |
match self { | |
Action::Open => 'o' | |
} | |
} | |
} | |
impl Selectable for Tier { | |
fn shortcut(&self) -> char { | |
match self { | |
Tier::Dev => 'm', | |
Tier::Int => 'b', | |
Tier::Prod => 'c', | |
Tier::Local => 'l' | |
} | |
} | |
} | |
impl Selectable for Service { | |
fn shortcut(&self) -> char { | |
match self { | |
Service::PosExample => 'p', | |
Service::Acquibase => 'c', | |
Service::SchemeServices => 's', | |
Service::EmpsaBridgeSimulatorAcquirer => 'u', | |
Service::EmpsaBridge => 'b', | |
Service::EmpsaBridgeSimulatorIssuer => 'i', | |
} | |
} | |
} | |
impl fmt::Display for Action { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
match self { | |
Action::Open => write!(f, "Open") | |
} | |
} | |
} | |
impl fmt::Display for Tier { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
match self { | |
Tier::Dev => write!(f, "dev"), | |
Tier::Int => write!(f, "int"), | |
Tier::Prod => write!(f, "prod"), | |
Tier::Local => write!(f, "local"), | |
} | |
} | |
} | |
impl fmt::Display for Service { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
match self { | |
Service::PosExample => write!(f, "POS Example"), | |
Service::Acquibase => write!(f, "AcquiBase"), | |
Service::SchemeServices => write!(f, "Scheme Services"), | |
Service::EmpsaBridgeSimulatorAcquirer => write!(f, "EMPSA Bridge Simulator (Acquirer)"), | |
Service::EmpsaBridge => write!(f, "EMPSA Bridge"), | |
Service::EmpsaBridgeSimulatorIssuer => write!(f, "EMPSA Bridge Simulator (Issuer)"), | |
} | |
} | |
} | |
impl ServiceLink for Service { | |
fn aws_prefix(&self) -> &str { | |
match self { | |
Service::PosExample => "pos-example", | |
Service::Acquibase => "acquibase", | |
Service::SchemeServices => "payments-admin", | |
Service::EmpsaBridgeSimulatorAcquirer => "admin.empsa-scheme-simulator", | |
Service::EmpsaBridge => "admin.empsa-bridge", | |
Service::EmpsaBridgeSimulatorIssuer => "admin.empsa-scheme-simulator", | |
} | |
} | |
fn local_prefix(&self) -> &str { | |
match self { | |
Service::PosExample => "pos-example", | |
Service::Acquibase => "acquibase", | |
Service::SchemeServices => "payments-admin", | |
Service::EmpsaBridgeSimulatorAcquirer => "bridge-simulator", | |
Service::EmpsaBridge => "bridge", | |
Service::EmpsaBridgeSimulatorIssuer => "bridge-simulator", | |
} | |
} | |
fn domain(&self) -> &str { | |
match self { | |
Service::PosExample => "bluecode", | |
Service::Acquibase => "bluecode", | |
Service::SchemeServices => "bluecode", | |
Service::EmpsaBridgeSimulatorAcquirer => "spt-payments", | |
Service::EmpsaBridge => "spt-payments", | |
Service::EmpsaBridgeSimulatorIssuer => "spt-payments", | |
} | |
} | |
fn path_suffix(&self) -> &str { | |
match self { | |
Service::EmpsaBridgeSimulatorAcquirer => "/admin/acq/acq_payments", | |
Service::EmpsaBridge => "/bridge/admin/signin", | |
Service::EmpsaBridgeSimulatorIssuer => "/admin/iss/payments", | |
_ => "" | |
} | |
} | |
} | |
#[derive(EnumIter, Copy, Clone, PartialEq, Eq, Hash)] | |
enum Action { | |
Open | |
} | |
#[derive(EnumIter, Copy, Clone, PartialEq, Eq, Hash)] | |
enum WizardStep { | |
Action, | |
Tier, | |
Services | |
} | |
pub trait TopLevelDomain { | |
fn tld(&self) -> &str; | |
} | |
impl TopLevelDomain for Tier { | |
fn tld(&self) -> &str { | |
match self { | |
// the local one is unused | |
// I consider nesting it: Aws(Dev | Int | Prod) | Local | |
Tier::Local => "de", | |
Tier::Prod => "com", | |
Tier::Int => "biz", | |
Tier::Dev => "mobi" | |
} | |
} | |
} | |
struct App { | |
bc_dev_domain: String, | |
wizard_step: WizardStep, | |
action: Option<Action>, | |
tier: Option<Tier> | |
} | |
impl App { | |
fn new() -> App { | |
App { | |
bc_dev_domain: env::var("BC_DEV_DOMAIN").expect("BC_DEV_DOMAIN env variable must be set"), | |
wizard_step: WizardStep::Action, | |
action: None, | |
tier: None | |
} | |
} | |
} | |
fn main() -> Result<(), Box<dyn Error>> { | |
// setup terminal | |
enable_raw_mode()?; | |
let mut stdout = io::stdout(); | |
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; | |
let backend = CrosstermBackend::new(stdout); | |
let mut terminal = Terminal::new(backend)?; | |
let mut app = App::new(); | |
// create app and run it | |
let res = run_app(&mut terminal, &mut app); | |
// restore terminal | |
disable_raw_mode()?; | |
execute!( | |
terminal.backend_mut(), | |
LeaveAlternateScreen, | |
DisableMouseCapture | |
)?; | |
terminal.show_cursor()?; | |
if let Err(err) = res { | |
println!("{:?}", err) | |
} | |
Ok(()) | |
} | |
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> { | |
loop { | |
terminal.draw(|f| ui(f, app))?; | |
if let Event::Key(key) = event::read()? { | |
match key.code { | |
KeyCode::Char('q') => return Ok(()), | |
KeyCode::Char(pressed_letter) => update(app, pressed_letter), | |
_ => () | |
} | |
} | |
} | |
} | |
fn update(app: &mut App, pressed_letter: char) { | |
match app.wizard_step { | |
WizardStep::Action => select_action(app, pressed_letter), | |
WizardStep::Tier => select_tier(app, pressed_letter), | |
WizardStep::Services => select_services(app, pressed_letter), | |
} | |
} | |
fn select_action(app: &mut App, pressed_letter: char) { | |
if let Some(action) = keymap::get_variant_from_letter::<Action>(pressed_letter) { | |
app.action = Some(action); | |
app.wizard_step = WizardStep::Tier; | |
} else { | |
//TODO: reset | |
} | |
} | |
fn select_tier(app: &mut App, pressed_letter: char) { | |
if let Some(tier) = keymap::get_variant_from_letter::<Tier>(pressed_letter) { | |
app.tier = Some(tier); | |
app.wizard_step = WizardStep::Services; | |
} else { | |
//TODO: reset | |
} | |
} | |
fn select_services(app: &mut App, pressed_letter: char) { | |
match pressed_letter { | |
'a' => { | |
for service in Service::iter() { | |
if let Some(tier) = app.tier { | |
open_service(tier, service, app.bc_dev_domain.clone()); | |
} | |
} | |
app.wizard_step = WizardStep::Action; | |
} | |
_ => | |
if let Some(service) = keymap::get_variant_from_letter::<Service>(pressed_letter) { | |
if let Some(tier) = app.tier { | |
open_service(tier, service, app.bc_dev_domain.clone()); | |
app.wizard_step = WizardStep::Action; | |
} | |
} | |
} | |
} | |
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) { | |
// Wrapping block for a group | |
// Just draw the block and the group on the same area and build the group | |
// with at least a margin of 1 | |
let size = f.size(); | |
let title = match app.wizard_step { | |
WizardStep::Action => "Select action", | |
WizardStep::Tier => "Select tier", | |
WizardStep::Services => "Select services" | |
}; | |
let top_block = Block::default() | |
.borders(Borders::ALL) | |
.title(title) | |
.title_alignment(Alignment::Left) | |
.border_type(BorderType::Rounded); | |
let items = match app.wizard_step { | |
WizardStep::Action => { | |
labels(Action::iter()) | |
}, | |
WizardStep::Tier => { | |
labels(Tier::iter()) | |
}, | |
WizardStep::Services => { | |
let mut labels = labels(Service::iter()); | |
labels.push("a - All".to_string()); | |
labels | |
} | |
}; | |
let list_items = to_list_items(items); | |
let menu = List::new(list_items).block(top_block); | |
f.render_widget(menu, size); | |
} | |
fn labels(enum_iter: impl IntoIterator<Item = impl fmt::Display + Selectable>) -> Vec<String> { | |
let mut labels: Vec<String> = Vec::new(); | |
for action in enum_iter { | |
let label: String = [action.shortcut().to_string(), "-".to_string(), action.to_string()].join(" "); | |
labels.push(label); | |
}; | |
labels | |
} | |
fn to_list_items(strings: Vec<String>) -> Vec<ListItem<'static>> { | |
let mut list_items: Vec<ListItem> = Vec::new(); | |
for string in strings { | |
let list_item = ListItem::new(string); | |
list_items.push(list_item); | |
} | |
list_items | |
} | |
fn open_service(tier: Tier, service: Service, bc_dev_domain: String) { | |
let link = link(tier, service, bc_dev_domain); | |
open::that(link).unwrap(); | |
} | |
fn link(tier: Tier, service: Service, bc_dev_domain: String) -> String { | |
match tier { | |
Tier::Local => format!("https://{}.{}{}", service.local_prefix(), bc_dev_domain, service.path_suffix()), | |
_ => format!("https://{}.{}.{}{}", service.aws_prefix(), service.domain(), tier.tld(), service.path_suffix()) | |
} | |
} | |
pub trait Selectable { | |
fn shortcut(&self) -> char; | |
} | |
pub mod keymap { | |
use std::collections::HashMap; | |
use crate::Selectable; | |
use strum::IntoEnumIterator; | |
use std::fmt::Display; | |
// Function to create hash map letter => EnumVariant for a generic enum | |
// Panics on duplicated letters | |
fn create_bidirectional_map<T: Selectable + IntoEnumIterator + std::hash::Hash + Eq + Clone + Copy + Display>() -> HashMap<char, T> { | |
let mut map = HashMap::new(); | |
let variants = T::iter(); | |
for variant in variants { | |
let letter = variant.shortcut(); | |
if let Some(existing_variant) = map.insert(letter, variant) { | |
panic!("Duplicate key detected: {} {} {}", letter, existing_variant, variant); | |
} | |
map.insert(letter, variant); | |
} | |
map | |
} | |
// Function to get the enum variant from a letter | |
// recreates the hashmap from scratch every time | |
// it is simpler and I don't worry about performance | |
pub fn get_variant_from_letter<T: Selectable + IntoEnumIterator + std::hash::Hash + Eq + Clone + Copy + Display>(letter: char) -> Option<T> { | |
let map = create_bidirectional_map::<T>(); | |
map.get(&letter).cloned() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment