canvas compiled for the first time

This commit is contained in:
Priec
2025-07-29 09:35:17 +02:00
parent 7129ec97fd
commit 15922ed953
13 changed files with 822 additions and 8 deletions

18
Cargo.lock generated
View File

@@ -470,6 +470,16 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "canvas"
version = "0.4.1"
dependencies = [
"anyhow",
"common",
"crossterm",
"ratatui",
]
[[package]]
name = "cassowary"
version = "0.3.0"
@@ -541,7 +551,7 @@ dependencies = [
[[package]]
name = "client"
version = "0.3.13"
version = "0.4.1"
dependencies = [
"anyhow",
"async-trait",
@@ -591,7 +601,7 @@ dependencies = [
[[package]]
name = "common"
version = "0.3.13"
version = "0.4.1"
dependencies = [
"prost",
"prost-types",
@@ -2877,7 +2887,7 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "search"
version = "0.3.13"
version = "0.4.1"
dependencies = [
"anyhow",
"common",
@@ -2976,7 +2986,7 @@ dependencies = [
[[package]]
name = "server"
version = "0.3.13"
version = "0.4.1"
dependencies = [
"anyhow",
"bcrypt",

View File

@@ -46,4 +46,8 @@ rust_decimal_macros = "1.37.1"
thiserror = "2.0.12"
regex = "1.11.1"
# Canvas crate
ratatui = { version = "0.29.0", features = ["crossterm"] }
crossterm = "0.28.1"
common = { path = "./common" }

160
canvas/CANVAS_MIGRATION.md Normal file
View File

@@ -0,0 +1,160 @@
# Canvas Crate Migration Documentation
## Files Moved from Client to Canvas
### Core Canvas Files
| **Canvas Location** | **Original Client Location** | **Purpose** |
|-------------------|---------------------------|-----------|
| `canvas/src/state.rs` | `client/src/state/pages/canvas_state.rs` | Core CanvasState trait |
| `canvas/src/actions/edit.rs` | `client/src/functions/modes/edit/form_e.rs` | Generic edit actions |
| `canvas/src/renderer.rs` | `client/src/components/handlers/canvas.rs` | Canvas rendering logic |
| `canvas/src/modes/highlight.rs` | `client/src/state/app/highlight.rs` | Highlight state types |
| `canvas/src/modes/manager.rs` | `client/src/modes/handlers/mode_manager.rs` | Mode management |
## Import Replacements Needed in Client
### 1. CanvasState Trait Usage
**Replace these imports:**
```rust
// OLD
use crate::state::pages::canvas_state::CanvasState;
// NEW
use canvas::CanvasState;
```
**Files that need updating:**
- `src/modes/canvas/edit.rs` - Line 9
- `src/modes/canvas/read_only.rs` - Line 5
- `src/ui/handlers/render.rs` - Line 17
- `src/state/pages/auth.rs` - All CanvasState impls
- `src/state/pages/form.rs` - CanvasState impl
- `src/state/pages/add_table.rs` - CanvasState impl
- `src/state/pages/add_logic.rs` - CanvasState impl
### 2. Edit Actions Usage
**Replace these imports:**
```rust
// OLD
use crate::functions::modes::edit::form_e::{execute_edit_action, execute_common_action};
// NEW
use canvas::{execute_edit_action, execute_common_action};
```
**Files that need updating:**
- `src/modes/canvas/edit.rs` - Lines 3-5
- `src/functions/modes/edit/auth_e.rs`
- `src/functions/modes/edit/add_table_e.rs`
- `src/functions/modes/edit/add_logic_e.rs`
### 3. Canvas Rendering Usage
**Replace these imports:**
```rust
// OLD
use crate::components::handlers::canvas::render_canvas;
// NEW
use canvas::render_canvas;
```
**Files that need updating:**
- Any component that renders forms (login, register, add_table, add_logic, forms)
### 4. Mode System Usage
**Replace these imports:**
```rust
// OLD
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
use crate::state::app::highlight::HighlightState;
// NEW
use canvas::{AppMode, ModeManager, HighlightState};
```
**Files that need updating:**
- `src/modes/handlers/event.rs` - Line 14
- `src/ui/handlers/ui.rs` - Mode derivation calls
- All mode handling files
## Theme Integration Required
The canvas crate expects a `CanvasTheme` trait. You need to implement this for your existing theme:
```rust
// In client/src/config/colors/themes.rs
use canvas::CanvasTheme;
use ratatui::style::Color;
impl CanvasTheme for Theme {
fn primary_fg(&self) -> Color { self.fg }
fn primary_bg(&self) -> Color { self.bg }
fn accent(&self) -> Color { self.accent }
fn warning(&self) -> Color { self.warning }
fn secondary(&self) -> Color { self.secondary }
fn highlight(&self) -> Color { self.highlight }
fn highlight_bg(&self) -> Color { self.highlight_bg }
}
```
## Systematic Replacement Strategy
### Phase 1: Fix Compilation (Do This First)
1. Update `client/Cargo.toml` to depend on canvas
2. Add theme implementation
3. Replace imports in core files
### Phase 2: Replace Feature-Specific Usage
1. Update auth components
2. Update form components
3. Update admin components
4. Update mode handlers
### Phase 3: Remove Old Files (After Everything Works)
1. Delete `src/state/pages/canvas_state.rs`
2. Delete `src/functions/modes/edit/form_e.rs`
3. Delete `src/components/handlers/canvas.rs`
4. Delete `src/state/app/highlight.rs`
5. Delete `src/modes/handlers/mode_manager.rs`
## Files Safe to Delete After Migration
**These can be removed once imports are updated:**
- `client/src/state/pages/canvas_state.rs`
- `client/src/functions/modes/edit/form_e.rs`
- `client/src/components/handlers/canvas.rs`
- `client/src/state/app/highlight.rs`
- `client/src/modes/handlers/mode_manager.rs`
## Quick Start Commands
```bash
# 1. Add canvas dependency to client
cd client
echo 'canvas = { path = "../canvas" }' >> Cargo.toml
# 2. Test compilation
cargo check
# 3. Fix imports one file at a time
# Start with: src/config/colors/themes.rs (add CanvasTheme impl)
# Then: src/modes/canvas/edit.rs (replace form_e imports)
# Then: src/modes/canvas/read_only.rs (replace canvas_state import)
# 4. After all imports fixed, delete old files
rm src/state/pages/canvas_state.rs
rm src/functions/modes/edit/form_e.rs
rm src/components/handlers/canvas.rs
rm src/state/app/highlight.rs
rm src/modes/handlers/mode_manager.rs
```
## Expected Compilation Errors
You'll get errors like:
- `cannot find type 'CanvasState' in this scope`
- `cannot find function 'execute_edit_action' in this scope`
- `cannot find type 'AppMode' in this scope`
Fix these by replacing the imports as documented above.

View File

@@ -10,3 +10,7 @@ repository.workspace = true
categories.workspace = true
[dependencies]
common = { path = "../common" }
ratatui = { workspace = true }
crossterm = { workspace = true }
anyhow = { workspace = true }

416
canvas/src/actions/edit.rs Normal file
View File

@@ -0,0 +1,416 @@
// canvas/src/actions/edit.rs
use crate::state::CanvasState;
use crossterm::event::{KeyCode, KeyEvent};
use anyhow::Result;
/// Execute a generic edit action on any CanvasState implementation.
/// This is the core function that makes the mode system work across all features.
pub async fn execute_edit_action<S: CanvasState>(
action: &str,
key: KeyEvent,
state: &mut S,
ideal_cursor_column: &mut usize,
) -> Result<String> {
// 1. Try feature-specific handler first (for autocomplete, field-specific logic, etc.)
let context = crate::state::ActionContext {
key_code: Some(key.code),
ideal_cursor_column: *ideal_cursor_column,
current_input: state.get_current_input().to_string(),
current_field: state.current_field(),
};
if let Some(result) = state.handle_feature_action(action, &context) {
return Ok(result);
}
// 2. Handle suggestion-related actions generically
if handle_suggestion_actions(action, state)? {
return Ok("".to_string()); // Suggestion action handled
}
// 3. Fall back to generic canvas actions (handles 95% of all actions)
handle_generic_action(action, key, state, ideal_cursor_column).await
}
/// Handle suggestion/autocomplete actions generically
fn handle_suggestion_actions<S: CanvasState>(action: &str, state: &mut S) -> Result<bool> {
match action {
"suggestion_down" => {
if let Some(suggestions) = state.get_suggestions() {
if !suggestions.is_empty() {
let current = state.get_selected_suggestion_index().unwrap_or(0);
let next = (current + 1) % suggestions.len();
state.set_selected_suggestion_index(Some(next));
return Ok(true);
}
}
Ok(false)
}
"suggestion_up" => {
if let Some(suggestions) = state.get_suggestions() {
if !suggestions.is_empty() {
let current = state.get_selected_suggestion_index().unwrap_or(0);
let prev = if current == 0 { suggestions.len() - 1 } else { current - 1 };
state.set_selected_suggestion_index(Some(prev));
return Ok(true);
}
}
Ok(false)
}
"select_suggestion" => {
// Let feature handle this via handle_feature_action since it's feature-specific
Ok(false)
}
"exit_suggestions" => {
state.deactivate_suggestions();
Ok(true)
}
_ => Ok(false)
}
}
/// Handle generic canvas actions (movement, editing, etc.)
async fn handle_generic_action<S: CanvasState>(
action: &str,
key: KeyEvent,
state: &mut S,
ideal_cursor_column: &mut usize,
) -> Result<String> {
match action {
"insert_char" => {
if let KeyCode::Char(c) = key.code {
let cursor_pos = state.current_cursor_pos();
let field_value = state.get_current_input_mut();
let mut chars: Vec<char> = field_value.chars().collect();
if cursor_pos <= chars.len() {
chars.insert(cursor_pos, c);
*field_value = chars.into_iter().collect();
state.set_current_cursor_pos(cursor_pos + 1);
state.set_has_unsaved_changes(true);
*ideal_cursor_column = state.current_cursor_pos();
}
} else {
return Ok("Error: insert_char called without a char key.".to_string());
}
Ok("".to_string())
}
"delete_char_backward" => {
if state.current_cursor_pos() > 0 {
let cursor_pos = state.current_cursor_pos();
let field_value = state.get_current_input_mut();
let mut chars: Vec<char> = field_value.chars().collect();
if cursor_pos <= chars.len() {
chars.remove(cursor_pos - 1);
*field_value = chars.into_iter().collect();
let new_pos = cursor_pos - 1;
state.set_current_cursor_pos(new_pos);
state.set_has_unsaved_changes(true);
*ideal_cursor_column = new_pos;
}
}
Ok("".to_string())
}
"delete_char_forward" => {
let cursor_pos = state.current_cursor_pos();
let field_value = state.get_current_input_mut();
let mut chars: Vec<char> = field_value.chars().collect();
if cursor_pos < chars.len() {
chars.remove(cursor_pos);
*field_value = chars.into_iter().collect();
state.set_has_unsaved_changes(true);
*ideal_cursor_column = cursor_pos;
}
Ok("".to_string())
}
"next_field" => {
let num_fields = state.fields().len();
if num_fields > 0 {
let current_field = state.current_field();
let new_field = (current_field + 1) % num_fields;
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
}
Ok("".to_string())
}
"prev_field" => {
let num_fields = state.fields().len();
if num_fields > 0 {
let current_field = state.current_field();
let new_field = if current_field == 0 {
num_fields - 1
} else {
current_field - 1
};
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
}
Ok("".to_string())
}
"move_left" => {
let new_pos = state.current_cursor_pos().saturating_sub(1);
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
Ok("".to_string())
}
"move_right" => {
let current_input = state.get_current_input();
let current_pos = state.current_cursor_pos();
if current_pos < current_input.len() {
let new_pos = current_pos + 1;
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
}
Ok("".to_string())
}
"move_up" => {
let num_fields = state.fields().len();
if num_fields > 0 {
let current_field = state.current_field();
let new_field = current_field.saturating_sub(1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
}
Ok("".to_string())
}
"move_down" => {
let num_fields = state.fields().len();
if num_fields > 0 {
let new_field = (state.current_field() + 1).min(num_fields - 1);
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
}
Ok("".to_string())
}
"move_line_start" => {
state.set_current_cursor_pos(0);
*ideal_cursor_column = 0;
Ok("".to_string())
}
"move_line_end" => {
let current_input = state.get_current_input();
let new_pos = current_input.len();
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
Ok("".to_string())
}
"move_first_line" => {
let num_fields = state.fields().len();
if num_fields > 0 {
state.set_current_field(0);
let current_input = state.get_current_input();
let max_pos = current_input.len();
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
}
Ok("Moved to first field".to_string())
}
"move_last_line" => {
let num_fields = state.fields().len();
if num_fields > 0 {
let new_field = num_fields - 1;
state.set_current_field(new_field);
let current_input = state.get_current_input();
let max_pos = current_input.len();
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
}
Ok("Moved to last field".to_string())
}
"move_word_next" => {
let current_input = state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
let final_pos = new_pos.min(current_input.len());
state.set_current_cursor_pos(final_pos);
*ideal_cursor_column = final_pos;
}
Ok("".to_string())
}
"move_word_end" => {
let current_input = state.get_current_input();
if !current_input.is_empty() {
let current_pos = state.current_cursor_pos();
let new_pos = find_word_end(current_input, current_pos);
let final_pos = if new_pos == current_pos {
find_word_end(current_input, new_pos + 1)
} else {
new_pos
};
let max_valid_index = current_input.len().saturating_sub(1);
let clamped_pos = final_pos.min(max_valid_index);
state.set_current_cursor_pos(clamped_pos);
*ideal_cursor_column = clamped_pos;
}
Ok("".to_string())
}
"move_word_prev" => {
let current_input = state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_prev_word_start(current_input, state.current_cursor_pos());
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
}
Ok("".to_string())
}
"move_word_end_prev" => {
let current_input = state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_prev_word_end(current_input, state.current_cursor_pos());
state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
}
Ok("Moved to previous word end".to_string())
}
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
}
}
// Word movement helper functions
#[derive(PartialEq)]
enum CharType {
Whitespace,
Alphanumeric,
Punctuation,
}
fn get_char_type(c: char) -> CharType {
if c.is_whitespace() {
CharType::Whitespace
} else if c.is_alphanumeric() {
CharType::Alphanumeric
} else {
CharType::Punctuation
}
}
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
if len == 0 || current_pos >= len {
return len;
}
let mut pos = current_pos;
let initial_type = get_char_type(chars[pos]);
while pos < len && get_char_type(chars[pos]) == initial_type {
pos += 1;
}
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
pos += 1;
}
pos
}
fn find_word_end(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
if len == 0 {
return 0;
}
let mut pos = current_pos.min(len - 1);
if get_char_type(chars[pos]) == CharType::Whitespace {
pos = find_next_word_start(text, pos);
}
if pos >= len {
return len.saturating_sub(1);
}
let word_type = get_char_type(chars[pos]);
while pos < len && get_char_type(chars[pos]) == word_type {
pos += 1;
}
pos.saturating_sub(1).min(len.saturating_sub(1))
}
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || current_pos == 0 {
return 0;
}
let mut pos = current_pos.saturating_sub(1);
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
pos -= 1;
}
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
return 0;
}
let word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
pos -= 1;
}
pos
}
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
if len == 0 || current_pos == 0 {
return 0;
}
let mut pos = current_pos.saturating_sub(1);
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
pos -= 1;
}
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
return 0;
}
if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace {
return 0;
}
let word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
pos -= 1;
}
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
pos -= 1;
}
if pos > 0 {
pos - 1
} else {
0
}
}

