diff --git a/crates/g3-cli/Cargo.toml b/crates/g3-cli/Cargo.toml index 79f369b..7c43308 100644 --- a/crates/g3-cli/Cargo.toml +++ b/crates/g3-cli/Cargo.toml @@ -12,7 +12,7 @@ tokio = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } rustyline = "17.0.1" dirs = "5.0" diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index c1a9466..fa8cce3 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -11,9 +11,11 @@ use tracing::{error, info}; mod retro_tui; mod tui; mod ui_writer_impl; +mod theme; use retro_tui::RetroTui; use tui::SimpleOutput; use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter}; +use theme::ColorTheme; #[derive(Parser)] #[command(name = "g3")] @@ -54,6 +56,10 @@ pub struct Cli { /// Use retro terminal UI (inspired by 80s sci-fi) #[arg(long)] pub retro: bool, + + /// Color theme for retro mode (default, dracula, or path to theme file) + #[arg(long, value_name = "THEME")] + pub theme: Option, } pub async fn run() -> Result<()> { @@ -171,7 +177,7 @@ pub async fn run() -> Result<()> { if cli.retro { // Use retro terminal UI - run_interactive_retro(config, cli.show_prompt, cli.show_code).await?; + run_interactive_retro(config, cli.show_prompt, cli.show_code, cli.theme).await?; } else { // Use standard terminal UI let output = SimpleOutput::new(); @@ -183,15 +189,24 @@ pub async fn run() -> Result<()> { Ok(()) } -async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool) -> Result<()> { +async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool, theme_name: Option) -> Result<()> { use crossterm::event::{self, Event, KeyCode, KeyModifiers}; use std::time::Duration; // Set environment variable to suppress println in other crates std::env::set_var("G3_RETRO_MODE", "1"); + // Load the color theme + let theme = match ColorTheme::load(theme_name.as_deref()) { + Ok(t) => t, + Err(e) => { + eprintln!("Failed to load theme: {}. Using default.", e); + ColorTheme::default() + } + }; + // Initialize the retro terminal UI - let tui = RetroTui::start().await?; + let tui = RetroTui::start(theme).await?; // Create agent with RetroTuiWriter let ui_writer = RetroTuiWriter::new(tui.clone()); @@ -266,10 +281,10 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo KeyCode::Right => { tui.cursor_right(); } - KeyCode::Home => { + KeyCode::Home if !key.modifiers.contains(KeyModifiers::CONTROL) => { tui.cursor_home(); } - KeyCode::End => { + KeyCode::End if !key.modifiers.contains(KeyModifiers::CONTROL) => { tui.cursor_end(); } KeyCode::Delete => { diff --git a/crates/g3-cli/src/retro_tui.rs b/crates/g3-cli/src/retro_tui.rs index e11fa46..7cfbf17 100644 --- a/crates/g3-cli/src/retro_tui.rs +++ b/crates/g3-cli/src/retro_tui.rs @@ -18,16 +18,9 @@ use std::time::{Duration, Instant}; use tokio::sync::mpsc; use std::collections::VecDeque; -// Retro sci-fi color scheme inspired by Alien terminals -const TERMINAL_GREEN: Color = Color::Rgb(136, 244, 152); // Mid green -const TERMINAL_AMBER: Color = Color::Rgb(242, 204, 148); // Softer amber for warnings -const TERMINAL_DIM_GREEN: Color = Color::Rgb(154, 174, 135); // softer vintage green for borders -const TERMINAL_BG: Color = Color::Rgb(0, 10, 0); // Very dark green background -const TERMINAL_CYAN: Color = Color::Rgb(0, 255, 255); // Cyan for highlights -const TERMINAL_RED: Color = Color::Rgb(239, 119, 109); // Red for errors or negative diffs -const TERMINAL_PALE_BLUE: Color = Color::Rgb(173, 234, 251); // Pale blue for READY status -const TERMINAL_DARK_AMBER: Color = Color::Rgb(204, 119, 34); // Dark amber for PROCESSING status -const TERMINAL_WHITE: Color = Color::Rgb(218, 218, 219); // Dimmer white for punchy text +use crate::theme::ColorTheme; + +// Color theme will be loaded dynamically // Scrolling configuration const SCROLL_PAST_END_BUFFER: usize = 10; // Extra lines to allow scrolling past the end @@ -64,6 +57,8 @@ pub enum TuiMessage { /// Shared state for the retro terminal struct TerminalState { + /// Color theme + theme: ColorTheme, /// Current input buffer input_buffer: String, /// Cursor position in input buffer (for editing) @@ -119,8 +114,9 @@ struct TerminalState { } impl TerminalState { - fn new() -> Self { + fn new(theme: ColorTheme) -> Self { Self { + theme, input_buffer: String::new(), cursor_position: 0, output_history: vec![ @@ -332,7 +328,7 @@ pub struct RetroTui { impl RetroTui { /// Create and start the retro terminal UI - pub async fn start() -> Result { + pub async fn start(theme: ColorTheme) -> Result { // Setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); @@ -343,7 +339,7 @@ impl RetroTui { // Create message channel let (tx, mut rx) = mpsc::unbounded_channel::(); - let state = Arc::new(Mutex::new(TerminalState::new())); + let state = Arc::new(Mutex::new(TerminalState::new(theme))); let terminal = Arc::new(Mutex::new(terminal)); // Clone for the background task @@ -575,16 +571,16 @@ impl RetroTui { } // Draw header/input area - Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_position, state.cursor_blink, state.is_processing); + Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_position, state.cursor_blink, state.is_processing, &state.theme); // Draw main output area - Self::draw_output_area(f, chunks[1], &state.output_history, state.scroll_offset); + Self::draw_output_area(f, chunks[1], &state.output_history, state.scroll_offset, &state.theme); // Draw activity area only if it's visible (during animation or when shown) if activity_height > 0 { // Apply fade effect by adjusting opacity through color intensity let opacity = state.activity_animation; - Self::draw_activity_area(f, chunks[2], state, opacity); + Self::draw_activity_area(f, chunks[2], state, opacity, &state.theme); } // Draw status bar - use the last chunk which is either index 2 or 3 @@ -600,6 +596,7 @@ impl RetroTui { state.context_info, &state.provider_info, state.status_blink, + &state.theme, ); })?; @@ -607,7 +604,7 @@ impl RetroTui { } /// Draw the input area with prompt - fn draw_input_area(f: &mut Frame, area: Rect, input_buffer: &str, cursor_position: usize, cursor_blink: bool, is_processing: bool) { + fn draw_input_area(f: &mut Frame, area: Rect, input_buffer: &str, cursor_position: usize, cursor_blink: bool, is_processing: bool, theme: &ColorTheme) { let prompt = "g3> "; let prompt_len = prompt.len(); @@ -664,14 +661,14 @@ impl RetroTui { } let input = Paragraph::new(display_text) - .style(Style::default().fg(TERMINAL_GREEN)) + .style(Style::default().fg(theme.terminal_green.to_color())) .block( Block::default() .title(" COMMAND INPUT ") .title_alignment(Alignment::Center) .borders(Borders::ALL) - .border_style(Style::default().fg(TERMINAL_DIM_GREEN)) - .style(Style::default().bg(TERMINAL_BG)), + .border_style(Style::default().fg(theme.terminal_dim_green.to_color())) + .style(Style::default().bg(theme.terminal_bg.to_color())), ); f.render_widget(input, area); @@ -683,6 +680,7 @@ impl RetroTui { area: Rect, output_history: &[String], scroll_offset: usize, + theme: &ColorTheme, ) { // Calculate visible lines (no borders now, but padding takes 2 lines) let visible_height = area.height.saturating_sub(2) as usize; // Account for padding @@ -719,7 +717,7 @@ impl RetroTui { return Line::from(Span::styled( format!(" {}", cleaned), Style::default() - .bg(TERMINAL_AMBER) + .bg(theme.terminal_amber.to_color()) .fg(Color::Black) .add_modifier(Modifier::BOLD), )); @@ -730,7 +728,7 @@ impl RetroTui { return Line::from(Span::styled( format!(" {}", cleaned), Style::default() - .bg(TERMINAL_GREEN) + .bg(theme.terminal_green.to_color()) .fg(Color::Black) .add_modifier(Modifier::BOLD), )); @@ -741,7 +739,7 @@ impl RetroTui { return Line::from(Span::styled( format!(" {}", cleaned), Style::default() - .bg(TERMINAL_RED) + .bg(theme.terminal_red.to_color()) .fg(Color::Black) .add_modifier(Modifier::BOLD), )); @@ -755,31 +753,31 @@ impl RetroTui { { return Line::from(Span::styled( format!(" {}", line), - Style::default().fg(TERMINAL_DIM_GREEN), + Style::default().fg(theme.terminal_dim_green.to_color()), )); } // Apply different colors based on content let style = if line.starts_with("ERROR:") { Style::default() - .fg(TERMINAL_RED) + .fg(theme.terminal_red.to_color()) .add_modifier(Modifier::BOLD) } else if line.starts_with('>') { - Style::default().fg(TERMINAL_CYAN) + Style::default().fg(theme.terminal_cyan.to_color()) } else if line.starts_with("SYSTEM:") || line.starts_with("WEYLAND") || line.starts_with("MU/TH/UR") { Style::default() - .fg(TERMINAL_AMBER) + .fg(theme.terminal_amber.to_color()) .add_modifier(Modifier::BOLD) } else if line.starts_with("SYSTEM INITIALIZED") || line.starts_with("AWAITING COMMAND") { Style::default() - .fg(TERMINAL_DIM_GREEN) + .fg(theme.terminal_dim_green.to_color()) .add_modifier(Modifier::BOLD) } else { - Style::default().fg(TERMINAL_GREEN) + Style::default().fg(theme.terminal_green.to_color()) }; Line::from(Span::styled(format!(" {}", line), style)) @@ -793,7 +791,7 @@ impl RetroTui { .borders(Borders::NONE) // Add padding to maintain the same spacing as borders would provide .padding(ratatui::widgets::Padding::new(1, 1, 1, 1)) - .style(Style::default().bg(TERMINAL_BG)), + .style(Style::default().bg(theme.terminal_bg.to_color())), ) .wrap(Wrap { trim: false }); @@ -806,7 +804,7 @@ impl RetroTui { .end_symbol(Some("▼")) .track_symbol(Some("│")) .thumb_symbol("█") - .style(Style::default().fg(TERMINAL_DIM_GREEN)); + .style(Style::default().fg(theme.terminal_dim_green.to_color())); let mut scrollbar_state = ScrollbarState::new(total_lines) .position(scroll) @@ -829,6 +827,7 @@ impl RetroTui { area: Rect, state: &TerminalState, opacity: f32, + theme: &ColorTheme, ) { // Note: scroll_offset is managed by the state and auto-scrolls to show latest content when new data arrives @@ -870,7 +869,7 @@ impl RetroTui { let visible_lines: Vec = if state.tool_activity.is_empty() { vec![Line::from(Span::styled( " No tool activity yet", - Style::default().fg(fade_color(TERMINAL_DIM_GREEN)).add_modifier(Modifier::ITALIC), + Style::default().fg(fade_color(theme.terminal_dim_green.to_color())).add_modifier(Modifier::ITALIC), ))] } else { state.tool_activity @@ -880,11 +879,11 @@ impl RetroTui { .map(|line| { // Style the header lines differently let style = if line.starts_with('[') && line.contains(']') { - Style::default().fg(fade_color(TERMINAL_CYAN)).add_modifier(Modifier::BOLD) + Style::default().fg(fade_color(theme.terminal_cyan.to_color())).add_modifier(Modifier::BOLD) } else if line.is_empty() { Style::default() } else { - Style::default().fg(fade_color(TERMINAL_GREEN)) + Style::default().fg(fade_color(theme.terminal_green.to_color())) }; Line::from(Span::styled(format!(" {}", line), style)) }) @@ -897,15 +896,15 @@ impl RetroTui { .title(" TOOL DETAIL ") .title_alignment(Alignment::Center) .borders(Borders::ALL) - .border_style(Style::default().fg(fade_color(TERMINAL_DIM_GREEN))) - .style(Style::default().bg(TERMINAL_BG)), + .border_style(Style::default().fg(fade_color(theme.terminal_dim_green.to_color()))) + .style(Style::default().bg(theme.terminal_bg.to_color())), ) .wrap(Wrap { trim: false }); f.render_widget(tool_output, chunks[0]); // Draw right half - Activity graphs with wave animations - Self::draw_activity_graphs(f, chunks[1], &state.token_wave_history, &state.sse_wave_history, opacity); + Self::draw_activity_graphs(f, chunks[1], &state.token_wave_history, &state.sse_wave_history, opacity, theme); } /// Draw activity graphs with wave animations for tokens and SSEs @@ -915,6 +914,7 @@ impl RetroTui { token_wave: &VecDeque, sse_wave: &VecDeque, opacity: f32, + theme: &ColorTheme, ) { // Apply fade effect by adjusting colors based on opacity let fade_color = |color: Color| -> Color { @@ -934,8 +934,8 @@ impl RetroTui { .title(" ACTIVITY ") .title_alignment(Alignment::Center) .borders(Borders::ALL) - .border_style(Style::default().fg(fade_color(TERMINAL_DIM_GREEN))) - .style(Style::default().bg(TERMINAL_BG)); + .border_style(Style::default().fg(fade_color(theme.terminal_dim_green.to_color()))) + .style(Style::default().bg(theme.terminal_bg.to_color())); // Calculate inner area for chart let inner = block.inner(area); @@ -963,8 +963,8 @@ impl RetroTui { graph_chunks[0], token_wave, "TOKENS", - fade_color(TERMINAL_CYAN), - fade_color(TERMINAL_DIM_GREEN), + fade_color(theme.terminal_cyan.to_color()), + fade_color(theme.terminal_dim_green.to_color()), opacity, ); @@ -974,8 +974,8 @@ impl RetroTui { graph_chunks[1], sse_wave, "SSE", - fade_color(TERMINAL_GREEN), - fade_color(TERMINAL_DIM_GREEN), + fade_color(theme.terminal_green.to_color()), + fade_color(theme.terminal_dim_green.to_color()), opacity, ); } @@ -987,7 +987,7 @@ impl RetroTui { wave_data: &VecDeque, label: &str, wave_color: Color, - axis_color: Color, + _axis_color: Color, _opacity: f32, ) { let width = area.width as usize; @@ -1038,6 +1038,7 @@ impl RetroTui { context_info: (u32, u32, f32), provider_info: &(String, String), status_blink: bool, + theme: &ColorTheme, ) { let (used, total, percentage) = context_info; @@ -1052,15 +1053,15 @@ impl RetroTui { let (status_color, status_text) = if status_line == "PROCESSING" { // Blink the PROCESSING status if status_blink { - (TERMINAL_DARK_AMBER, status_line) + (theme.terminal_dark_amber.to_color(), status_line) } else { - (TERMINAL_BG, " ") // Hide text by matching background + (theme.terminal_bg.to_color(), " ") // Hide text by matching background } } else if status_line == "READY" { - (TERMINAL_PALE_BLUE, status_line) + (theme.terminal_pale_blue.to_color(), status_line) } else { // Default to amber for other statuses - (TERMINAL_AMBER, status_line) + (theme.terminal_amber.to_color(), status_line) }; // Build the status line with different colored spans @@ -1068,7 +1069,7 @@ impl RetroTui { Span::styled( " STATUS: ", Style::default() - .fg(TERMINAL_AMBER) + .fg(theme.terminal_amber.to_color()) .add_modifier(Modifier::BOLD), ), Span::styled( @@ -1080,25 +1081,25 @@ impl RetroTui { Span::styled( " | CONTEXT: ", Style::default() - .fg(TERMINAL_AMBER) + .fg(theme.terminal_amber.to_color()) .add_modifier(Modifier::BOLD), ), Span::styled( format!("{} {:.1}% ({}/{})", meter, percentage, used, total), Style::default() - .fg(TERMINAL_AMBER) + .fg(theme.terminal_amber.to_color()) .add_modifier(Modifier::BOLD), ), Span::styled( " | ", Style::default() - .fg(TERMINAL_AMBER) + .fg(theme.terminal_amber.to_color()) .add_modifier(Modifier::BOLD), ), Span::styled( format!("{} ", model), Style::default() - .fg(TERMINAL_AMBER) + .fg(theme.terminal_amber.to_color()) .add_modifier(Modifier::BOLD), ), ]; @@ -1106,7 +1107,7 @@ impl RetroTui { let status_line = Line::from(status_spans); let status = Paragraph::new(status_line) - .style(Style::default().bg(TERMINAL_BG)) + .style(Style::default().bg(theme.terminal_bg.to_color())) .alignment(Alignment::Left); f.render_widget(status, area); diff --git a/crates/g3-cli/src/theme.rs b/crates/g3-cli/src/theme.rs new file mode 100644 index 0000000..ee4ce93 --- /dev/null +++ b/crates/g3-cli/src/theme.rs @@ -0,0 +1,185 @@ +use ratatui::style::Color; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; +use anyhow::Result; + +/// Color theme configuration for the retro TUI +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ColorTheme { + /// Name of the theme + pub name: String, + + /// Main terminal text color (for general output) + pub terminal_green: ColorValue, + + /// Warning/system messages color + pub terminal_amber: ColorValue, + + /// Border and dim text color + pub terminal_dim_green: ColorValue, + + /// Background color + pub terminal_bg: ColorValue, + + /// Highlight/emphasis color + pub terminal_cyan: ColorValue, + + /// Error/negative diff color + pub terminal_red: ColorValue, + + /// READY status color + pub terminal_pale_blue: ColorValue, + + /// PROCESSING status color + pub terminal_dark_amber: ColorValue, + + /// Bright/punchy text color + pub terminal_white: ColorValue, +} + +/// Represents a color value that can be serialized/deserialized +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ColorValue { + /// RGB color with r, g, b components + Rgb { r: u8, g: u8, b: u8 }, + /// Named color + Named(String), +} + +impl ColorValue { + /// Convert to ratatui Color + pub fn to_color(&self) -> Color { + match self { + ColorValue::Rgb { r, g, b } => Color::Rgb(*r, *g, *b), + ColorValue::Named(name) => match name.to_lowercase().as_str() { + "black" => Color::Black, + "red" => Color::Red, + "green" => Color::Green, + "yellow" => Color::Yellow, + "blue" => Color::Blue, + "magenta" => Color::Magenta, + "cyan" => Color::Cyan, + "gray" | "grey" => Color::Gray, + "darkgray" | "darkgrey" => Color::DarkGray, + "lightred" => Color::LightRed, + "lightgreen" => Color::LightGreen, + "lightyellow" => Color::LightYellow, + "lightblue" => Color::LightBlue, + "lightmagenta" => Color::LightMagenta, + "lightcyan" => Color::LightCyan, + "white" => Color::White, + _ => Color::White, // Default fallback + }, + } + } +} + +impl ColorTheme { + /// Load a theme from a JSON file + pub fn from_file>(path: P) -> Result { + let content = fs::read_to_string(path)?; + let theme: ColorTheme = serde_json::from_str(&content)?; + Ok(theme) + } + + /// Save a theme to a JSON file + pub fn to_file>(&self, path: P) -> Result<()> { + let content = serde_json::to_string_pretty(self)?; + fs::write(path, content)?; + Ok(()) + } + + /// Get the default retro sci-fi theme (inspired by Alien terminals) + pub fn default() -> Self { + ColorTheme { + name: "Retro Sci-Fi".to_string(), + terminal_green: ColorValue::Rgb { r: 136, g: 244, b: 152 }, + terminal_amber: ColorValue::Rgb { r: 242, g: 204, b: 148 }, + terminal_dim_green: ColorValue::Rgb { r: 154, g: 174, b: 135 }, + terminal_bg: ColorValue::Rgb { r: 0, g: 10, b: 0 }, + terminal_cyan: ColorValue::Rgb { r: 0, g: 255, b: 255 }, + terminal_red: ColorValue::Rgb { r: 239, g: 119, b: 109 }, + terminal_pale_blue: ColorValue::Rgb { r: 173, g: 234, b: 251 }, + terminal_dark_amber: ColorValue::Rgb { r: 204, g: 119, b: 34 }, + terminal_white: ColorValue::Rgb { r: 218, g: 218, b: 219 }, + } + } + + /// Get the Dracula theme + pub fn dracula() -> Self { + ColorTheme { + name: "Dracula".to_string(), + terminal_green: ColorValue::Rgb { r: 80, g: 250, b: 123 }, // Dracula green + terminal_amber: ColorValue::Rgb { r: 255, g: 184, b: 108 }, // Dracula orange + terminal_dim_green: ColorValue::Rgb { r: 98, g: 114, b: 164 }, // Dracula comment + terminal_bg: ColorValue::Rgb { r: 40, g: 42, b: 54 }, // Dracula background + terminal_cyan: ColorValue::Rgb { r: 139, g: 233, b: 253 }, // Dracula cyan + terminal_red: ColorValue::Rgb { r: 255, g: 85, b: 85 }, // Dracula red + terminal_pale_blue: ColorValue::Rgb { r: 189, g: 147, b: 249 }, // Dracula purple + terminal_dark_amber: ColorValue::Rgb { r: 255, g: 121, b: 198 }, // Dracula pink + terminal_white: ColorValue::Rgb { r: 248, g: 248, b: 242 }, // Dracula foreground + } + } + + /// Get a theme by name or from file + pub fn load(theme_name: Option<&str>) -> Result { + match theme_name { + None => Ok(Self::default()), + Some("default") | Some("retro") => Ok(Self::default()), + Some("dracula") => Ok(Self::dracula()), + Some(path) => { + // Try to load from file + if Path::new(path).exists() { + Self::from_file(path) + } else { + // Try to find in standard locations + let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; + let theme_file = home.join(".config").join("g3").join("themes").join(format!("{}.json", path)); + if theme_file.exists() { + Self::from_file(theme_file) + } else { + Err(anyhow::anyhow!("Theme '{}' not found", path)) + } + } + } + } + } +} + +/// Create example theme files in the user's config directory +pub fn create_example_themes() -> Result<()> { + let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; + let themes_dir = home.join(".config").join("g3").join("themes"); + + // Create directory if it doesn't exist + fs::create_dir_all(&themes_dir)?; + + // Save default theme + let default_theme = ColorTheme::default(); + default_theme.to_file(themes_dir.join("retro.json"))?; + + // Save Dracula theme + let dracula_theme = ColorTheme::dracula(); + dracula_theme.to_file(themes_dir.join("dracula.json"))?; + + // Create a custom example theme (Matrix-inspired) + let matrix_theme = ColorTheme { + name: "Matrix".to_string(), + terminal_green: ColorValue::Rgb { r: 0, g: 255, b: 0 }, + terminal_amber: ColorValue::Rgb { r: 0, g: 200, b: 0 }, + terminal_dim_green: ColorValue::Rgb { r: 0, g: 100, b: 0 }, + terminal_bg: ColorValue::Rgb { r: 0, g: 0, b: 0 }, + terminal_cyan: ColorValue::Rgb { r: 0, g: 255, b: 128 }, + terminal_red: ColorValue::Rgb { r: 255, g: 0, b: 0 }, + terminal_pale_blue: ColorValue::Rgb { r: 0, g: 255, b: 200 }, + terminal_dark_amber: ColorValue::Rgb { r: 0, g: 150, b: 0 }, + terminal_white: ColorValue::Rgb { r: 200, g: 255, b: 200 }, + }; + matrix_theme.to_file(themes_dir.join("matrix.json"))?; + + println!("Example theme files created in: {}", themes_dir.display()); + + Ok(()) +} \ No newline at end of file