ticker-mac 0.0.7

macOS egui GUI for Ticker — a tick-based spreadsheet.
/// Keyboard event → Command translation.
///
/// Called once per frame from `App::update()` via `ctx.input_mut(...)`.
/// Returns `Some(Command)` if a key was consumed, `None` otherwise.
use egui::{Context, Event, Key, Modifiers};

use crate::command::Command;
use crate::mode::Mode;

/// Process keyboard input for the current frame.
///
/// `edit_buffer` is the current content of the active text buffer (edit,
/// formula, command).  The returned command may include `TypeChar` / `Backspace`
/// / `DeleteBuffer` characters that the caller appends to its own buffer.
pub fn process(
    ctx: &Context,
    mode: &Mode,
    edit_buffer: &str,
    is_prop_focused: bool,
) -> Option<Command> {
    ctx.input_mut(|i| translate(i, mode, edit_buffer, is_prop_focused))
}

fn translate(
    i: &mut egui::InputState,
    mode: &Mode,
    edit_buffer: &str,
    is_prop_focused: bool,
) -> Option<Command> {
    // ── Modifier shorthands ───────────────────────────────────────────────────
    let ctrl  = i.modifiers.ctrl || i.modifiers.mac_cmd;
    let shift = i.modifiers.shift;

    // ── Mode-agnostic ─────────────────────────────────────────────────────────
    // Help
    if mode != &Mode::Help {
        if consume(i, ctrl, shift, false, Key::H) && matches!(mode, Mode::Command) {
            // :h — handled via RunCommand path
        } else if i.consume_key(Modifiers::NONE, Key::F1) {
            return Some(Command::OpenHelp);
        }
    }

    match mode {
        // ── Normal ────────────────────────────────────────────────────────────
        Mode::Normal if !is_prop_focused => normal_mode(i, ctrl, shift),

        // ── Property panel focused ────────────────────────────────────────────
        Mode::Normal if is_prop_focused => prop_normal(i, ctrl),

        // ── Editing ───────────────────────────────────────────────────────────
        Mode::Editing => editing_mode(i, ctrl, edit_buffer),

        // ── Formula tree editor ──────────────────────────────────────────────
        Mode::FormulaTree => formula_tree_mode(i),

        // ── Formula name ──────────────────────────────────────────────────────
        Mode::FormulaName => formula_name_mode(i),

        // ── Formula args ─────────────────────────────────────────────────────
        Mode::FormulaArgs => formula_args_mode(i, edit_buffer),

        // ── Command ───────────────────────────────────────────────────────────
        Mode::Command => command_mode(i),

        // ── Command args ─────────────────────────────────────────────────────
        Mode::CommandArgs { .. } => command_mode(i),

        // ── Help ─────────────────────────────────────────────────────────────
        Mode::Help => {
            if i.consume_key(Modifiers::NONE, Key::Escape)
                || i.consume_key(Modifiers::COMMAND, Key::W)
                || i.consume_key(egui::Modifiers::COMMAND | Modifiers::SHIFT, Key::Slash)
            {
                return Some(Command::CloseHelp);
            }
            None
        }

        _ => None,
    }
}

// ─── Normal mode ──────────────────────────────────────────────────────────────

