Skip to main content

enya_plugin/
headless.rs

1use rustc_hash::FxHashMap;
2
3use crate::types::{
4    BoxFuture, CustomChartConfig, CustomChartData, CustomTableConfig, CustomTableData,
5    FocusedPaneInfo, GaugePaneConfig, GaugePaneData, HttpError, HttpResponse, LogLevel,
6    NotificationLevel, PluginHost, StatPaneConfig, StatPaneData, Theme,
7};
8
9/// A headless implementation of `PluginHost` for CLI use.
10///
11/// Provides real implementations for non-UI methods (HTTP, logging, notifications)
12/// and no-ops for UI-specific methods (panes, time range, custom visualizations).
13pub struct HeadlessPluginHost;
14
15impl PluginHost for HeadlessPluginHost {
16    fn notify(&self, level: NotificationLevel, message: &str) {
17        let prefix = match level {
18            NotificationLevel::Info => "info",
19            NotificationLevel::Warning => "warn",
20            NotificationLevel::Error => "error",
21        };
22        println!("[{prefix}] {message}");
23    }
24
25    fn request_repaint(&self) {
26        // No-op: no UI to repaint
27    }
28
29    fn log(&self, level: LogLevel, message: &str) {
30        match level {
31            LogLevel::Debug => log::debug!("{message}"),
32            LogLevel::Info => log::info!("{message}"),
33            LogLevel::Warn => log::warn!("{message}"),
34            LogLevel::Error => log::error!("{message}"),
35        }
36    }
37
38    fn version(&self) -> &'static str {
39        env!("CARGO_PKG_VERSION")
40    }
41
42    fn is_wasm(&self) -> bool {
43        false
44    }
45
46    fn theme(&self) -> Theme {
47        Theme::Dark
48    }
49
50    fn theme_name(&self) -> &'static str {
51        "dark"
52    }
53
54    fn clipboard_write(&self, _text: &str) -> bool {
55        false
56    }
57
58    fn clipboard_read(&self) -> Option<String> {
59        None
60    }
61
62    fn spawn(&self, _future: BoxFuture<()>) {
63        log::warn!("spawn() not available in headless mode");
64    }
65
66    fn http_get(
67        &self,
68        url: &str,
69        headers: &FxHashMap<String, String>,
70    ) -> Result<HttpResponse, HttpError> {
71        let mut request = ureq::get(url);
72        for (key, value) in headers {
73            request = request.header(key.as_str(), value.as_str());
74        }
75
76        match request.call() {
77            Ok(response) => {
78                let status = response.status().as_u16();
79                let mut response_headers = FxHashMap::default();
80                for (name, value) in response.headers().iter() {
81                    if let Ok(v) = value.to_str() {
82                        response_headers.insert(name.to_string(), v.to_string());
83                    }
84                }
85                match response.into_body().read_to_string() {
86                    Ok(body) => Ok(HttpResponse {
87                        status,
88                        body,
89                        headers: response_headers,
90                    }),
91                    Err(e) => Err(HttpError {
92                        message: format!("Failed to read response body: {e}"),
93                    }),
94                }
95            }
96            Err(e) => Err(HttpError {
97                message: format!("HTTP GET failed: {e}"),
98            }),
99        }
100    }
101
102    fn http_post(
103        &self,
104        url: &str,
105        body: &str,
106        headers: &FxHashMap<String, String>,
107    ) -> Result<HttpResponse, HttpError> {
108        let mut request = ureq::post(url);
109        for (key, value) in headers {
110            request = request.header(key.as_str(), value.as_str());
111        }
112        if !headers.contains_key("Content-Type") && !headers.contains_key("content-type") {
113            request = request.header("Content-Type", "application/json");
114        }
115
116        match request.send(body) {
117            Ok(response) => {
118                let status = response.status().as_u16();
119                let mut response_headers = FxHashMap::default();
120                for (name, value) in response.headers().iter() {
121                    if let Ok(v) = value.to_str() {
122                        response_headers.insert(name.to_string(), v.to_string());
123                    }
124                }
125                match response.into_body().read_to_string() {
126                    Ok(resp_body) => Ok(HttpResponse {
127                        status,
128                        body: resp_body,
129                        headers: response_headers,
130                    }),
131                    Err(e) => Err(HttpError {
132                        message: format!("Failed to read response body: {e}"),
133                    }),
134                }
135            }
136            Err(e) => Err(HttpError {
137                message: format!("HTTP POST failed: {e}"),
138            }),
139        }
140    }
141
142    // Pane management — no-ops in headless mode
143
144    fn add_query_pane(&self, _query: &str, _title: Option<&str>) {}
145    fn add_logs_pane(&self) {}
146    fn add_tracing_pane(&self, _trace_id: Option<&str>) {}
147    fn add_terminal_pane(&self) {}
148    fn add_sql_pane(&self) {}
149    fn close_focused_pane(&self) {}
150    fn focus_pane(&self, _direction: &str) {}
151
152    // Time range — no-ops / defaults
153
154    fn set_time_range_preset(&self, _preset: &str) {}
155    fn set_time_range_absolute(&self, _start_secs: f64, _end_secs: f64) {}
156    fn get_time_range(&self) -> (f64, f64) {
157        (0.0, 0.0)
158    }
159
160    // Custom panes — no-ops
161
162    fn register_custom_table_pane(&self, _config: CustomTableConfig) {}
163    fn add_custom_table_pane(&self, _pane_type: &str) {}
164    fn update_custom_table_data(&self, _pane_id: usize, _data: CustomTableData) {}
165    fn update_custom_table_data_by_type(&self, _pane_type: &str, _data: CustomTableData) {}
166
167    fn register_custom_chart_pane(&self, _config: CustomChartConfig) {}
168    fn add_custom_chart_pane(&self, _pane_type: &str) {}
169    fn update_custom_chart_data_by_type(&self, _pane_type: &str, _data: CustomChartData) {}
170
171    fn register_stat_pane(&self, _config: StatPaneConfig) {}
172    fn add_stat_pane(&self, _pane_type: &str) {}
173    fn update_stat_data_by_type(&self, _pane_type: &str, _data: StatPaneData) {}
174
175    fn register_gauge_pane(&self, _config: GaugePaneConfig) {}
176    fn add_gauge_pane(&self, _pane_type: &str) {}
177    fn update_gauge_data_by_type(&self, _pane_type: &str, _data: GaugePaneData) {}
178
179    fn get_focused_pane_info(&self) -> Option<FocusedPaneInfo> {
180        None
181    }
182}