a7n 0.18.0

Command-line and terminal interface for the Apeirogon software KVM
// SPDX-License-Identifier: GPL-3.0-or-later

use a7n_ipc::{Edge, LayoutInfo, LayoutPlacement, Placement, validate};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

const MOVE_STEP: i32 = 100;
const FINE_MOVE_STEP: i32 = 10;
const SEGMENT_COUNT: u8 = 12;
const EDGES: [Edge; 4] = [Edge::Left, Edge::Top, Edge::Right, Edge::Bottom];

#[derive(Debug)]
pub(super) struct Editor {
    pub(super) placements: Vec<Placement>,
    pub(super) names: Vec<String>,
    pub(super) selected: usize,
    pub(super) lost_edit: bool,
    segment: u8,
}

#[derive(Debug)]
pub(super) enum Action {
    Continue,
    Quit,
    Save(Vec<LayoutPlacement>),
}

impl Editor {
    pub(super) fn new(info: LayoutInfo) -> Result<Self> {
        let draft = a7n_ipc::layout::LayoutDraft::try_from(info)?;
        Ok(Self {
            placements: draft.placements,
            names: draft.names,
            selected: 0,
            segment: 0,
            lost_edit: draft.lost_edit,
        })
    }

    pub(super) fn handle(&mut self, key: KeyEvent) -> Action {
        if key.kind == KeyEventKind::Release {
            return Action::Continue;
        }
        match key.code {
            KeyCode::Esc | KeyCode::Char('q') => return Action::Quit,
            KeyCode::Enter | KeyCode::Char('s') if validate(&self.placements).is_ok() => {
                return Action::Save(
                    self.placements
                        .iter()
                        .map(|p| LayoutPlacement {
                            machine: p.id,
                            x: p.x,
                            y: p.y,
                            width: p.width,
                            height: p.height,
                            blocked: p.blocked,
                        })
                        .collect(),
                );
            }
            _ => {}
        }
        if self.placements.is_empty() {
            return Action::Continue;
        }
        let step = if key.modifiers.contains(KeyModifiers::SHIFT) {
            FINE_MOVE_STEP
        } else {
            MOVE_STEP
        };
        let (edge, third) = self.focused_segment();
        let placement = &mut self.placements[self.selected];
        match key.code {
            KeyCode::BackTab => self.select_previous(),
            KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => self.select_previous(),
            KeyCode::Tab => {
                self.selected = (self.selected + 1) % self.placements.len();
                self.segment = 0;
            }
            KeyCode::Char('e' | 'E') => {
                let backwards =
                    key.code == KeyCode::Char('E') || key.modifiers.contains(KeyModifiers::SHIFT);
                self.segment =
                    (self.segment + if backwards { SEGMENT_COUNT - 1 } else { 1 }) % SEGMENT_COUNT;
            }
            KeyCode::Char('x') => {
                placement.toggle_blocked(edge, third);
            }
            KeyCode::Left => placement.x = placement.x.saturating_sub(step),
            KeyCode::Right => placement.x = placement.x.saturating_add(step),
            KeyCode::Up => placement.y = placement.y.saturating_sub(step),
            KeyCode::Down => placement.y = placement.y.saturating_add(step),
            _ => {}
        }
        Action::Continue
    }

    pub(super) fn focused_segment(&self) -> (Edge, u8) {
        (EDGES[usize::from(self.segment / 3)], self.segment % 3)
    }

    fn select_previous(&mut self) {
        self.segment = 0;
        self.selected = (self.selected + self.placements.len() - 1) % self.placements.len();
    }
}

#[cfg(test)]
mod tests;