View File

@@ -0,0 +1,3 @@
// canvas/src/actions/mod.rs
pub mod edit;

30
canvas/src/lib.rs Normal file
View File

@@ -0,0 +1,30 @@
// canvas/src/lib.rs
//! Canvas - A reusable text editing and form canvas system
//!
//! This crate provides a generic canvas abstraction for building text-based interfaces
//! with multiple input fields, cursor management, and mode-based editing.
pub mod state;
pub mod actions;
pub mod modes;
pub mod suggestions;
// Re-export the main types for easy use
pub use state::{CanvasState, ActionContext};
pub use actions::edit::execute_edit_action;
pub use modes::{AppMode, ModeManager, HighlightState};
pub use suggestions::SuggestionState;
// High-level convenience API
pub mod prelude {
pub use crate::{
CanvasState,
ActionContext,
execute_edit_action,
AppMode,
ModeManager,
HighlightState,
SuggestionState,
};
}

View File

@@ -0,0 +1,15 @@
// src/state/app/highlight.rs
// canvas/src/modes/highlight.rs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HighlightState {
Off,
Characterwise { anchor: (usize, usize) }, // (field_index, char_position)
Linewise { anchor_line: usize }, // field_index
}
impl Default for HighlightState {
fn default() -> Self {
HighlightState::Off
}
}

