Skip to content

Instantly share code, notes, and snippets.

@tomekowal
Last active May 21, 2023 08:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomekowal/77c2dac3e9b874a209f6f1570b3d7ba4 to your computer and use it in GitHub Desktop.
Save tomekowal/77c2dac3e9b874a209f6f1570b3d7ba4 to your computer and use it in GitHub Desktop.
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