trackWork 0.15.0

A terminal-based time tracking application for managing work sessions
use crate::app::InputMode;
use crate::integrations::IntegrationKind;

/// Get the active text field and cursor position for the current input mode.
/// Returns None if the current field is not a text-editable field.
pub fn get_active_text_and_cursor(mode: &mut InputMode) -> Option<(&mut String, &mut usize)> {
    match mode {
        InputMode::Creating {
            description, start_time, end_time, issue_key,
            current_field, cursor_pos, ..
        } => {
            let text = match *current_field {
                0 => description,
                1 => start_time,
                2 => end_time,
                3 => issue_key,
                _ => return None,
            };
            Some((text, cursor_pos))
        }
        InputMode::Editing {
            description, start_time, end_time, issue_key,
            current_field, cursor_pos, ..
        } => {
            let text = match *current_field {
                0 => description,
                1 => start_time,
                2 => end_time,
                3 => issue_key,
                _ => return None,
            };
            Some((text, cursor_pos))
        }
        InputMode::EditingDay {
            start_time, end_time, current_field, cursor_pos, ..
        } => {
            let text = match *current_field {
                0 => start_time,
                1 => end_time,
                _ => return None,
            };
            Some((text, cursor_pos))
        }
        InputMode::Triggers {
            urls, bodies, current_field, cursor_pos, ..
        } => {
            let event = *current_field / 3;
            let text = match *current_field % 3 {
                1 => &mut urls[event],
                2 => &mut bodies[event],
                _ => return None, // enabled toggle: not a text field
            };
            Some((text, cursor_pos))
        }
        InputMode::Settings {
            integration, open_command, open_worklog_command,
            jira_url_setting, jira_email, jira_api_token, date_format,
            current_field, cursor_pos, ..
        } => {
            let text = match integration {
                IntegrationKind::CustomCommands => match *current_field {
                    1 => open_command,
                    2 => open_worklog_command,
                    3 => date_format,
                    _ => return None,
                },
                IntegrationKind::Jira => match *current_field {
                    1 => jira_url_setting,
                    2 => jira_email,
                    3 => jira_api_token,
                    4 => date_format,
                    _ => return None,
                },
            };
            Some((text, cursor_pos))
        }
        InputMode::PassphrasePrompt {
            passphrase, cursor_pos, ..
        } => {
            Some((passphrase, cursor_pos))
        }
        InputMode::PassphraseChange {
            old_passphrase, new_passphrase, confirm_passphrase,
            current_field, cursor_pos, is_initial_setup, ..
        } => {
            let text = if *is_initial_setup {
                match *current_field {
                    0 => new_passphrase,
                    1 => confirm_passphrase,
                    _ => return None,
                }
            } else {
                match *current_field {
                    0 => old_passphrase,
                    1 => new_passphrase,
                    2 => confirm_passphrase,
                    _ => return None,
                }
            };
            Some((text, cursor_pos))
        }
        _ => None,
    }
}

/// Move the cursor one character to the left.
pub fn cursor_left(mode: &mut InputMode) {
    if let Some((_, cursor_pos)) = get_active_text_and_cursor(mode) {
        if *cursor_pos > 0 {
            *cursor_pos -= 1;
        }
    }
}

/// Move the cursor one character to the right.
pub fn cursor_right(mode: &mut InputMode) {
    if let Some((text, cursor_pos)) = get_active_text_and_cursor(mode) {
        if *cursor_pos < text.chars().count() {
            *cursor_pos += 1;
        }
    }
}

/// Insert a character at the cursor position and advance the cursor.
pub fn insert_char(mode: &mut InputMode, c: char) {
    if let Some((text, cursor_pos)) = get_active_text_and_cursor(mode) {
        let byte_pos: usize = text.chars().take(*cursor_pos).map(|ch| ch.len_utf8()).sum();
        text.insert(byte_pos, c);
        *cursor_pos += 1;
    }
}

/// Delete the character before the cursor (backspace).
pub fn delete_char(mode: &mut InputMode) {
    if let Some((text, cursor_pos)) = get_active_text_and_cursor(mode) {
        if *cursor_pos > 0 {
            let char_idx = *cursor_pos - 1;
            let byte_pos: usize = text.chars().take(char_idx).map(|ch| ch.len_utf8()).sum();
            let removed_len = text.chars().nth(char_idx).map(|ch| ch.len_utf8()).unwrap_or(0);
            text.replace_range(byte_pos..byte_pos + removed_len, "");
            *cursor_pos -= 1;
        }
    }
}

/// Reset cursor to end of the active text field (used when switching fields).
pub fn reset_cursor_to_end(mode: &mut InputMode) {
    if let Some((text, cursor_pos)) = get_active_text_and_cursor(mode) {
        *cursor_pos = text.chars().count();
    }
}

/// Insert a string at the cursor position (e.g. for variable insertion).
pub fn insert_str(mode: &mut InputMode, s: &str) {
    if let Some((text, cursor_pos)) = get_active_text_and_cursor(mode) {
        let byte_pos: usize = text.chars().take(*cursor_pos).map(|ch| ch.len_utf8()).sum();
        text.insert_str(byte_pos, s);
        *cursor_pos += s.chars().count();
    }
}

/// Render text with a blinking cursor `|` at the given position.
/// The cursor blinks on a ~500ms cycle based on system time.
/// Returns the text with `|` inserted at cursor_pos when visible.
pub fn render_with_cursor(text: &str, cursor_pos: usize, is_active: bool) -> String {
    if !is_active {
        return text.to_string();
    }

    // Blink: show cursor for 500ms, hide for 500ms
    let millis = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis();
    let cursor_visible = (millis / 500) % 2 == 0;

    let before: String = text.chars().take(cursor_pos).collect();
    let after: String = text.chars().skip(cursor_pos).collect();

    if cursor_visible {
        format!("{}|{}", before, after)
    } else {
        format!("{} {}", before, after)
    }
}