pub(crate) mod devices;
pub(crate) mod picker;
pub(crate) mod whois;
use std::time::{Duration, Instant};
use self::devices::{DeviceTable, Duplicates, Movement};
use self::picker::{Picker, PickerKey};
use self::whois::{FormKey, FormOutcome, WhoIsForm};
use super::log_layer::LogRing;
use super::message::{AddressStyle, Command, OpId, OpOutcome, WhoIsSpec, WorkerEvent};
pub(crate) const CTRL_C_WINDOW: Duration = Duration::from_secs(2);
const FLASH_FOR: Duration = Duration::from_secs(4);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Link {
Picking,
Connecting,
Up {
local: String,
},
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Overlay {
None,
Help,
WhoIs(WhoIsForm),
Picker(Picker),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OpState {
Sending,
Listening {
until: Instant,
},
Done,
Cancelled,
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Operation {
pub(crate) id: OpId,
pub(crate) spec: WhoIsSpec,
pub(crate) replies: usize,
pub(crate) state: OpState,
}
impl Operation {
pub(crate) fn running(&self) -> bool {
matches!(self.state, OpState::Sending | OpState::Listening { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Flash {
pub(crate) text: String,
pub(crate) until: Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FilterKey {
Char(char),
Backspace,
Commit,
Clear,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Action {
Tick {
now: Instant,
dropped: u64,
log_generation: u64,
},
Resize,
Quit,
CtrlC,
Escape,
ToggleHelp,
ToggleLog,
OpenWhoIs,
Move(Movement),
CycleSort,
ReverseSort,
StartFilter,
Filter(FilterKey),
Form(FormKey),
Picker(PickerKey),
Worker(WorkerEvent),
}
pub(crate) struct AppConfig {
pub(crate) transport: &'static str,
pub(crate) style: AddressStyle,
pub(crate) color: bool,
pub(crate) picker: Option<Picker>,
pub(crate) now: Instant,
}
pub(crate) struct App {
pub(crate) transport: &'static str,
pub(crate) style: AddressStyle,
pub(crate) color: bool,
pub(crate) link: Link,
pub(crate) devices: DeviceTable,
pub(crate) duplicates: Duplicates,
pub(crate) overlay: Overlay,
pub(crate) filter_editing: bool,
pub(crate) show_log: bool,
pub(crate) op: Option<Operation>,
last_spec: Option<WhoIsSpec>,
next_op: OpId,
pub(crate) flash: Option<Flash>,
ctrl_c_at: Option<Instant>,
epoch: Instant,
pub(crate) now: Instant,
pub(crate) dropped: u64,
pub(crate) log: LogRing,
log_generation: u64,
pub(crate) quit: bool,
pub(crate) exit_error: Option<String>,
pub(crate) exit_code: Option<i32>,
pub(crate) dirty: bool,
}
impl App {
pub(crate) fn new(config: AppConfig, log: LogRing) -> Self {
let (link, overlay) = match config.picker {
Some(picker) => (Link::Picking, Overlay::Picker(picker)),
None => (Link::Connecting, Overlay::None),
};
let log_generation = log.generation();
Self {
transport: config.transport,
style: config.style,
color: config.color,
link,
devices: DeviceTable::default(),
duplicates: Duplicates::default(),
overlay,
filter_editing: false,
show_log: false,
op: None,
last_spec: None,
next_op: 0,
flash: None,
ctrl_c_at: None,
epoch: config.now,
now: config.now,
dropped: 0,
log,
log_generation,
quit: false,
exit_error: None,
exit_code: None,
dirty: true,
}
}
pub(crate) fn settle(&mut self) {
self.devices.settle();
}
pub(crate) fn fail(&mut self, error: impl Into<String>, code: i32) {
if self.exit_error.is_none() {
self.exit_error = Some(error.into());
self.exit_code = Some(code);
}
self.quit = true;
}
pub(crate) fn op_running(&self) -> bool {
self.op.as_ref().is_some_and(Operation::running)
}
fn flash(&mut self, text: impl Into<String>, lasts: Duration) {
self.flash = Some(Flash {
text: text.into(),
until: self.now + lasts,
});
}
fn running_op_mut(&mut self, id: OpId) -> Option<&mut Operation> {
self.op.as_mut().filter(|op| op.id == id && op.running())
}
fn tick(&mut self, now: Instant, dropped: u64, log_generation: u64) {
let second = |t: Instant| t.saturating_duration_since(self.epoch).as_secs();
if second(now) != second(self.now) && (!self.devices.is_empty() || self.op_running()) {
self.dirty = true;
}
self.now = now;
if dropped != self.dropped {
self.dropped = dropped;
self.dirty = true;
}
if log_generation != self.log_generation {
self.log_generation = log_generation;
self.dirty |= self.show_log;
}
if self.flash.as_ref().is_some_and(|f| now >= f.until) {
self.flash = None;
self.dirty = true;
}
if self
.ctrl_c_at
.is_some_and(|t| now.saturating_duration_since(t) >= CTRL_C_WINDOW)
{
self.ctrl_c_at = None;
}
}
fn ctrl_c(&mut self, out: &mut Vec<Command>) {
let now = self.now;
if self
.ctrl_c_at
.is_some_and(|t| now.saturating_duration_since(t) < CTRL_C_WINDOW)
{
self.quit = true;
return;
}
if matches!(self.link, Link::Picking | Link::Connecting) {
self.quit = true;
return;
}
self.ctrl_c_at = Some(now);
let cancelled = if self.overlay != Overlay::None {
self.overlay = Overlay::None;
Some("Closed")
} else if self.filter_editing {
self.filter_editing = false;
Some("Closed the filter")
} else if let Some(op) = self.op.as_mut().filter(|op| op.running()) {
op.state = OpState::Cancelled;
out.push(Command::Cancel { op: op.id });
Some("Cancelled the Who-Is")
} else {
None
};
let text = match cancelled {
Some(what) => format!("{what}. Press Ctrl-C again to quit."),
None => "Press Ctrl-C again, or q, to quit.".to_string(),
};
self.flash(text, CTRL_C_WINDOW);
}
fn escape(&mut self) {
match self.overlay {
Overlay::Picker(_) => self.quit = true,
Overlay::Help | Overlay::WhoIs(_) => self.overlay = Overlay::None,
Overlay::None if !self.devices.filter().is_empty() => {
self.devices.clear_filter();
self.devices.settle();
}
Overlay::None => self.flash = None,
}
}
fn open_who_is(&mut self) {
if !matches!(self.link, Link::Up { .. }) {
self.flash("Not connected yet.", FLASH_FOR);
return;
}
let form = self
.last_spec
.as_ref()
.map(WhoIsForm::from_spec)
.unwrap_or_default();
self.overlay = Overlay::WhoIs(form);
}
fn filter_key(&mut self, key: FilterKey) {
match key {
FilterKey::Char(ch) => self.devices.push_filter(ch),
FilterKey::Backspace => self.devices.pop_filter(),
FilterKey::Commit => self.filter_editing = false,
FilterKey::Clear => {
self.devices.clear_filter();
self.filter_editing = false;
}
}
self.devices.settle();
}
fn form_key(&mut self, key: FormKey, out: &mut Vec<Command>) {
let known = self.devices.len();
let style = self.style;
let Overlay::WhoIs(form) = &mut self.overlay else {
return;
};
if let FormOutcome::Send(spec) = form.key(key, style, known) {
self.overlay = Overlay::None;
self.start_who_is(spec, out);
}
}
fn start_who_is(&mut self, spec: WhoIsSpec, out: &mut Vec<Command>) {
if let Some(op) = self.op.as_mut().filter(|op| op.running()) {
op.state = OpState::Cancelled;
}
self.next_op += 1;
self.last_spec = Some(spec.clone());
self.op = Some(Operation {
id: self.next_op,
spec: spec.clone(),
replies: 0,
state: OpState::Sending,
});
out.push(Command::WhoIs {
op: self.next_op,
spec,
});
}
fn picker_key(&mut self, key: PickerKey, out: &mut Vec<Command>) {
let Overlay::Picker(picker) = &mut self.overlay else {
return;
};
if let Some(iface) = picker.key(key) {
self.overlay = Overlay::None;
self.link = Link::Connecting;
self.flash(
format!(
"Using {} ({}, broadcast {}).",
iface.name, iface.ip, iface.broadcast
),
FLASH_FOR,
);
out.push(Command::Connect {
interface: iface.ip,
broadcast: iface.broadcast,
});
}
}
fn worker_event(&mut self, event: WorkerEvent) {
match event {
WorkerEvent::Connected { local } => self.link = Link::Up { local },
WorkerEvent::ConnectFailed { error } => {
self.link = Link::Failed(error.clone());
self.fail(error, 1);
}
WorkerEvent::Discovered(row) | WorkerEvent::Updated(row) => {
if let Some(op) = self.op.as_mut().filter(|op| op.running()) {
op.replies += usize::from(op.spec.wants(row.instance));
}
self.devices.upsert(row);
}
WorkerEvent::Lost(row) => self.devices.remove(row.instance),
WorkerEvent::Collision { retained, incoming } => {
self.duplicates.record(&retained, &incoming);
}
WorkerEvent::Snapshot(rows) => self.devices.replace_all(rows),
WorkerEvent::WhoIsSent { op } => {
let now = self.now;
if let Some(op) = self.running_op_mut(op) {
op.state = OpState::Listening {
until: now + op.spec.listen,
};
}
}
WorkerEvent::OpFinished { op, outcome } => {
if let Some(op) = self.running_op_mut(op) {
op.state = match outcome {
OpOutcome::Completed => OpState::Done,
OpOutcome::Cancelled => OpState::Cancelled,
OpOutcome::Failed(error) => OpState::Failed(error),
};
}
}
}
}
}
pub(crate) fn update(app: &mut App, action: Action) -> Vec<Command> {
let mut out = Vec::new();
if !matches!(action, Action::Tick { .. }) {
app.dirty = true;
}
match action {
Action::Tick {
now,
dropped,
log_generation,
} => app.tick(now, dropped, log_generation),
Action::Resize => {}
Action::Quit => app.quit = true,
Action::CtrlC => app.ctrl_c(&mut out),
Action::Escape => app.escape(),
Action::ToggleHelp => {
app.overlay = match std::mem::replace(&mut app.overlay, Overlay::None) {
Overlay::None => Overlay::Help,
Overlay::Help => Overlay::None,
other => other,
}
}
Action::ToggleLog => app.show_log = !app.show_log,
Action::OpenWhoIs => app.open_who_is(),
Action::Move(movement) => app.devices.move_selection(movement),
Action::CycleSort => {
app.devices.cycle_sort();
app.devices.settle();
}
Action::ReverseSort => {
app.devices.reverse();
app.devices.settle();
}
Action::StartFilter => app.filter_editing = true,
Action::Filter(key) => app.filter_key(key),
Action::Form(key) => app.form_key(key, &mut out),
Action::Picker(key) => app.picker_key(key, &mut out),
Action::Worker(event) => app.worker_event(event),
}
out
}