# 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
| `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()`