Skip to main content

datui_lib/widgets/
text_input_common.rs

1use color_eyre::Result;
2use std::fs;
3use std::io::{BufRead, BufReader, Write};
4
5use crate::cache::CacheManager;
6
7/// Shared utilities for text input widgets
8/// Load history from a cache file
9pub fn load_history_impl(cache: &CacheManager, history_id: &str) -> Result<Vec<String>> {
10    let history_file = cache.cache_file(&format!("{}_history.txt", history_id));
11
12    if !history_file.exists() {
13        return Ok(Vec::new());
14    }
15
16    let file = fs::File::open(&history_file)?;
17    let reader = BufReader::new(file);
18    let mut history = Vec::new();
19
20    for line in reader.lines() {
21        let line = line?;
22        if !line.trim().is_empty() {
23            history.push(line);
24        }
25    }
26
27    Ok(history)
28}
29
30/// Save history to a cache file
31pub fn save_history_impl(
32    cache: &CacheManager,
33    history_id: &str,
34    history: &[String],
35    limit: usize,
36) -> Result<()> {
37    cache.ensure_cache_dir()?;
38    let history_file = cache.cache_file(&format!("{}_history.txt", history_id));
39
40    let mut file = fs::File::create(&history_file)?;
41
42    // Write history entries (oldest first, but we keep the most recent `limit` entries)
43    let start = history.len().saturating_sub(limit);
44    for entry in history.iter().skip(start) {
45        writeln!(file, "{}", entry)?;
46    }
47
48    Ok(())
49}
50
51/// Add entry to history with deduplication
52/// Only consecutive duplicate entries are skipped
53pub fn add_to_history(history: &mut Vec<String>, entry: String) {
54    // Only skip if the new entry matches the last entry (consecutive duplicate)
55    if let Some(last) = history.last() {
56        if last == &entry {
57            return; // Skip consecutive duplicate
58        }
59    }
60    history.push(entry);
61}
62
63/// Convert character position to byte position in a UTF-8 string
64pub fn char_to_byte_pos(text: &str, char_pos: usize) -> usize {
65    text.chars().take(char_pos).map(|c| c.len_utf8()).sum()
66}
67
68/// Convert byte position to character position in a UTF-8 string
69pub fn byte_to_char_pos(text: &str, byte_pos: usize) -> usize {
70    let mut char_pos = 0;
71    let mut byte_count = 0;
72
73    for ch in text.chars() {
74        if byte_count >= byte_pos {
75            break;
76        }
77        byte_count += ch.len_utf8();
78        char_pos += 1;
79    }
80
81    char_pos
82}
83
84/// Get the character at a given character position
85pub fn char_at(text: &str, char_pos: usize) -> Option<char> {
86    text.chars().nth(char_pos)
87}
88
89/// Get the byte range for a character at a given character position
90pub fn char_byte_range(text: &str, char_pos: usize) -> Option<(usize, usize)> {
91    let mut byte_start = 0;
92
93    for (char_count, ch) in text.chars().enumerate() {
94        if char_count == char_pos {
95            return Some((byte_start, byte_start + ch.len_utf8()));
96        }
97        byte_start += ch.len_utf8();
98    }
99
100    None
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_char_to_byte_pos() {
109        assert_eq!(char_to_byte_pos("hello", 0), 0);
110        assert_eq!(char_to_byte_pos("hello", 5), 5);
111        assert_eq!(char_to_byte_pos("café", 3), 3); // 'é' is 2 bytes
112        assert_eq!(char_to_byte_pos("café", 4), 5);
113        assert_eq!(char_to_byte_pos("🚀", 0), 0);
114        assert_eq!(char_to_byte_pos("🚀", 1), 4); // Emoji is 4 bytes
115    }
116
117    #[test]
118    fn test_byte_to_char_pos() {
119        assert_eq!(byte_to_char_pos("hello", 0), 0);
120        assert_eq!(byte_to_char_pos("hello", 5), 5);
121        assert_eq!(byte_to_char_pos("café", 3), 3);
122        assert_eq!(byte_to_char_pos("café", 5), 4);
123        assert_eq!(byte_to_char_pos("🚀", 0), 0);
124        assert_eq!(byte_to_char_pos("🚀", 4), 1);
125    }
126
127    #[test]
128    fn test_char_at() {
129        assert_eq!(char_at("hello", 0), Some('h'));
130        assert_eq!(char_at("hello", 4), Some('o'));
131        assert_eq!(char_at("café", 3), Some('é'));
132        assert_eq!(char_at("🚀", 0), Some('🚀'));
133        assert_eq!(char_at("hello", 10), None);
134    }
135
136    #[test]
137    fn test_char_byte_range() {
138        assert_eq!(char_byte_range("hello", 0), Some((0, 1)));
139        assert_eq!(char_byte_range("hello", 4), Some((4, 5)));
140        assert_eq!(char_byte_range("café", 3), Some((3, 5))); // 'é' is 2 bytes
141        assert_eq!(char_byte_range("🚀", 0), Some((0, 4))); // Emoji is 4 bytes
142        assert_eq!(char_byte_range("hello", 10), None);
143    }
144
145    #[test]
146    fn test_add_to_history() {
147        let mut history = Vec::new();
148
149        // Add first entry
150        add_to_history(&mut history, "query1".to_string());
151        assert_eq!(history.len(), 1);
152
153        // Add different entry
154        add_to_history(&mut history, "query2".to_string());
155        assert_eq!(history.len(), 2);
156
157        // Add consecutive duplicate (should be skipped)
158        add_to_history(&mut history, "query2".to_string());
159        assert_eq!(history.len(), 2);
160
161        // Add non-consecutive duplicate (should be preserved)
162        add_to_history(&mut history, "query1".to_string());
163        assert_eq!(history.len(), 3);
164        assert_eq!(history[0], "query1");
165        assert_eq!(history[1], "query2");
166        assert_eq!(history[2], "query1");
167    }
168}