View File

@@ -0,0 +1,34 @@
// src/modes/handlers/mode_manager.rs
// canvas/src/modes/manager.rs
use crate::modes::highlight::HighlightState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode {
General, // For intro and admin screens
ReadOnly, // Canvas read-only mode
Edit, // Canvas edit mode
Highlight, // Canvas highlight/visual mode
Command, // Command mode overlay
}
pub struct ModeManager;
impl ModeManager {
// Mode transition rules
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
!matches!(current_mode, AppMode::Edit)
}
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
matches!(current_mode, AppMode::ReadOnly)
}
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
matches!(current_mode, AppMode::Edit | AppMode::Command | AppMode::Highlight)
}
pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool {
matches!(current_mode, AppMode::ReadOnly)
}
}

7
canvas/src/modes/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
// canvas/src/modes/mod.rs
pub mod highlight;
pub mod manager;
pub use highlight::HighlightState;
pub use manager::{AppMode, ModeManager};

64
canvas/src/state.rs Normal file
View File

@@ -0,0 +1,64 @@
// canvas/src/state.rs
/// Context passed to feature-specific action handlers
#[derive(Debug)]
pub struct ActionContext {
pub key_code: Option<crossterm::event::KeyCode>,
pub ideal_cursor_column: usize,
pub current_input: String,
pub current_field: usize,
}
/// Core trait that any form-like state must implement to work with the canvas system.
/// This enables the same mode behaviors (edit, read-only, highlight) to work across
/// any implementation - login forms, data entry forms, configuration screens, etc.
pub trait CanvasState {
// --- Core Navigation ---
fn current_field(&self) -> usize;
fn current_cursor_pos(&self) -> usize;
fn set_current_field(&mut self, index: usize);
fn set_current_cursor_pos(&mut self, pos: usize);
// --- Data Access ---
fn get_current_input(&self) -> &str;
fn get_current_input_mut(&mut self) -> &mut String;
fn inputs(&self) -> Vec<&String>;
fn fields(&self) -> Vec<&str>;
// --- State Management ---
fn has_unsaved_changes(&self) -> bool;
fn set_has_unsaved_changes(&mut self, changed: bool);
// --- Autocomplete/Suggestions (Optional) ---
fn get_suggestions(&self) -> Option<&[String]> {
None
}
fn get_selected_suggestion_index(&self) -> Option<usize> {
None
}
fn set_selected_suggestion_index(&mut self, _index: Option<usize>) {
// Default: no-op (override if you support suggestions)
}
fn activate_suggestions(&mut self, _suggestions: Vec<String>) {
// Default: no-op (override if you support suggestions)
}
fn deactivate_suggestions(&mut self) {
// Default: no-op (override if you support suggestions)
}
// --- Feature-specific action handling ---
fn handle_feature_action(&mut self, action: &str, context: &ActionContext) -> Option<String> {
None // Default: no feature-specific handling
}
// --- Display Overrides (for links, computed values, etc.) ---
fn get_display_value_for_field(&self, index: usize) -> &str {
self.inputs()
.get(index)
.map(|s| s.as_str())
.unwrap_or("")
}
fn has_display_override(&self, _index: usize) -> bool {
false
}
}