fn normal_mode(i: &mut egui::InputState, ctrl: bool, _shift: bool) -> Option<Command> {
    // Undo / Redo
    if i.consume_key(Modifiers::COMMAND, Key::Z)                              { return Some(Command::Undo); }
    if i.consume_key(Modifiers::COMMAND | Modifiers::SHIFT, Key::Z)           { return Some(Command::Redo); }

    // Navigation
    if consume(i, ctrl, false, false, Key::ArrowUp)    { return Some(Command::MoveFirstTick); }
    if consume(i, ctrl, false, false, Key::ArrowDown)  { return Some(Command::MoveLastTick); }
    if consume(i, ctrl, false, false, Key::ArrowLeft)  { return Some(Command::MoveFirstCol); }
    if consume(i, ctrl, false, false, Key::ArrowRight) { return Some(Command::MoveLastCol); }
    if consume(i, ctrl, false, false, Key::Home)       { return Some(Command::MoveFirstCol); }
    if consume(i, ctrl, false, false, Key::End)        { return Some(Command::MoveLastCol); }

    if i.consume_key(Modifiers::NONE, Key::ArrowUp)    { return Some(Command::MoveUp); }
    if i.consume_key(Modifiers::NONE, Key::ArrowDown)  { return Some(Command::MoveDown); }
    if i.consume_key(Modifiers::NONE, Key::ArrowLeft)  { return Some(Command::MoveLeft); }
    if i.consume_key(Modifiers::NONE, Key::ArrowRight) { return Some(Command::MoveRight); }
    if i.consume_key(Modifiers::NONE, Key::Home)       { return Some(Command::MoveFirstTick); }
    if i.consume_key(Modifiers::NONE, Key::End)        { return Some(Command::MoveLastTick); }
    if i.consume_key(Modifiers::NONE, Key::PageUp)     { return Some(Command::PageUp); }
    if i.consume_key(Modifiers::NONE, Key::PageDown)   { return Some(Command::PageDown); }

    // Sheet switching
    if i.consume_key(Modifiers::NONE, Key::CloseBracket) { return Some(Command::NextSheet); }
    if i.consume_key(Modifiers::NONE, Key::OpenBracket)  { return Some(Command::PrevSheet); }
    for (n, key) in [(1, Key::Num1),(2,Key::Num2),(3,Key::Num3),(4,Key::Num4),(5,Key::Num5),
                     (6,Key::Num6),(7,Key::Num7),(8,Key::Num8),(9,Key::Num9)] {
        if i.consume_key(Modifiers::COMMAND, key) { return Some(Command::GoToSheet(n - 1)); }
    }

    // Delete cell
    if i.consume_key(Modifiers::NONE, Key::Delete)     { return Some(Command::DeleteCell); }
    if i.consume_key(Modifiers::NONE, Key::Backspace)  { return Some(Command::DeleteCell); }

    // F2 — edit keeping value
    if i.consume_key(Modifiers::NONE, Key::F2)         { return Some(Command::StartEditKeepValue); }

    // F4 — repeat last command
    if i.consume_key(Modifiers::NONE, Key::F4)         { return Some(Command::RepeatLastCommand); }

    // Period navigation
    if i.consume_key(Modifiers::NONE, Key::Plus)       { return Some(Command::PeriodIncrease); }
    if i.consume_key(Modifiers::NONE, Key::Minus)      { return Some(Command::PeriodDecrease); }

    // Tab — shift focus to property panel
    if i.consume_key(Modifiers::NONE, Key::Tab)        { return Some(Command::FocusPropertyPanel); }

    // Enter — start editing (clears cell)
    if i.consume_key(Modifiers::NONE, Key::Enter)      { return Some(Command::StartEdit); }

    // Help
    if i.consume_key(Modifiers::COMMAND | Modifiers::SHIFT, Key::Slash) { return Some(Command::OpenHelp); }

    // Undo / Redo (also catch Cmd+Y as alternative redo)
    if i.consume_key(Modifiers::COMMAND, Key::Y)       { return Some(Command::Redo); }

    // Save
    if i.consume_key(Modifiers::COMMAND, Key::S)       { return Some(Command::Save); }

    // Text events: '=' starts formula, ':' opens command, +/- changes period, any other char starts editing
    for event in i.events.iter() {
        if let Event::Text(text) = event {
            if text == "=" {
                i.events.clear();
                return Some(Command::StartFormula);
            }
            if text == ":" {
                i.events.clear();
                return Some(Command::TypeChar(':'));
            }
            if text == "+" {
                i.events.clear();
                return Some(Command::PeriodIncrease);
            }
            if text == "-" {
                i.events.clear();
                return Some(Command::PeriodDecrease);
            }
            if let Some(c) = text.chars().next() {
                if !c.is_control() {
                    i.events.clear();
                    return Some(Command::TypeChar(c));
                }
            }
        }
    }

    None
}

