sql_cli/
yank_manager.rs

1use crate::app_state_container::AppStateContainer;
2use crate::buffer::BufferAPI;
3use crate::data_exporter::DataExporter;
4use anyhow::{anyhow, Result};
5use serde_json::Value;
6use tracing::trace;
7
8/// Manages clipboard operations for data yanking
9pub struct YankManager;
10
11/// Result of a yank operation
12pub struct YankResult {
13    pub description: String,
14    pub preview: String,
15    pub full_value: String,
16}
17
18impl YankManager {
19    /// Yank a single cell value to clipboard
20    pub fn yank_cell(
21        buffer: &dyn BufferAPI,
22        state_container: &AppStateContainer,
23        row_index: usize,
24        column_index: usize,
25    ) -> Result<YankResult> {
26        // Prefer DataView when available (handles filtering)
27        let (value, header, actual_row_index) = if let Some(dataview) = buffer.get_dataview() {
28            trace!(
29                "yank_cell: Using DataView for cell at visual_row={}, col={}",
30                row_index,
31                column_index
32            );
33
34            // The row_index here is the visual row index (e.g., row 0 in filtered view)
35            // DataView's get_cell_value already handles this correctly
36            let value = dataview
37                .get_cell_value(row_index, column_index)
38                .unwrap_or_else(|| "NULL".to_string());
39
40            let headers = dataview.column_names();
41            let header = headers
42                .get(column_index)
43                .ok_or_else(|| anyhow!("Column index out of bounds"))?
44                .clone();
45
46            // Get the actual data row index from the filtered view
47            // When filtered, we need to translate visual row to actual data row
48            let actual_row =
49                if let Some(filtered_idx) = dataview.visible_row_indices().get(row_index) {
50                    *filtered_idx
51                } else {
52                    row_index
53                };
54
55            (value, header, actual_row)
56        } else if let Some(datatable) = buffer.get_datatable() {
57            trace!(
58                "yank_cell: Using DataTable for cell at row={}, col={}",
59                row_index,
60                column_index
61            );
62
63            let row_data = datatable
64                .get_row_as_strings(row_index)
65                .ok_or_else(|| anyhow!("Row index out of bounds"))?;
66
67            let headers = datatable.column_names();
68            let header = headers
69                .get(column_index)
70                .ok_or_else(|| anyhow!("Column index out of bounds"))?
71                .clone();
72
73            let value = row_data
74                .get(column_index)
75                .cloned()
76                .unwrap_or_else(|| "NULL".to_string());
77
78            (value, header, row_index)
79        } else {
80            return Err(anyhow!("No data available"));
81        };
82
83        // Prepare display value
84        let col_name = header.to_string();
85        let display_value = if value.len() > 20 {
86            format!("{}...", &value[..17])
87        } else {
88            value.clone()
89        };
90
91        // Copy to clipboard using AppStateContainer
92        let clipboard_len = value.len();
93        state_container.yank_cell(
94            actual_row_index,
95            column_index,
96            value.clone(),
97            display_value.clone(),
98        )?;
99
100        Ok(YankResult {
101            description: format!("{} ({} chars)", col_name, clipboard_len),
102            preview: display_value,
103            full_value: value,
104        })
105    }
106
107    /// Yank an entire row as tab-separated values
108    pub fn yank_row(
109        buffer: &dyn BufferAPI,
110        state_container: &AppStateContainer,
111        row_index: usize,
112    ) -> Result<YankResult> {
113        // Prefer DataView when available (handles filtering)
114        let (row_data, actual_row_index) = if let Some(dataview) = buffer.get_dataview() {
115            trace!("yank_row: Using DataView for row {}", row_index);
116            let data = dataview
117                .get_row_values(row_index)
118                .ok_or_else(|| anyhow!("Row index out of bounds"))?;
119
120            // Get the actual data row index from the filtered view
121            let actual_row =
122                if let Some(filtered_idx) = dataview.visible_row_indices().get(row_index) {
123                    *filtered_idx
124                } else {
125                    row_index
126                };
127
128            (data, actual_row)
129        } else if let Some(datatable) = buffer.get_datatable() {
130            trace!("yank_row: Using DataTable for row {}", row_index);
131            let data = datatable
132                .get_row_as_strings(row_index)
133                .ok_or_else(|| anyhow!("Row index out of bounds"))?;
134            (data, row_index)
135        } else {
136            return Err(anyhow!("No data available"));
137        };
138
139        // Convert row to tab-separated text
140        let row_text = row_data.join("\t");
141
142        // Count values for preview
143        let num_values = row_data.len();
144
145        // Copy to clipboard using AppStateContainer
146        let clipboard_len = row_text.len();
147        state_container.yank_row(
148            actual_row_index,
149            row_text.clone(),
150            format!("{} values", num_values),
151        )?;
152
153        Ok(YankResult {
154            description: format!("Row {} ({} chars)", row_index + 1, clipboard_len),
155            preview: format!("{} values", num_values),
156            full_value: row_text,
157        })
158    }
159
160    /// Yank an entire column
161    pub fn yank_column(
162        buffer: &dyn BufferAPI,
163        state_container: &AppStateContainer,
164        column_index: usize,
165    ) -> Result<YankResult> {
166        // Prefer DataView when available (handles filtering)
167        let (column_values, header) = if let Some(dataview) = buffer.get_dataview() {
168            let headers = dataview.column_names();
169            let header = headers
170                .get(column_index)
171                .ok_or_else(|| anyhow!("Column index out of bounds"))?
172                .clone();
173
174            trace!(
175                "yank_column: Using DataView for column {} ({}), visible rows: {}",
176                column_index,
177                header,
178                dataview.row_count()
179            );
180
181            let values = dataview.get_column_values(column_index);
182            (values, header)
183        } else if let Some(datatable) = buffer.get_datatable() {
184            // Fall back to DataTable for legacy buffers
185            let headers = datatable.column_names();
186            let header = headers
187                .get(column_index)
188                .ok_or_else(|| anyhow!("Column index out of bounds"))?
189                .clone();
190
191            trace!(
192                "yank_column: Using DataTable for column {} ({}), total rows: {}",
193                column_index,
194                header,
195                datatable.row_count()
196            );
197
198            // Check if fuzzy filter is active
199            let mut column_values = Vec::new();
200            if buffer.is_fuzzy_filter_active() {
201                let filtered_indices = buffer.get_fuzzy_filter_indices();
202                trace!(
203                    "yank_column: Filter active, yanking {} filtered rows",
204                    filtered_indices.len()
205                );
206
207                for &row_idx in filtered_indices {
208                    if let Some(row_data) = datatable.get_row_as_strings(row_idx) {
209                        let value = row_data
210                            .get(column_index)
211                            .cloned()
212                            .unwrap_or_else(|| "NULL".to_string())
213                            .replace('\t', "    ")
214                            .replace('\n', " ")
215                            .replace('\r', "");
216                        column_values.push(value);
217                    }
218                }
219            } else {
220                trace!(
221                    "yank_column: No filter, yanking all {} rows",
222                    datatable.row_count()
223                );
224
225                for row_idx in 0..datatable.row_count() {
226                    if let Some(row_data) = datatable.get_row_as_strings(row_idx) {
227                        let value = row_data
228                            .get(column_index)
229                            .cloned()
230                            .unwrap_or_else(|| "NULL".to_string())
231                            .replace('\t', "    ")
232                            .replace('\n', " ")
233                            .replace('\r', "");
234                        column_values.push(value);
235                    }
236                }
237            }
238
239            (column_values, header)
240        } else {
241            return Err(anyhow!("No data available"));
242        };
243
244        // Use Windows-compatible line endings (\r\n) for better clipboard compatibility
245        let column_text = column_values.join("\r\n");
246
247        let preview = if column_values.len() > 5 {
248            format!("{} values", column_values.len())
249        } else {
250            column_values.join(", ")
251        };
252
253        // Copy to clipboard using AppStateContainer
254        let clipboard_len = column_text.len();
255        state_container.yank_column(
256            header.to_string(),
257            column_index,
258            column_text.clone(),
259            preview.clone(),
260        )?;
261
262        Ok(YankResult {
263            description: format!("Column '{}' ({} chars)", header, clipboard_len),
264            preview,
265            full_value: column_text,
266        })
267    }
268
269    /// Yank all data as TSV (Tab-Separated Values) for better Windows clipboard compatibility
270    pub fn yank_all(
271        buffer: &dyn BufferAPI,
272        state_container: &AppStateContainer,
273    ) -> Result<YankResult> {
274        // Prefer DataView when available (which handles filtering/sorting)
275        let tsv_text = if let Some(dataview) = buffer.get_dataview() {
276            // Use DataView's built-in TSV export
277            dataview.to_tsv()?
278        } else if let Some(datatable) = buffer.get_datatable() {
279            // Fall back to DataTable for legacy buffers
280            let data = Self::datatable_to_json(datatable)?;
281            DataExporter::generate_tsv_text(&data)
282                .ok_or_else(|| anyhow!("Failed to generate TSV"))?
283        } else {
284            return Err(anyhow!("No data available"));
285        };
286
287        // Copy to clipboard using AppStateContainer
288        let clipboard_len = tsv_text.len();
289
290        // Create preview based on what data source we used
291        let (row_count, col_count, filter_info) = if let Some(dataview) = buffer.get_dataview() {
292            // Get counts from DataView
293            let rows = dataview.row_count();
294            let cols = dataview.column_count();
295            let filtered = dataview.has_filter();
296            (rows, cols, if filtered { " (filtered)" } else { "" })
297        } else if let Some(datatable) = buffer.get_datatable() {
298            // Get counts from DataTable
299            let rows = datatable.row_count();
300            let cols = datatable.column_count();
301            (rows, cols, "")
302        } else {
303            (0, 0, "")
304        };
305
306        // Call AppStateContainer's yank_all
307        let preview = format!("{} rows × {} columns", row_count, col_count);
308        state_container.yank_all(tsv_text.clone(), preview.clone())?;
309
310        Ok(YankResult {
311            description: format!("All data{} as TSV ({} chars)", filter_info, clipboard_len),
312            preview,
313            full_value: tsv_text,
314        })
315    }
316
317    /// Helper to convert DataTable to JSON for TSV generation
318    fn datatable_to_json(datatable: &crate::data::datatable::DataTable) -> Result<Vec<Value>> {
319        let headers = datatable.column_names();
320        let mut json_data = Vec::new();
321
322        for row_idx in 0..datatable.row_count() {
323            if let Some(row_data) = datatable.get_row_as_strings(row_idx) {
324                let mut obj = serde_json::Map::new();
325                for (i, header) in headers.iter().enumerate() {
326                    if let Some(value) = row_data.get(i) {
327                        // Try to preserve original types
328                        if value == "NULL" || value.is_empty() {
329                            obj.insert(header.clone(), Value::Null);
330                        } else if let Ok(n) = value.parse::<f64>() {
331                            obj.insert(
332                                header.clone(),
333                                Value::Number(
334                                    serde_json::Number::from_f64(n)
335                                        .unwrap_or_else(|| serde_json::Number::from(0)),
336                                ),
337                            );
338                        } else if value == "true" || value == "false" {
339                            obj.insert(header.clone(), Value::Bool(value == "true"));
340                        } else {
341                            obj.insert(header.clone(), Value::String(value.clone()));
342                        }
343                    } else {
344                        obj.insert(header.clone(), Value::Null);
345                    }
346                }
347                json_data.push(Value::Object(obj));
348            }
349        }
350
351        Ok(json_data)
352    }
353}