67
canvas/src/suggestions.rs Normal file
View File

@@ -0,0 +1,67 @@
// canvas/src/suggestions.rs
/// Generic suggestion system that can be implemented by any CanvasState
#[derive(Debug, Clone)]
pub struct SuggestionState {
pub suggestions: Vec<String>,
pub selected_index: Option<usize>,
pub is_active: bool,
pub trigger_chars: Vec<char>, // Characters that trigger suggestions
}
impl Default for SuggestionState {
fn default() -> Self {
Self {
suggestions: Vec::new(),
selected_index: None,
is_active: false,
trigger_chars: vec![], // No auto-trigger by default
}
}
}
impl SuggestionState {
pub fn new(trigger_chars: Vec<char>) -> Self {
Self {
trigger_chars,
..Default::default()
}
}
pub fn activate_with_suggestions(&mut self, suggestions: Vec<String>) {
self.suggestions = suggestions;
self.is_active = !self.suggestions.is_empty();
self.selected_index = if self.is_active { Some(0) } else { None };
}
pub fn deactivate(&mut self) {
self.suggestions.clear();
self.selected_index = None;
self.is_active = false;
}
pub fn select_next(&mut self) {
if !self.suggestions.is_empty() {
let current = self.selected_index.unwrap_or(0);
self.selected_index = Some((current + 1) % self.suggestions.len());
}
}
pub fn select_previous(&mut self) {
if !self.suggestions.is_empty() {
let current = self.selected_index.unwrap_or(0);
self.selected_index = Some(
if current == 0 { self.suggestions.len() - 1 } else { current - 1 }
);
}
}
pub fn get_selected(&self) -> Option<&String> {
self.selected_index
.and_then(|idx| self.suggestions.get(idx))
}
pub fn should_trigger(&self, c: char) -> bool {
self.trigger_chars.contains(&c)
}
}

View File

@@ -5,17 +5,17 @@ edition.workspace = true
license.workspace = true
[dependencies]
anyhow = "1.0.98"
anyhow = { workspace = true }
async-trait = "0.1.88"
common = { path = "../common" }
ratatui = { workspace = true }
crossterm = { workspace = true }
prost-types = { workspace = true }
crossterm = "0.28.1"
dirs = "6.0.0"
dotenvy = "0.15.7"
lazy_static = "1.5.0"
prost = "0.13.5"
ratatui = { version = "0.29.0", features = ["crossterm"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
time = "0.3.41"
@@ -30,7 +30,7 @@ unicode-width = "0.2.0"
[features]
default = []
ui-debug = []
[dev-dependencies]
rstest = "0.25.0"