// ─── Property panel normal mode ───────────────────────────────────────────────

fn prop_normal(i: &mut egui::InputState, _ctrl: bool) -> Option<Command> {
    if i.consume_key(Modifiers::NONE, Key::ArrowUp)       { return Some(Command::PropMoveUp); }
    if i.consume_key(Modifiers::NONE, Key::ArrowDown)     { return Some(Command::PropMoveDown); }
    if i.consume_key(Modifiers::NONE, Key::Delete)        { return Some(Command::PropDeleteSelected); }
    if i.consume_key(Modifiers::NONE, Key::Escape)        { return Some(Command::FocusGrid); }
    if i.consume_key(Modifiers::NONE, Key::Tab)           { return Some(Command::FocusGrid); }
    if i.consume_key(Modifiers::NONE, Key::Enter)         { return Some(Command::PropStartEdit); }
    if i.consume_key(Modifiers::NONE, Key::CloseBracket)  { return Some(Command::NextSheet); }
    if i.consume_key(Modifiers::NONE, Key::OpenBracket)   { return Some(Command::PrevSheet); }
    // '=' may arrive as Key::Equals on some platforms
    if i.consume_key(Modifiers::NONE, Key::Equals)        { return Some(Command::PropStartFormula); }

    for event in i.events.iter() {
        if let Event::Text(text) = event {
            if text == "=" {
                i.events.clear();
                return Some(Command::PropStartFormula);
            }
            if let Some(c) = text.chars().next() {
                if !c.is_control() {
                    i.events.clear();
                    return Some(Command::TypeChar(c));
                }
            }
        }
    }
    None
}

// ─── Editing mode ─────────────────────────────────────────────────────────────

fn editing_mode(i: &mut egui::InputState, _ctrl: bool, edit_buffer: &str) -> Option<Command> {
    if i.consume_key(Modifiers::NONE, Key::Escape)    { return Some(Command::Cancel); }
    if i.consume_key(Modifiers::NONE, Key::Backspace)  { return Some(Command::Backspace); }
    if i.consume_key(Modifiers::NONE, Key::Delete)     { return Some(Command::DeleteBuffer); }
    // Arrow keys confirm and move
    if i.consume_key(Modifiers::NONE, Key::ArrowUp)    { return Some(Command::Confirm); }
    if i.consume_key(Modifiers::NONE, Key::ArrowDown)  { return Some(Command::Confirm); }
    // Tab confirms and moves right
    if i.consume_key(Modifiers::NONE, Key::Tab)        { return Some(Command::Confirm); }
    if i.consume_key(Modifiers::NONE, Key::Enter)      { return Some(Command::Confirm); }
    if i.consume_key(Modifiers::NONE, Key::Space)      { return Some(Command::TypeChar(' ')); }
    // '=' as first character starts formula mode (Key event fallback)
    if edit_buffer.is_empty() {
        if i.consume_key(Modifiers::NONE, Key::Equals) { return Some(Command::TypeChar('=')); }
    }

    for event in i.events.iter() {
        if let Event::Text(text) = event {
            if let Some(c) = text.chars().next() {
                if !c.is_control() {
                    i.events.clear();
                    return Some(Command::TypeChar(c));
                }
            }
        }
    }
    None
}

// ─── Formula tree mode ────────────────────────────────────────────────────────

