mod app;
mod aws_ssm;
mod editor_tools;
mod models;
mod text_edit;
mod ui;
use std::{
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
use app::App;
use aws_ssm::{fetch_parameter_value_from_ssm, put_parameter_value_to_ssm};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use editor_tools::{copy_to_clipboard, open_value_in_editor};
use models::{CreateField, ValueEditorMode};
use ratatui::{Terminal, backend::CrosstermBackend};
use text_edit::{
backspace_at_cursor, delete_at_cursor, insert_char_at_cursor, move_cursor_down,
move_cursor_left, move_cursor_line_end, move_cursor_line_start, move_cursor_right,
move_cursor_up,
};
use ui::{draw, draw_loading};
fn main() -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let app_result = run(&mut terminal);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
app_result
}
fn run(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
let (init_tx, init_rx) = mpsc::channel::<App>();
thread::spawn(move || {
let app = App::new();
let _ = init_tx.send(app);
});
let spinner = ["|", "/", "-", "\\"];
let mut spinner_idx = 0usize;
let started = Instant::now();
loop {
if let Ok(mut app) = init_rx.try_recv() {
return run_app_loop(terminal, &mut app);
}
let frame_spinner = spinner[spinner_idx % spinner.len()];
spinner_idx = spinner_idx.wrapping_add(1);
let elapsed_secs = started.elapsed().as_secs();
terminal.draw(|frame| draw_loading(frame, frame_spinner, elapsed_secs))?;
if event::poll(Duration::from_millis(120))? {
let Event::Key(key) = event::read()? else {
continue;
};
if key.kind == KeyEventKind::Press
&& matches!(key.code, KeyCode::Char('q') | KeyCode::Esc)
{
return Ok(());
}
}
}
}
fn run_app_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
) -> io::Result<()> {
loop {
app.pump_full_refresh_updates();
app.pump_value_updates();
app.prefetch_near_selected();
terminal.draw(|frame| draw(frame, app))?;
if event::poll(Duration::from_millis(120))? {
let Event::Key(key) = event::read()? else {
continue;
};
if key.kind != KeyEventKind::Press {
continue;
}
if app.create_mode {
if key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char('s') | KeyCode::Char('S'))
{
app.submit_create();
continue;
}
match app.create_field {
CreateField::Name => match key.code {
KeyCode::Esc => app.cancel_create(),
KeyCode::Tab | KeyCode::Down | KeyCode::Up => app.switch_create_field(),
KeyCode::Enter => {
app.create_field = CreateField::Value;
app.create_value_mode = ValueEditorMode::Insert;
}
KeyCode::Left => {
move_cursor_left(&app.create_name, &mut app.create_name_cursor)
}
KeyCode::Right => {
move_cursor_right(&app.create_name, &mut app.create_name_cursor)
}
KeyCode::Home => {
move_cursor_line_start(&app.create_name, &mut app.create_name_cursor)
}
KeyCode::End => {
move_cursor_line_end(&app.create_name, &mut app.create_name_cursor)
}
KeyCode::Backspace => {
backspace_at_cursor(&mut app.create_name, &mut app.create_name_cursor)
}
KeyCode::Delete => {
delete_at_cursor(&mut app.create_name, &mut app.create_name_cursor)
}
KeyCode::Char(c) => {
if !key.modifiers.contains(KeyModifiers::CONTROL) {
insert_char_at_cursor(
&mut app.create_name,
&mut app.create_name_cursor,
c,
);
}
}
_ => {}
},
CreateField::Value => match app.create_value_mode {
ValueEditorMode::Insert => match key.code {
KeyCode::Esc => app.create_value_mode = ValueEditorMode::Normal,
KeyCode::Tab => app.switch_create_field(),
KeyCode::Left => {
move_cursor_left(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Right => {
move_cursor_right(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Up => {
move_cursor_up(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Down => {
move_cursor_down(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Home => {
move_cursor_line_start(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::End => {
move_cursor_line_end(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Enter => insert_char_at_cursor(
&mut app.create_value,
&mut app.create_value_cursor,
'\n',
),
KeyCode::Backspace => {
backspace_at_cursor(&mut app.create_value, &mut app.create_value_cursor)
}
KeyCode::Delete => {
delete_at_cursor(&mut app.create_value, &mut app.create_value_cursor)
}
KeyCode::Char(c) => {
if !key.modifiers.contains(KeyModifiers::CONTROL) {
insert_char_at_cursor(
&mut app.create_value,
&mut app.create_value_cursor,
c,
);
}
}
_ => {}
},
ValueEditorMode::Normal => match key.code {
KeyCode::Esc => app.cancel_create(),
KeyCode::Tab => app.switch_create_field(),
KeyCode::Char('i') => app.create_value_mode = ValueEditorMode::Insert,
KeyCode::Char('a') => {
move_cursor_right(&app.create_value, &mut app.create_value_cursor);
app.create_value_mode = ValueEditorMode::Insert;
}
KeyCode::Char('h') | KeyCode::Left => {
move_cursor_left(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Char('l') | KeyCode::Right => {
move_cursor_right(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Char('k') | KeyCode::Up => {
move_cursor_up(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Char('j') | KeyCode::Down => {
move_cursor_down(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Char('0') | KeyCode::Home => {
move_cursor_line_start(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Char('$') | KeyCode::End => {
move_cursor_line_end(&app.create_value, &mut app.create_value_cursor)
}
KeyCode::Char('x') | KeyCode::Delete => {
delete_at_cursor(&mut app.create_value, &mut app.create_value_cursor)
}
KeyCode::Enter => app.submit_create(),
_ => {}
},
},
}
continue;
}
if app.search_mode {
match key.code {
KeyCode::Esc | KeyCode::Enter => app.end_search(),
KeyCode::Down | KeyCode::Char('j') => app.next(),
KeyCode::Up | KeyCode::Char('k') => app.previous(),
KeyCode::Backspace => {
app.query.pop();
app.apply_filter();
}
KeyCode::Char(c) => {
app.query.push(c);
app.apply_filter();
}
_ => {}
}
} else {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('/') => app.start_search(),
KeyCode::Char('a') => app.start_create(),
KeyCode::Char('R') => app.start_full_refresh(),
KeyCode::Char('y') => {
if let Some(param) = app.selected_parameter() {
let param_name = param.name.clone();
let param_value = param.value.clone();
if let Some(value) = param_value {
match copy_to_clipboard(&value) {
Ok(()) => {
app.status =
format!("Copied value of {param_name} to clipboard");
}
Err(err) => {
app.status = format!("Clipboard error: {err}");
}
}
} else {
app.request_value_for_name(¶m_name);
app.status =
format!("Value for {param_name} not loaded yet. Loading in background");
}
} else {
app.status = String::from("No selection to copy");
}
}
KeyCode::Char('e') => {
if let Some(param) = app.selected_parameter() {
let param_name = param.name.clone();
let param_meta = param.meta.clone();
match fetch_parameter_value_from_ssm(
¶m_name,
app.configured_region_owned(),
) {
Ok(fetched_value) => {
app.set_value_for_name(¶m_name, fetched_value.clone());
match open_value_in_editor(terminal, &fetched_value) {
Ok(Some(edited_value)) => {
match put_parameter_value_to_ssm(
¶m_name,
&edited_value,
¶m_meta,
app.configured_region_owned(),
) {
Ok(()) => {
app.set_value_for_name(¶m_name, edited_value);
app.status = format!(
"Updated {} in SSM and local state",
param_name
);
}
Err(err) => {
app.status = format!("SSM update error: {err}");
}
}
}
Ok(None) => {
app.status = format!("No changes for {}", param_name);
}
Err(err) => {
app.status = format!("Editor error: {err}");
}
}
}
Err(err) => {
app.status = format!("Fetch before edit failed: {err}");
}
}
} else {
app.status = String::from("No selection to edit");
}
}
KeyCode::Down | KeyCode::Char('j') => app.next(),
KeyCode::Up | KeyCode::Char('k') => app.previous(),
_ => {}
}
}
}
}
}