trackWork 0.15.0

A terminal-based time tracking application for managing work sessions
# Cursor Module Architecture

## Overview

`cursor.rs` owns all text cursor logic for input fields across the application. It was extracted from `app.rs` to keep that file focused on application state and business logic.

## Design

The module operates on `&mut InputMode` rather than `&mut App`, which avoids borrow checker issues (no double-borrow of `self`) and keeps the API clean.

### Core function: `get_active_text_and_cursor()`

Maps an `InputMode` variant + its `current_field` index to the active `(&mut String, &mut usize)` pair (text content and cursor position). Returns `None` for non-text fields (integration selector, legacy toggle, color pickers).

This is the single source of truth for "which string is being edited right now" - all other functions in the module call it.

### Field mapping

**Creating / Editing**: field 0=description, 1=start_time, 2=end_time, 3=issue_key

**Settings (CustomCommands)**: field 1=open_command, 2=open_worklog_command, 3=date_format

**Settings (Jira)**: field 1=jira_url, 2=jira_email, 3=api_token, 4=date_format

Fields 0 (integration selector), legacy_time_format, and color fields return `None` - they are not text-editable.

## Public API

| Function | Purpose |
|----------|---------|
| `cursor_left(mode)` | Move cursor one char left |
| `cursor_right(mode)` | Move cursor one char right |
| `insert_char(mode, c)` | Insert char at cursor, advance cursor |
| `delete_char(mode)` | Delete char before cursor (backspace) |
| `insert_str(mode, s)` | Insert string at cursor (variable insertion) |
| `reset_cursor_to_end(mode)` | Move cursor to end of field (called on Tab/field switch) |
| `render_with_cursor(text, pos, active)` | Render text with blinking `\|` at position |

## UTF-8 Safety

All operations use character iteration, never byte indexing:
- Cursor position is a **character index**, not a byte offset
- `insert_char` converts char index to byte offset via `chars().take(n).map(|c| c.len_utf8()).sum()`
- `delete_char` finds the byte range of the character before cursor the same way
- This is critical for Nordic characters (a, o, a) and emoji

## Blinking

`render_with_cursor()` uses `SystemTime` to toggle cursor visibility on a 500ms cycle. The main event loop polls at 100ms, so the UI redraws frequently enough to animate the blink. When the cursor is hidden, a space is rendered in its place to maintain layout width.

## Integration with app.rs

`app.rs` has thin wrapper methods that delegate to this module:

```rust
pub fn cursor_left(&mut self) {
    cursor::cursor_left(&mut self.input_mode);
    self.update_field_label();
}
```

`input_char()` and `delete_char()` in `app.rs` handle suggestion-reset logic for Creating mode, then call `cursor::insert_char()` / `cursor::delete_char()`.

`next_field()` and `previous_field()` call `cursor::reset_cursor_to_end()` after switching fields so the cursor starts at the end of the new field's text.

## Adding cursor support to a new InputMode variant

1. Add `cursor_pos: usize` field to the variant
2. Add a match arm in `get_active_text_and_cursor()` mapping field indices to strings
3. Initialize `cursor_pos` at all creation sites (typically `0` for empty fields, `text.chars().count()` for pre-filled)
4. Add `KeyCode::Left` / `KeyCode::Right` handlers in the input handler calling `app.cursor_left()` / `app.cursor_right()`