Skip to main content

limit_cli/tui/commands/
share.rs

1//! Share command - export sessions
2//!
3//! Handles /share clipboard/md/json
4
5use super::{Command, CommandContext, CommandResult};
6use crate::error::CliError;
7use crate::session_share::{ExportFormat, SessionShare};
8
9/// Share command - exports session to clipboard or file
10pub struct ShareCommand;
11
12impl ShareCommand {
13    pub fn new() -> Self {
14        Self
15    }
16
17    fn get_format(&self, args: &str) -> Option<ExportFormat> {
18        match args.trim().to_lowercase().as_str() {
19            "" | "clipboard" | "cb" => Some(ExportFormat::Markdown),
20            "md" | "markdown" => Some(ExportFormat::Markdown),
21            "json" => Some(ExportFormat::Json),
22            _ => None,
23        }
24    }
25
26    fn handle_clipboard(
27        &self,
28        ctx: &mut CommandContext,
29        format: ExportFormat,
30        session_id: &str,
31    ) -> Result<CommandResult, CliError> {
32        let messages = ctx.messages.lock().unwrap().clone();
33        let total_input_tokens = *ctx.total_input_tokens.lock().unwrap();
34        let total_output_tokens = *ctx.total_output_tokens.lock().unwrap();
35
36        // Check if there are messages to share
37        let user_assistant_count = messages
38            .iter()
39            .filter(|m| matches!(m.role, limit_llm::Role::User | limit_llm::Role::Assistant))
40            .count();
41
42        if user_assistant_count == 0 {
43            ctx.add_system_message(
44                "⚠ No messages to share. Start a conversation first.".to_string(),
45            );
46            return Ok(CommandResult::Continue);
47        }
48
49        match SessionShare::generate_share_content(
50            session_id,
51            &messages,
52            total_input_tokens,
53            total_output_tokens,
54            None, // model - not available in context
55            format,
56        ) {
57            Ok(content) => {
58                if let Some(ref clipboard) = ctx.clipboard {
59                    match clipboard.lock().unwrap().set_text(&content) {
60                        Ok(()) => {
61                            let short_id = &session_id[..session_id.len().min(8)];
62                            ctx.add_system_message(format!(
63                                "✓ Session {} copied to clipboard ({} messages, {} tokens)",
64                                short_id,
65                                user_assistant_count,
66                                total_input_tokens + total_output_tokens
67                            ));
68                        }
69                        Err(e) => {
70                            ctx.add_system_message(format!(
71                                "❌ Failed to copy to clipboard: {}",
72                                e
73                            ));
74                        }
75                    }
76                } else {
77                    ctx.add_system_message(
78                        "❌ Clipboard not available. Try '/share md' to save as file.".to_string(),
79                    );
80                }
81            }
82            Err(e) => {
83                ctx.add_system_message(format!("❌ Failed to generate share content: {}", e));
84            }
85        }
86
87        Ok(CommandResult::Continue)
88    }
89
90    fn handle_file_export(
91        &self,
92        ctx: &mut CommandContext,
93        format: ExportFormat,
94        session_id: &str,
95    ) -> Result<CommandResult, CliError> {
96        let messages = ctx.messages.lock().unwrap().clone();
97        let total_input_tokens = *ctx.total_input_tokens.lock().unwrap();
98        let total_output_tokens = *ctx.total_output_tokens.lock().unwrap();
99
100        // Check if there are messages to share
101        let user_assistant_count = messages
102            .iter()
103            .filter(|m| matches!(m.role, limit_llm::Role::User | limit_llm::Role::Assistant))
104            .count();
105
106        if user_assistant_count == 0 {
107            ctx.add_system_message(
108                "⚠ No messages to share. Start a conversation first.".to_string(),
109            );
110            return Ok(CommandResult::Continue);
111        }
112
113        match SessionShare::export_session(
114            session_id,
115            &messages,
116            total_input_tokens,
117            total_output_tokens,
118            None, // model - not available in context
119            format,
120        ) {
121            Ok((filepath, export)) => {
122                let short_id = &session_id[..session_id.len().min(8)];
123                let extension = match format {
124                    ExportFormat::Markdown => "md",
125                    ExportFormat::Json => "json",
126                };
127                ctx.add_system_message(format!(
128                    "✓ Session {} exported to {}\n  ({} messages, {} tokens)\n  Location: ~/.limit/exports/",
129                    short_id,
130                    extension,
131                    user_assistant_count,
132                    total_input_tokens + total_output_tokens
133                ));
134
135                tracing::info!(
136                    "Session exported to {:?} ({} messages)",
137                    filepath,
138                    export.messages.len()
139                );
140            }
141            Err(e) => {
142                ctx.add_system_message(format!("❌ Failed to export session: {}", e));
143            }
144        }
145
146        Ok(CommandResult::Continue)
147    }
148}
149
150impl Command for ShareCommand {
151    fn name(&self) -> &str {
152        "share"
153    }
154
155    fn description(&self) -> &str {
156        "Export session to clipboard or file"
157    }
158
159    fn usage(&self) -> Vec<&str> {
160        vec!["/share", "/share md", "/share json"]
161    }
162
163    fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError> {
164        let format = match self.get_format(args) {
165            Some(f) => f,
166            None => {
167                ctx.add_system_message(
168                    "Invalid format. Use: /share, /share md, /share json".to_string(),
169                );
170                return Ok(CommandResult::Continue);
171            }
172        };
173
174        let session_id = ctx.session_id.clone();
175        let args_lower = args.trim().to_lowercase();
176
177        // If clipboard mode (empty, "clipboard", or "cb")
178        if args_lower.is_empty() || args_lower == "clipboard" || args_lower == "cb" {
179            self.handle_clipboard(ctx, format, &session_id)
180        } else {
181            // File export mode
182            self.handle_file_export(ctx, format, &session_id)
183        }
184    }
185}
186
187impl Default for ShareCommand {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_share_command() {
199        let cmd = ShareCommand::new();
200        assert_eq!(cmd.name(), "share");
201    }
202
203    #[test]
204    fn test_share_default() {
205        let cmd = ShareCommand;
206        assert_eq!(cmd.name(), "share");
207    }
208
209    #[test]
210    fn test_get_format() {
211        let cmd = ShareCommand::new();
212
213        assert!(matches!(cmd.get_format(""), Some(ExportFormat::Markdown)));
214        assert!(matches!(cmd.get_format("md"), Some(ExportFormat::Markdown)));
215        assert!(matches!(cmd.get_format("json"), Some(ExportFormat::Json)));
216        assert!(cmd.get_format("invalid").is_none());
217    }
218}