fn formula_tree_mode(i: &mut egui::InputState) -> Option<Command> {
    if i.consume_key(Modifiers::NONE, Key::Escape)    { return Some(Command::Cancel); }
    if i.consume_key(Modifiers::NONE, Key::Backspace) { return Some(Command::Backspace); }
    if i.consume_key(Modifiers::NONE, Key::Delete)    { return Some(Command::DeleteBuffer); }
    if i.consume_key(Modifiers::NONE, Key::Enter)     { return Some(Command::Confirm); }
    if i.consume_key(Modifiers::NONE, Key::ArrowLeft) { return Some(Command::MoveLeft); }
    if i.consume_key(Modifiers::NONE, Key::ArrowRight){ return Some(Command::MoveRight); }
    if i.consume_key(Modifiers::NONE, Key::ArrowUp)   { return Some(Command::MoveUp); }
    if i.consume_key(Modifiers::NONE, Key::ArrowDown) { return Some(Command::MoveDown); }
    for event in i.events.iter() {
        if let Event::Text(text) = event {
            if let Some(c) = text.chars().next() {
                if !c.is_control() {
                    i.events.clear();
                    return Some(Command::TypeChar(c));
                }
            }
        }
    }
    None
}

// ─── Formula name mode ────────────────────────────────────────────────────────

fn formula_name_mode(i: &mut egui::InputState) -> Option<Command> {
    if i.consume_key(Modifiers::NONE, Key::Escape)   { return Some(Command::Cancel); }
    if i.consume_key(Modifiers::NONE, Key::Backspace) { return Some(Command::Backspace); }
    if i.consume_key(Modifiers::NONE, Key::Enter)     { return Some(Command::Confirm); }

    for event in i.events.iter() {
        if let Event::Text(text) = event {
            if let Some(c) = text.chars().next() {
                if !c.is_control() {
                    i.events.clear();
                    return Some(Command::TypeChar(c));
                }
            }
        }
    }
    None
}

// ─── Formula args mode ────────────────────────────────────────────────────────

fn formula_args_mode(i: &mut egui::InputState, edit_buffer: &str) -> Option<Command> {
    if i.consume_key(Modifiers::NONE, Key::Escape)   { return Some(Command::Cancel); }
    if i.consume_key(Modifiers::NONE, Key::Backspace) { return Some(Command::Backspace); }
    if i.consume_key(Modifiers::NONE, Key::Delete)    { return Some(Command::DeleteBuffer); }
    if i.consume_key(Modifiers::NONE, Key::Enter) {
        return Some(if edit_buffer.is_empty() {
            Command::FinishVariadic
        } else {
            Command::Confirm
        });
    }

    // '=' inside an arg slot → start nested formula
    for event in i.events.iter() {
        if let Event::Text(text) = event {
            if text == "=" && edit_buffer.is_empty() {
                i.events.clear();
                return Some(Command::StartNestedFormula);
            }
            if let Some(c) = text.chars().next() {
                if !c.is_control() {
                    i.events.clear();
                    return Some(Command::TypeChar(c));
                }
            }
        }
    }
    None
}

// ─── Command mode ─────────────────────────────────────────────────────────────

fn command_mode(i: &mut egui::InputState) -> Option<Command> {
    if i.consume_key(Modifiers::NONE, Key::Escape)   { return Some(Command::Cancel); }
    if i.consume_key(Modifiers::NONE, Key::Backspace) { return Some(Command::Backspace); }
    if i.consume_key(Modifiers::NONE, Key::Enter)     { return Some(Command::Confirm); }
    // Space may arrive as a Key event rather than Text on some platforms
    if i.consume_key(Modifiers::NONE, Key::Space)     { return Some(Command::TypeChar(' ')); }

    for event in i.events.iter() {
        if let Event::Text(text) = event {
            if let Some(c) = text.chars().next() {
                if !c.is_control() {
                    i.events.clear();
                    return Some(Command::TypeChar(c));
                }
            }
        }
    }
    None
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

/// Consume `key` with optional Ctrl/Cmd modifier.
fn consume(i: &mut egui::InputState, ctrl: bool, _shift: bool, _alt: bool, key: Key) -> bool {
    if ctrl {
        i.consume_key(Modifiers::COMMAND, key) || i.consume_key(Modifiers::CTRL, key)
    } else {
        false
    }
}