feat: add reconciliation loop
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nocontrol-tui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "nocontrol_tui"
|
||||
|
||||
@@ -3,20 +3,22 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
|
||||
},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use nocontrol::{manifests::ManifestState, ControlPlane, Operator, Specification};
|
||||
use nocontrol::{ControlPlane, Operator, Specification, manifests::ManifestState};
|
||||
use ratatui::{
|
||||
Frame, Terminal,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -265,22 +267,36 @@ impl<TOperator: Operator> App<TOperator> {
|
||||
}
|
||||
["help"] => {
|
||||
self.messages.push("Commands:".to_string());
|
||||
self.messages.push(" get, list - Refresh manifest list".to_string());
|
||||
self.messages.push(" describe - Show selected manifest details".to_string());
|
||||
self.messages.push(" history - Show command history".to_string());
|
||||
self.messages.push(" clear - Clear output messages".to_string());
|
||||
self.messages.push(" quit, exit, q - Exit application".to_string());
|
||||
self.messages.push(" help - Show this help".to_string());
|
||||
self.messages
|
||||
.push(" get, list - Refresh manifest list".to_string());
|
||||
self.messages
|
||||
.push(" describe - Show selected manifest details".to_string());
|
||||
self.messages
|
||||
.push(" history - Show command history".to_string());
|
||||
self.messages
|
||||
.push(" clear - Clear output messages".to_string());
|
||||
self.messages
|
||||
.push(" quit, exit, q - Exit application".to_string());
|
||||
self.messages
|
||||
.push(" help - Show this help".to_string());
|
||||
self.messages.push("".to_string());
|
||||
self.messages.push("Keys:".to_string());
|
||||
self.messages.push(" / - Toggle search mode".to_string());
|
||||
self.messages.push(" ↑/↓ - Navigate list OR cycle command history".to_string());
|
||||
self.messages.push(" Enter - Execute command (or exit search)".to_string());
|
||||
self.messages.push(" Esc - Clear input / exit search".to_string());
|
||||
self.messages.push(" q - Quick quit (when input empty)".to_string());
|
||||
self.messages
|
||||
.push(" / - Toggle search mode".to_string());
|
||||
self.messages
|
||||
.push(" ↑/↓ - Navigate list OR cycle command history".to_string());
|
||||
self.messages
|
||||
.push(" Enter - Execute command (or exit search)".to_string());
|
||||
self.messages
|
||||
.push(" Esc - Clear input / exit search".to_string());
|
||||
self.messages
|
||||
.push(" q - Quick quit (when input empty)".to_string());
|
||||
}
|
||||
_ => {
|
||||
self.messages.push(format!("Unknown command: {}. Type 'help' for commands.", cmd));
|
||||
self.messages.push(format!(
|
||||
"Unknown command: {}. Type 'help' for commands.",
|
||||
cmd
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,76 +337,76 @@ where
|
||||
}
|
||||
|
||||
// Handle input with timeout
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
let mut app = app.lock().await;
|
||||
if event::poll(Duration::from_millis(100))?
|
||||
&& let Event::Key(key) = event::read()?
|
||||
&& key.kind == KeyEventKind::Press
|
||||
{
|
||||
let mut app = app.lock().await;
|
||||
|
||||
match (key.code, key.modifiers, app.input_mode) {
|
||||
// Quick quit with 'q' when input is empty
|
||||
(KeyCode::Char('q'), KeyModifiers::NONE, InputMode::Normal)
|
||||
if app.command_input.is_empty() => {
|
||||
app.should_quit = true;
|
||||
}
|
||||
|
||||
// Toggle search mode with '/'
|
||||
(KeyCode::Char('/'), KeyModifiers::NONE, _) => {
|
||||
app.toggle_search_mode();
|
||||
}
|
||||
|
||||
// Character input
|
||||
(KeyCode::Char(c), _, _) => {
|
||||
app.command_input.push(c);
|
||||
app.update_search();
|
||||
}
|
||||
|
||||
// Backspace
|
||||
(KeyCode::Backspace, _, _) => {
|
||||
app.command_input.pop();
|
||||
app.update_search();
|
||||
}
|
||||
|
||||
// Enter key
|
||||
(KeyCode::Enter, _, InputMode::Search) => {
|
||||
// Exit search mode but keep filter
|
||||
app.input_mode = InputMode::Normal;
|
||||
app.command_input.clear();
|
||||
}
|
||||
(KeyCode::Enter, _, InputMode::Normal) => {
|
||||
app.execute_command().await;
|
||||
}
|
||||
|
||||
// Arrow keys
|
||||
(KeyCode::Up, _, InputMode::Normal) if !app.command_input.is_empty() => {
|
||||
// Navigate history when typing a command
|
||||
app.history_previous();
|
||||
}
|
||||
(KeyCode::Down, _, InputMode::Normal) if !app.command_input.is_empty() => {
|
||||
// Navigate history when typing a command
|
||||
app.history_next();
|
||||
}
|
||||
(KeyCode::Up, _, _) => {
|
||||
// Navigate manifest list
|
||||
app.previous();
|
||||
}
|
||||
(KeyCode::Down, _, _) => {
|
||||
// Navigate manifest list
|
||||
app.next();
|
||||
}
|
||||
|
||||
// Escape key
|
||||
(KeyCode::Esc, _, InputMode::Search) => {
|
||||
app.toggle_search_mode();
|
||||
app.command_input.clear();
|
||||
}
|
||||
(KeyCode::Esc, _, InputMode::Normal) => {
|
||||
app.command_input.clear();
|
||||
app.history_index = None;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
match (key.code, key.modifiers, app.input_mode) {
|
||||
// Quick quit with 'q' when input is empty
|
||||
(KeyCode::Char('q'), KeyModifiers::NONE, InputMode::Normal)
|
||||
if app.command_input.is_empty() =>
|
||||
{
|
||||
app.should_quit = true;
|
||||
}
|
||||
|
||||
// Toggle search mode with '/'
|
||||
(KeyCode::Char('/'), KeyModifiers::NONE, _) => {
|
||||
app.toggle_search_mode();
|
||||
}
|
||||
|
||||
// Character input
|
||||
(KeyCode::Char(c), _, _) => {
|
||||
app.command_input.push(c);
|
||||
app.update_search();
|
||||
}
|
||||
|
||||
// Backspace
|
||||
(KeyCode::Backspace, _, _) => {
|
||||
app.command_input.pop();
|
||||
app.update_search();
|
||||
}
|
||||
|
||||
// Enter key
|
||||
(KeyCode::Enter, _, InputMode::Search) => {
|
||||
// Exit search mode but keep filter
|
||||
app.input_mode = InputMode::Normal;
|
||||
app.command_input.clear();
|
||||
}
|
||||
(KeyCode::Enter, _, InputMode::Normal) => {
|
||||
app.execute_command().await;
|
||||
}
|
||||
|
||||
// Arrow keys
|
||||
(KeyCode::Up, _, InputMode::Normal) if !app.command_input.is_empty() => {
|
||||
// Navigate history when typing a command
|
||||
app.history_previous();
|
||||
}
|
||||
(KeyCode::Down, _, InputMode::Normal) if !app.command_input.is_empty() => {
|
||||
// Navigate history when typing a command
|
||||
app.history_next();
|
||||
}
|
||||
(KeyCode::Up, _, _) => {
|
||||
// Navigate manifest list
|
||||
app.previous();
|
||||
}
|
||||
(KeyCode::Down, _, _) => {
|
||||
// Navigate manifest list
|
||||
app.next();
|
||||
}
|
||||
|
||||
// Escape key
|
||||
(KeyCode::Esc, _, InputMode::Search) => {
|
||||
app.toggle_search_mode();
|
||||
app.command_input.clear();
|
||||
}
|
||||
(KeyCode::Esc, _, InputMode::Normal) => {
|
||||
app.command_input.clear();
|
||||
app.history_index = None;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -403,10 +419,10 @@ fn ui<TOperator: Operator>(f: &mut Frame, app: &mut App<TOperator>) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Title
|
||||
Constraint::Min(10), // Main content
|
||||
Constraint::Length(10), // Messages
|
||||
Constraint::Length(3), // Command input
|
||||
Constraint::Length(3), // Title
|
||||
Constraint::Min(10), // Main content
|
||||
Constraint::Length(10), // Messages
|
||||
Constraint::Length(3), // Command input
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
@@ -415,21 +431,25 @@ fn ui<TOperator: Operator>(f: &mut Frame, app: &mut App<TOperator>) {
|
||||
InputMode::Normal => "NORMAL",
|
||||
InputMode::Search => "SEARCH",
|
||||
};
|
||||
let title = Paragraph::new(vec![
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
"NoControl - Kubernetes-like Control Plane",
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!("[{}]", mode_text),
|
||||
Style::default()
|
||||
.fg(if app.input_mode == InputMode::Search { Color::Yellow } else { Color::Green })
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]),
|
||||
])
|
||||
let title = Paragraph::new(vec![Line::from(vec![
|
||||
Span::styled(
|
||||
"NoControl - Kubernetes-like Control Plane",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!("[{}]", mode_text),
|
||||
Style::default()
|
||||
.fg(if app.input_mode == InputMode::Search {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Green
|
||||
})
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])])
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
@@ -454,7 +474,8 @@ fn ui<TOperator: Operator>(f: &mut Frame, app: &mut App<TOperator>) {
|
||||
|
||||
fn render_manifest_list<TOperator: Operator>(f: &mut Frame, app: &mut App<TOperator>, area: Rect) {
|
||||
// Collect filtered manifests data before borrowing list_state
|
||||
let filtered_data: Vec<_> = app.filtered_indices
|
||||
let filtered_data: Vec<_> = app
|
||||
.filtered_indices
|
||||
.iter()
|
||||
.filter_map(|&idx| app.manifests.get(idx))
|
||||
.map(|m| (m.manifest.name.clone(), m.status.status.clone()))
|
||||
@@ -469,14 +490,12 @@ fn render_manifest_list<TOperator: Operator>(f: &mut Frame, app: &mut App<TOpera
|
||||
nocontrol::manifests::ManifestStatusState::Pending => Color::Gray,
|
||||
nocontrol::manifests::ManifestStatusState::Stopping => Color::Magenta,
|
||||
nocontrol::manifests::ManifestStatusState::Deleting => Color::Red,
|
||||
nocontrol::manifests::ManifestStatusState::Errored => Color::Red,
|
||||
};
|
||||
|
||||
let status_text = format!("{:?}", status);
|
||||
let content = Line::from(vec![
|
||||
Span::styled(
|
||||
"● ",
|
||||
Style::default().fg(status_color),
|
||||
),
|
||||
Span::styled("● ", Style::default().fg(status_color)),
|
||||
Span::raw(name),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
@@ -526,7 +545,10 @@ fn render_manifest_details<TOperator: Operator>(f: &mut Frame, app: &App<TOperat
|
||||
Span::raw(format!("{:?}", manifest.status.status)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Generation: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
"Generation: ",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!("{}", manifest.generation)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
@@ -541,7 +563,10 @@ fn render_manifest_details<TOperator: Operator>(f: &mut Frame, app: &App<TOperat
|
||||
|
||||
if !manifest.status.events.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled("Events:", Style::default().add_modifier(Modifier::BOLD))));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Events:",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
for event in manifest.status.events.iter().rev().take(5) {
|
||||
lines.push(Line::from(format!(" • {}", event.message)));
|
||||
}
|
||||
@@ -549,7 +574,10 @@ fn render_manifest_details<TOperator: Operator>(f: &mut Frame, app: &App<TOperat
|
||||
|
||||
if !manifest.status.changes.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled("Changes:", Style::default().add_modifier(Modifier::BOLD))));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Changes:",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
for change in manifest.status.changes.iter().rev().take(5) {
|
||||
lines.push(Line::from(format!(
|
||||
" • {:?} at {}",
|
||||
@@ -596,7 +624,10 @@ fn render_command_input<TOperator: Operator>(f: &mut Frame, app: &App<TOperator>
|
||||
String::new()
|
||||
};
|
||||
(
|
||||
format!(" Command{} (/ to search, ↑↓ for history, 'help' for commands) ", hist_info),
|
||||
format!(
|
||||
" Command{} (/ to search, ↑↓ for history, 'help' for commands) ",
|
||||
hist_info
|
||||
),
|
||||
app.command_input.as_str(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
)
|
||||
@@ -604,7 +635,9 @@ fn render_command_input<TOperator: Operator>(f: &mut Frame, app: &App<TOperator>
|
||||
InputMode::Search => (
|
||||
" Search (fuzzy) - Enter to apply, Esc to cancel ".to_string(),
|
||||
app.command_input.as_str(),
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user