use std::cell::{Ref, RefCell};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use futures_util::StreamExt;
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use k8s_openapi::api::core::v1::Pod;
use kube::Client;
use kube::api::{Api, DeleteParams, EvictParams, ListParams, LogParams, Patch, PatchParams};
use kube::core::{DynamicObject, TypeMeta};
use kube::discovery::ApiResource;
use kube::runtime::watcher;
use ratatui::widgets::{ListState, TableState};
use serde_json::{Value, json};
use tokio::sync::mpsc::Sender;
use tokio::task::JoinHandle;
use crate::k8s::{Cluster, Kind};
use crate::store::{Msg, Pulse, Store, XrayItem, row_key};
const MAX_LOG_LINES: usize = 5_000;
const MAX_LOG_LINES_PAUSED: usize = 100_000;
const LOG_BATCH_LINES: usize = 64;
const LOG_BATCH_MS: u64 = 50;
const FLUX_SUSPENDABLE_KINDS: &[&str] = &[
"kustomizations",
"helmreleases",
"gitrepositories",
"helmrepositories",
"ocirepositories",
"buckets",
"imagerepositories",
"imageupdateautomations",
"alerts",
"receivers",
];
pub const FLUX_MENU_ITEMS: &[&str] = &["Suspend", "Resume", "Reconcile now", "Cancel"];
const EXTERNAL_SECRET_KINDS: &[&str] = &["externalsecrets", "pushsecrets"];
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Mode {
Table,
Command,
Filter,
Detail,
Logs,
LogFilter,
Help,
Namespaces,
Contexts,
Containers,
SetImage,
Confirm,
Prompt,
Pulse,
Xray,
Diff,
Events,
FluxMenu,
PortForwards,
Skins,
}
pub enum Suspend {
Shell(Vec<String>),
}
pub struct PortForward {
ns: String,
target: String,
ports: String,
child: tokio::process::Child,
}
impl PortForward {
pub fn label(&self) -> String {
format!("{} {} -n {}", self.target, self.ports, self.ns)
}
}
impl Drop for PortForward {
fn drop(&mut self) {
let _ = self.child.start_kill();
}
}
enum ConfirmAction {
Delete {
targets: Vec<(String, String)>,
force: bool,
},
Drain { targets: Vec<String> },
}
#[derive(Clone)]
enum LogSource {
Pod {
ns: String,
name: String,
containers: Vec<String>,
},
Selector { ns: String, labels: String },
Single {
ns: String,
pod: String,
container: Option<String>,
previous: bool,
},
}
enum PromptKind {
Scale {
ns: String,
name: String,
},
PortForward {
ns: String,
name: String,
},
SetImage {
ns: String,
name: String,
plural: String,
container: String,
},
}
pub struct Scrollable {
pub title: String,
pub lines: VecDeque<String>,
pub scroll: usize,
}
#[derive(Clone)]
pub struct Suggestion {
pub label: String,
pub kind: SuggestKind,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SuggestKind {
Command,
Resource,
}
struct PaletteCommand {
action: PaletteAction,
names: &'static [&'static str],
}
#[derive(Clone, Copy)]
enum PaletteAction {
Quit,
Ctx,
Pulse,
Xray,
Diff,
Events,
PortForwards,
Skin,
}
const PALETTE_COMMANDS: &[PaletteCommand] = &[
PaletteCommand {
action: PaletteAction::Ctx,
names: &["ctx", "context", "contexts"],
},
PaletteCommand {
action: PaletteAction::Pulse,
names: &["pulse", "dashboard", "pu"],
},
PaletteCommand {
action: PaletteAction::Xray,
names: &["xray", "x"],
},
PaletteCommand {
action: PaletteAction::Diff,
names: &["diff"],
},
PaletteCommand {
action: PaletteAction::Events,
names: &["events", "event"],
},
PaletteCommand {
action: PaletteAction::PortForwards,
names: &["pf", "portforwards", "forwards"],
},
PaletteCommand {
action: PaletteAction::Skin,
names: &["skin", "skins"],
},
PaletteCommand {
action: PaletteAction::Quit,
names: &["quit", "q", "q!"],
},
];
impl Scrollable {
fn empty() -> Self {
Self {
title: String::new(),
lines: VecDeque::new(),
scroll: 0,
}
}
pub fn scroll_by(&mut self, delta: i32) {
let max = self.lines.len().saturating_sub(1) as i64;
self.scroll = (self.scroll as i64 + delta as i64).clamp(0, max) as usize;
}
}
pub struct LogsView {
pub view: Scrollable,
pub follow: bool,
pub filter: String,
pub wrap: bool,
pub timestamps: bool,
pub stopped: bool,
pub viewport_rows: usize,
pub viewport_h: usize,
pub last_wrap_width: usize,
source: Option<LogSource>,
}
impl Default for LogsView {
fn default() -> Self {
Self {
view: Scrollable::empty(),
follow: true,
filter: String::new(),
wrap: false,
timestamps: false,
stopped: false,
viewport_rows: 0,
viewport_h: 0,
last_wrap_width: 0,
source: None,
}
}
}
enum SortKey {
Num(f64),
Text(String),
}
impl SortKey {
fn cmp_to(&self, other: &Self) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (self, other) {
(SortKey::Num(a), SortKey::Num(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
(SortKey::Text(a), SortKey::Text(b)) => a.cmp(b),
(SortKey::Num(_), SortKey::Text(_)) => Ordering::Less,
(SortKey::Text(_), SortKey::Num(_)) => Ordering::Greater,
}
}
}
#[derive(Default)]
struct RowsCache {
dirty: bool,
keys: Vec<String>,
cells: HashMap<String, CellCacheEntry>,
}
struct CellCacheEntry {
plural: String,
resource_version: Option<String>,
cells: Vec<String>,
status_idx: Option<usize>,
}
pub(crate) struct TableCellCache<'a> {
cache: Ref<'a, RowsCache>,
}
impl TableCellCache<'_> {
pub(crate) fn get(&self, key: &str) -> Option<(&[String], Option<usize>)> {
self.cache
.cells
.get(key)
.map(|entry| (entry.cells.as_slice(), entry.status_idx))
}
}
struct Frame {
kind: Option<Kind>,
kind_plural: String,
namespace: String,
labels: Option<String>,
fields: Option<String>,
filter: String,
scope_label: Option<String>,
selected: Option<usize>,
}
pub struct App {
pub cluster: Cluster,
pub store: Store,
pub kind: Option<Kind>,
pub kind_plural: String,
pub namespace: String,
pub labels: Option<String>,
pub fields: Option<String>,
pub scope_label: Option<String>,
pub generation: u64,
gen_flag: Arc<AtomicU64>,
pub tasks: Vec<JoinHandle<()>>,
pub tx: Sender<Msg>,
stack: Vec<Frame>,
pub mode: Mode,
pub table_state: TableState,
pub marked: HashSet<String>,
pub sort_column: Option<usize>,
pub sort_desc: bool,
pub filter: String,
pub command: String,
pub cmd_suggestions: Vec<Suggestion>,
pub cmd_sel: usize,
pub flash: String,
pub flash_err: bool,
pub detail: Scrollable,
pub logs: LogsView,
pub ns_list: Vec<String>,
pub ns_state: ListState,
pub ns_filter: String,
pub ctx_list: Vec<String>,
pub ctx_state: ListState,
pub ctx_filter: String,
pub user_aliases: HashMap<String, String>,
pub plugins: Vec<crate::config::Plugin>,
rbac_allowed: Option<HashSet<String>>,
last_rbac_ns: Option<String>,
pub container_list: Vec<String>,
pub container_state: ListState,
container_pod: Option<(String, String)>,
pub flux_menu_state: ListState,
pub port_forwards: Vec<PortForward>,
pub pf_state: ListState,
pub skin_list: Vec<String>,
pub skin_state: ListState,
pub skin_colors: HashMap<String, String>,
pub image_values: Vec<String>,
image_target: Option<(String, String, String)>,
pub metrics: HashMap<String, (i64, i64)>,
pub pulse: Pulse,
pub xray_items: Vec<XrayItem>,
pub xray_state: ListState,
pub confirm_label: String,
confirm_action: Option<ConfirmAction>,
pub prompt_label: String,
pub prompt_input: String,
prompt_kind: Option<PromptKind>,
log_gen: u64,
log_flag: Arc<AtomicU64>,
log_tasks: Vec<JoinHandle<()>>,
event_gen: u64,
event_task: Option<JoinHandle<()>>,
pub pending: Option<Suspend>,
return_mode: Mode,
return_selection: Option<String>,
pub should_quit: bool,
matcher: SkimMatcherV2,
rows_cache: RefCell<RowsCache>,
}
impl App {
pub fn new(cluster: Cluster, tx: Sender<Msg>) -> Self {
let namespace = cluster.default_namespace.clone();
Self {
cluster,
store: Store::default(),
kind: None,
kind_plural: String::new(),
namespace,
labels: None,
fields: None,
scope_label: None,
generation: 0,
gen_flag: Arc::new(AtomicU64::new(0)),
tasks: Vec::new(),
tx,
stack: Vec::new(),
mode: Mode::Table,
table_state: TableState::default(),
marked: HashSet::new(),
sort_column: None,
sort_desc: false,
filter: String::new(),
command: String::new(),
cmd_suggestions: Vec::new(),
cmd_sel: 0,
flash: "Welcome to sofka — ':' resource · enter drill · d describe · l logs · ? help"
.into(),
flash_err: false,
detail: Scrollable::empty(),
logs: LogsView::default(),
ns_list: Vec::new(),
ns_state: ListState::default(),
ns_filter: String::new(),
ctx_list: Vec::new(),
ctx_state: ListState::default(),
ctx_filter: String::new(),
user_aliases: HashMap::new(),
plugins: Vec::new(),
rbac_allowed: None,
last_rbac_ns: None,
container_list: Vec::new(),
container_state: ListState::default(),
container_pod: None,
flux_menu_state: ListState::default(),
port_forwards: Vec::new(),
pf_state: ListState::default(),
skin_list: crate::theme::BUILTIN_NAMES
.iter()
.map(|name| (*name).to_string())
.collect(),
skin_state: ListState::default(),
skin_colors: HashMap::new(),
image_values: Vec::new(),
image_target: None,
metrics: HashMap::new(),
pulse: Pulse::default(),
xray_items: Vec::new(),
xray_state: ListState::default(),
confirm_label: String::new(),
confirm_action: None,
prompt_label: String::new(),
prompt_input: String::new(),
prompt_kind: None,
log_gen: 0,
log_flag: Arc::new(AtomicU64::new(0)),
log_tasks: Vec::new(),
event_gen: 0,
event_task: None,
pending: None,
return_mode: Mode::Table,
return_selection: None,
should_quit: false,
matcher: SkimMatcherV2::default(),
rows_cache: RefCell::new(RowsCache {
dirty: true,
keys: Vec::new(),
cells: HashMap::new(),
}),
}
}
pub fn all_namespaces(&self) -> bool {
self.namespace.is_empty()
}
}
mod actions;
mod dashboards;
mod details;
mod helpers;
mod input;
mod lifecycle;
mod logs;
mod navigation;
mod overlays;
mod pickers;
mod rows;
use helpers::*;
#[cfg(test)]
mod tests;