Skip to main content

enya_plugin/
types.rs

1//! Core types for the plugin system.
2//!
3//! These types are designed to be independent of any specific editor implementation,
4//! allowing the plugin system to be used in different contexts.
5
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use std::collections::BTreeMap;
11
12use rustc_hash::FxHashMap;
13
14/// Notification level for user-facing messages.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum NotificationLevel {
17    Info,
18    Warning,
19    Error,
20}
21
22impl NotificationLevel {
23    /// Parse a notification level from a string.
24    pub fn parse(s: &str) -> Self {
25        match s {
26            "error" => Self::Error,
27            "warn" | "warning" => Self::Warning,
28            _ => Self::Info,
29        }
30    }
31}
32
33/// Log level for plugin logging.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum LogLevel {
36    Debug,
37    Info,
38    Warn,
39    Error,
40}
41
42impl LogLevel {
43    /// Parse a log level from a string (case-insensitive).
44    pub fn parse(s: &str) -> Self {
45        match s.to_ascii_lowercase().as_str() {
46            "debug" => Self::Debug,
47            "warn" | "warning" => Self::Warn,
48            "error" => Self::Error,
49            _ => Self::Info,
50        }
51    }
52}
53
54/// Application theme.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum Theme {
57    #[default]
58    Dark,
59    Light,
60    /// Custom theme identified by name
61    Custom,
62}
63
64/// A boxed future for async operations.
65pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
66
67/// HTTP response returned from http_get/http_post.
68#[derive(Debug, Clone)]
69pub struct HttpResponse {
70    /// HTTP status code (e.g., 200, 404, 500)
71    pub status: u16,
72    /// Response body as a string
73    pub body: String,
74    /// Response headers
75    pub headers: FxHashMap<String, String>,
76}
77
78/// HTTP request error.
79#[derive(Debug, Clone)]
80pub struct HttpError {
81    /// Error message
82    pub message: String,
83}
84
85// ==================== Custom Pane Types ====================
86
87/// Column configuration for a custom table pane.
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89pub struct TableColumnConfig {
90    /// Column header name
91    pub name: String,
92    /// Key to look up in row data (optional, defaults to name)
93    pub key: Option<String>,
94    /// Optional fixed width in pixels (as integer for Hash/Eq)
95    pub width: Option<u32>,
96}
97
98impl TableColumnConfig {
99    /// Create a new column configuration.
100    pub fn new(name: impl Into<String>) -> Self {
101        Self {
102            name: name.into(),
103            key: None,
104            width: None,
105        }
106    }
107
108    /// Set the data key for this column.
109    pub fn with_key(mut self, key: impl Into<String>) -> Self {
110        self.key = Some(key.into());
111        self
112    }
113
114    /// Set the width for this column (in pixels).
115    pub fn with_width(mut self, width: u32) -> Self {
116        self.width = Some(width);
117        self
118    }
119
120    /// Get the width as f32 for rendering.
121    pub fn width_f32(&self) -> Option<f32> {
122        self.width.map(|w| w as f32)
123    }
124
125    /// Get the key to use for looking up data (falls back to name).
126    pub fn data_key(&self) -> &str {
127        self.key.as_deref().unwrap_or(&self.name)
128    }
129}
130
131/// Configuration for a custom table pane type.
132#[derive(Debug, Clone, PartialEq, Eq, Hash)]
133pub struct CustomTableConfig {
134    /// Unique identifier for this pane type
135    pub name: String,
136    /// Display title for the pane
137    pub title: String,
138    /// Column definitions
139    pub columns: Vec<TableColumnConfig>,
140    /// Auto-refresh interval in seconds (0 = manual only)
141    pub refresh_interval: u32,
142    /// Plugin that registered this pane type
143    pub plugin_name: String,
144}
145
146/// A single row of data for a custom table.
147#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
148pub struct CustomTableRow {
149    /// Cell values keyed by column key (BTreeMap for deterministic ordering and Hash)
150    pub cells: BTreeMap<String, String>,
151}
152
153impl CustomTableRow {
154    /// Create a new empty row.
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Add a cell value.
160    pub fn with_cell(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
161        self.cells.insert(key.into(), value.into());
162        self
163    }
164
165    /// Get a cell value by key.
166    pub fn get(&self, key: &str) -> Option<&str> {
167        self.cells.get(key).map(|s| s.as_str())
168    }
169}
170
171/// Data returned by a custom table pane's fetch function.
172#[derive(Debug, Clone, PartialEq, Eq, Hash)]
173pub struct CustomTableData {
174    /// Rows of data
175    pub rows: Vec<CustomTableRow>,
176    /// Error message if fetch failed
177    pub error: Option<String>,
178}
179
180impl CustomTableData {
181    /// Create successful table data with rows.
182    pub fn with_rows(rows: Vec<CustomTableRow>) -> Self {
183        Self { rows, error: None }
184    }
185
186    /// Create error result.
187    pub fn with_error(message: impl Into<String>) -> Self {
188        Self {
189            rows: Vec::new(),
190            error: Some(message.into()),
191        }
192    }
193}
194
195// ==================== Custom Chart Pane Types ====================
196
197/// A single data point in a chart series.
198#[derive(Debug, Clone, PartialEq)]
199pub struct ChartDataPoint {
200    /// Unix timestamp in seconds
201    pub timestamp: f64,
202    /// Value at this timestamp
203    pub value: f64,
204}
205
206impl ChartDataPoint {
207    /// Create a new data point.
208    pub fn new(timestamp: f64, value: f64) -> Self {
209        Self { timestamp, value }
210    }
211}
212
213/// A single series in a chart (line).
214#[derive(Debug, Clone, PartialEq)]
215pub struct ChartSeries {
216    /// Display name for the series
217    pub name: String,
218    /// Tags/labels for the series (BTreeMap for deterministic ordering)
219    pub tags: BTreeMap<String, String>,
220    /// Data points in the series
221    pub points: Vec<ChartDataPoint>,
222}
223
224impl ChartSeries {
225    /// Create a new series with the given name.
226    pub fn new(name: impl Into<String>) -> Self {
227        Self {
228            name: name.into(),
229            tags: BTreeMap::new(),
230            points: Vec::new(),
231        }
232    }
233
234    /// Add a tag/label to the series.
235    pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
236        self.tags.insert(key.into(), value.into());
237        self
238    }
239
240    /// Add a data point to the series.
241    pub fn with_point(mut self, timestamp: f64, value: f64) -> Self {
242        self.points.push(ChartDataPoint::new(timestamp, value));
243        self
244    }
245
246    /// Add multiple data points to the series.
247    pub fn with_points(mut self, points: Vec<ChartDataPoint>) -> Self {
248        self.points.extend(points);
249        self
250    }
251}
252
253/// Configuration for a custom chart pane type.
254#[derive(Debug, Clone, PartialEq, Eq, Hash)]
255pub struct CustomChartConfig {
256    /// Unique identifier for this pane type
257    pub name: String,
258    /// Display title for the pane
259    pub title: String,
260    /// Unit label for the Y-axis (e.g., "ms", "bytes", "%")
261    pub y_unit: Option<String>,
262    /// Auto-refresh interval in seconds (0 = manual only)
263    pub refresh_interval: u32,
264    /// Plugin that registered this pane type
265    pub plugin_name: String,
266}
267
268/// Data to display in a custom chart pane.
269#[derive(Debug, Clone, PartialEq)]
270pub struct CustomChartData {
271    /// Series to display
272    pub series: Vec<ChartSeries>,
273    /// Error message if fetch failed
274    pub error: Option<String>,
275}
276
277impl CustomChartData {
278    /// Create chart data with series.
279    pub fn with_series(series: Vec<ChartSeries>) -> Self {
280        Self {
281            series,
282            error: None,
283        }
284    }
285
286    /// Create error result.
287    pub fn with_error(message: impl Into<String>) -> Self {
288        Self {
289            series: Vec::new(),
290            error: Some(message.into()),
291        }
292    }
293}
294
295// ==================== Custom Stat Pane Types ====================
296
297/// Threshold configuration for stat/gauge visualizations.
298#[derive(Debug, Clone, PartialEq)]
299pub struct ThresholdConfig {
300    /// Value at which this threshold applies
301    pub value: f64,
302    /// Color name: "green", "yellow", "red", "blue", or hex "#RRGGBB"
303    pub color: String,
304    /// Optional label for the threshold
305    pub label: Option<String>,
306}
307
308impl ThresholdConfig {
309    /// Create a new threshold.
310    pub fn new(value: f64, color: impl Into<String>) -> Self {
311        Self {
312            value,
313            color: color.into(),
314            label: None,
315        }
316    }
317
318    /// Set a label for this threshold.
319    pub fn with_label(mut self, label: impl Into<String>) -> Self {
320        self.label = Some(label.into());
321        self
322    }
323}
324
325/// Configuration for a custom stat pane type.
326#[derive(Debug, Clone, PartialEq, Eq, Hash)]
327pub struct StatPaneConfig {
328    /// Unique identifier for this pane type
329    pub name: String,
330    /// Display title for the pane
331    pub title: String,
332    /// Unit label (e.g., "jobs", "ms", "%")
333    pub unit: Option<String>,
334    /// Auto-refresh interval in seconds (0 = manual only)
335    pub refresh_interval: u32,
336    /// Plugin that registered this pane type
337    pub plugin_name: String,
338}
339
340/// Data to display in a stat pane.
341#[derive(Debug, Clone, PartialEq)]
342pub struct StatPaneData {
343    /// Current value to display
344    pub value: f64,
345    /// Sparkline data (recent history)
346    pub sparkline: Vec<f64>,
347    /// Change from previous period (percentage)
348    pub change_value: Option<f64>,
349    /// Description of change period (e.g., "vs last hour")
350    pub change_period: Option<String>,
351    /// Thresholds for coloring the value
352    pub thresholds: Vec<ThresholdConfig>,
353    /// Error message if fetch failed
354    pub error: Option<String>,
355}
356
357impl Default for StatPaneData {
358    fn default() -> Self {
359        Self {
360            value: 0.0,
361            sparkline: Vec::new(),
362            change_value: None,
363            change_period: None,
364            thresholds: Vec::new(),
365            error: None,
366        }
367    }
368}
369
370impl StatPaneData {
371    /// Create stat data with a value.
372    pub fn with_value(value: f64) -> Self {
373        Self {
374            value,
375            ..Default::default()
376        }
377    }
378
379    /// Create error result.
380    pub fn with_error(message: impl Into<String>) -> Self {
381        Self {
382            error: Some(message.into()),
383            ..Default::default()
384        }
385    }
386
387    /// Set sparkline data.
388    pub fn sparkline(mut self, data: Vec<f64>) -> Self {
389        self.sparkline = data;
390        self
391    }
392
393    /// Set change indicator.
394    pub fn change(mut self, value: f64, period: impl Into<String>) -> Self {
395        self.change_value = Some(value);
396        self.change_period = Some(period.into());
397        self
398    }
399
400    /// Add a threshold.
401    pub fn threshold(mut self, threshold: ThresholdConfig) -> Self {
402        self.thresholds.push(threshold);
403        self
404    }
405}
406
407// ==================== Focused Pane Info ====================
408
409/// Information about the currently focused pane.
410/// Used for sharing context to external services like Slack/Discord.
411#[derive(Debug, Clone, Default, PartialEq, Eq)]
412pub struct FocusedPaneInfo {
413    /// Type of the pane (e.g., "query", "logs", "tracing", "terminal", "sql", "custom_table")
414    pub pane_type: String,
415    /// Display title of the pane (if any)
416    pub title: Option<String>,
417    /// Query string (for query panes)
418    pub query: Option<String>,
419    /// Metric name (for query panes, extracted from PromQL)
420    pub metric_name: Option<String>,
421}
422
423impl FocusedPaneInfo {
424    /// Create a new focused pane info.
425    pub fn new(pane_type: impl Into<String>) -> Self {
426        Self {
427            pane_type: pane_type.into(),
428            title: None,
429            query: None,
430            metric_name: None,
431        }
432    }
433
434    /// Set the title.
435    pub fn with_title(mut self, title: impl Into<String>) -> Self {
436        self.title = Some(title.into());
437        self
438    }
439
440    /// Set the query.
441    pub fn with_query(mut self, query: impl Into<String>) -> Self {
442        self.query = Some(query.into());
443        self
444    }
445
446    /// Set the metric name.
447    pub fn with_metric_name(mut self, metric_name: impl Into<String>) -> Self {
448        self.metric_name = Some(metric_name.into());
449        self
450    }
451}
452
453// ==================== Custom Gauge Pane Types ====================
454
455/// Configuration for a custom gauge pane type.
456#[derive(Debug, Clone, PartialEq, Eq, Hash)]
457pub struct GaugePaneConfig {
458    /// Unique identifier for this pane type
459    pub name: String,
460    /// Display title for the pane
461    pub title: String,
462    /// Unit label (e.g., "%", "MB", "req/s")
463    pub unit: Option<String>,
464    /// Minimum value of the gauge range (stored as scaled i64 for Hash)
465    pub min_scaled: i64,
466    /// Maximum value of the gauge range (stored as scaled i64 for Hash)
467    pub max_scaled: i64,
468    /// Auto-refresh interval in seconds (0 = manual only)
469    pub refresh_interval: u32,
470    /// Plugin that registered this pane type
471    pub plugin_name: String,
472}
473
474/// Scale factor for converting f64 to i64 for hashable storage.
475const GAUGE_SCALE_FACTOR: f64 = 1_000_000.0;
476
477impl GaugePaneConfig {
478    /// Get minimum value as f64.
479    pub fn min(&self) -> f64 {
480        self.min_scaled as f64 / GAUGE_SCALE_FACTOR
481    }
482
483    /// Get maximum value as f64.
484    pub fn max(&self) -> f64 {
485        self.max_scaled as f64 / GAUGE_SCALE_FACTOR
486    }
487
488    /// Set range from f64 values.
489    /// Values are clamped to representable range to avoid overflow.
490    pub fn set_range(&mut self, min: f64, max: f64) {
491        self.min_scaled = scale_f64_to_i64(min);
492        self.max_scaled = scale_f64_to_i64(max);
493    }
494}
495
496/// Safely scale an f64 value to i64, clamping to representable range.
497/// Handles NaN, infinity, and values that would overflow after scaling.
498fn scale_f64_to_i64(value: f64) -> i64 {
499    if value.is_nan() {
500        return 0;
501    }
502    let scaled = value * GAUGE_SCALE_FACTOR;
503    if scaled >= i64::MAX as f64 {
504        i64::MAX
505    } else if scaled <= i64::MIN as f64 {
506        i64::MIN
507    } else {
508        scaled as i64
509    }
510}
511
512/// Data to display in a gauge pane.
513#[derive(Debug, Clone, PartialEq)]
514pub struct GaugePaneData {
515    /// Current value
516    pub value: f64,
517    /// Thresholds for coloring
518    pub thresholds: Vec<ThresholdConfig>,
519    /// Error message if fetch failed
520    pub error: Option<String>,
521}
522
523impl Default for GaugePaneData {
524    fn default() -> Self {
525        Self {
526            value: 0.0,
527            thresholds: Vec::new(),
528            error: None,
529        }
530    }
531}
532
533impl GaugePaneData {
534    /// Create gauge data with a value.
535    pub fn with_value(value: f64) -> Self {
536        Self {
537            value,
538            ..Default::default()
539        }
540    }
541
542    /// Create error result.
543    pub fn with_error(message: impl Into<String>) -> Self {
544        Self {
545            error: Some(message.into()),
546            ..Default::default()
547        }
548    }
549
550    /// Add a threshold.
551    pub fn threshold(mut self, threshold: ThresholdConfig) -> Self {
552        self.thresholds.push(threshold);
553        self
554    }
555}
556
557/// Trait for the host application to implement.
558///
559/// This provides the interface that plugins use to interact with the host
560/// (typically the Enya editor). The host implements this trait and provides
561/// it to plugins via the `PluginContext`.
562pub trait PluginHost: Send + Sync {
563    /// Send a notification to the user.
564    fn notify(&self, level: NotificationLevel, message: &str);
565
566    /// Request a UI repaint.
567    fn request_repaint(&self);
568
569    /// Log a message.
570    fn log(&self, level: LogLevel, message: &str);
571
572    /// Get the host application version.
573    fn version(&self) -> &'static str;
574
575    /// Check if running in WASM environment.
576    fn is_wasm(&self) -> bool;
577
578    /// Get the current theme.
579    fn theme(&self) -> Theme;
580
581    /// Get the current theme name as a string (e.g., "tokyo-night", "catppuccin").
582    fn theme_name(&self) -> &'static str;
583
584    /// Write text to the system clipboard.
585    /// Returns true if successful, false if clipboard is unavailable.
586    fn clipboard_write(&self, text: &str) -> bool;
587
588    /// Read text from the system clipboard.
589    /// Returns None if clipboard is empty or unavailable.
590    fn clipboard_read(&self) -> Option<String>;
591
592    /// Spawn an async task (may not be available in all environments).
593    fn spawn(&self, future: BoxFuture<()>);
594
595    /// Perform an HTTP GET request.
596    /// Returns the response or an error message.
597    fn http_get(
598        &self,
599        url: &str,
600        headers: &FxHashMap<String, String>,
601    ) -> Result<HttpResponse, HttpError>;
602
603    /// Perform an HTTP POST request.
604    /// Returns the response or an error message.
605    fn http_post(
606        &self,
607        url: &str,
608        body: &str,
609        headers: &FxHashMap<String, String>,
610    ) -> Result<HttpResponse, HttpError>;
611
612    // ==================== Pane Management ====================
613
614    /// Add a query pane with the given PromQL query and optional title.
615    fn add_query_pane(&self, query: &str, title: Option<&str>);
616
617    /// Add a logs pane.
618    fn add_logs_pane(&self);
619
620    /// Add a tracing pane, optionally pre-filled with a trace ID.
621    fn add_tracing_pane(&self, trace_id: Option<&str>);
622
623    /// Add a terminal pane (native only, no-op on WASM).
624    fn add_terminal_pane(&self);
625
626    /// Add a SQL pane.
627    fn add_sql_pane(&self);
628
629    /// Close the currently focused pane.
630    fn close_focused_pane(&self);
631
632    /// Focus pane in the given direction ("left", "right", "up", "down").
633    fn focus_pane(&self, direction: &str);
634
635    // ==================== Time Range ====================
636
637    /// Set time range to a preset (e.g., "5m", "15m", "1h", "6h", "24h", "7d").
638    fn set_time_range_preset(&self, preset: &str);
639
640    /// Set absolute time range (start and end in seconds since Unix epoch).
641    fn set_time_range_absolute(&self, start_secs: f64, end_secs: f64);
642
643    /// Get the current time range as (start_secs, end_secs).
644    /// Note: This returns cached values; may not reflect real-time updates.
645    fn get_time_range(&self) -> (f64, f64);
646
647    // ==================== Custom Panes ====================
648
649    /// Register a custom table pane type.
650    fn register_custom_table_pane(&self, config: CustomTableConfig);
651
652    /// Add an instance of a custom table pane.
653    fn add_custom_table_pane(&self, pane_type: &str);
654
655    /// Update data for a custom table pane by pane ID.
656    /// Called by plugins when they have new data to display.
657    fn update_custom_table_data(&self, pane_id: usize, data: CustomTableData);
658
659    /// Update data for all custom table panes of a given type.
660    /// Called by plugins when they have new data to display.
661    fn update_custom_table_data_by_type(&self, pane_type: &str, data: CustomTableData);
662
663    // ==================== Custom Chart Panes ====================
664
665    /// Register a custom chart pane type.
666    fn register_custom_chart_pane(&self, config: CustomChartConfig);
667
668    /// Add an instance of a custom chart pane.
669    fn add_custom_chart_pane(&self, pane_type: &str);
670
671    /// Update data for all custom chart panes of a given type.
672    fn update_custom_chart_data_by_type(&self, pane_type: &str, data: CustomChartData);
673
674    // ==================== Custom Stat Panes ====================
675
676    /// Register a custom stat pane type.
677    fn register_stat_pane(&self, config: StatPaneConfig);
678
679    /// Add an instance of a stat pane.
680    fn add_stat_pane(&self, pane_type: &str);
681
682    /// Update data for all stat panes of a given type.
683    fn update_stat_data_by_type(&self, pane_type: &str, data: StatPaneData);
684
685    // ==================== Custom Gauge Panes ====================
686
687    /// Register a custom gauge pane type.
688    fn register_gauge_pane(&self, config: GaugePaneConfig);
689
690    /// Add an instance of a gauge pane.
691    fn add_gauge_pane(&self, pane_type: &str);
692
693    /// Update data for all gauge panes of a given type.
694    fn update_gauge_data_by_type(&self, pane_type: &str, data: GaugePaneData);
695
696    // ==================== Focused Pane ====================
697
698    /// Get information about the currently focused pane.
699    /// Returns None if no pane is focused.
700    fn get_focused_pane_info(&self) -> Option<FocusedPaneInfo>;
701}
702
703/// Reference-counted plugin host.
704pub type PluginHostRef = Arc<dyn PluginHost>;
705
706/// Context provided to plugins for interacting with the host.
707pub struct PluginContext {
708    host: PluginHostRef,
709}
710
711impl PluginContext {
712    /// Create a new plugin context with the given host.
713    pub fn new(host: PluginHostRef) -> Self {
714        Self { host }
715    }
716
717    /// Send a notification to the user.
718    pub fn notify(&self, level: &str, message: &str) {
719        self.host.notify(NotificationLevel::parse(level), message);
720    }
721
722    /// Request a UI repaint.
723    pub fn request_repaint(&self) {
724        self.host.request_repaint();
725    }
726
727    /// Log a message.
728    pub fn log(&self, level: LogLevel, message: &str) {
729        self.host.log(level, message);
730    }
731
732    /// Get the host application version.
733    pub fn editor_version(&self) -> &'static str {
734        self.host.version()
735    }
736
737    /// Check if running in WASM environment.
738    pub fn is_wasm(&self) -> bool {
739        self.host.is_wasm()
740    }
741
742    /// Get the current theme.
743    pub fn theme(&self) -> Theme {
744        self.host.theme()
745    }
746
747    /// Get the current theme name as a string.
748    pub fn theme_name(&self) -> &'static str {
749        self.host.theme_name()
750    }
751
752    /// Write text to the system clipboard.
753    pub fn clipboard_write(&self, text: &str) -> bool {
754        self.host.clipboard_write(text)
755    }
756
757    /// Read text from the system clipboard.
758    pub fn clipboard_read(&self) -> Option<String> {
759        self.host.clipboard_read()
760    }
761
762    /// Spawn an async task.
763    pub fn spawn<F>(&self, future: F)
764    where
765        F: Future<Output = ()> + Send + 'static,
766    {
767        self.host.spawn(Box::pin(future));
768    }
769
770    /// Get a reference to the underlying host.
771    pub fn host(&self) -> &PluginHostRef {
772        &self.host
773    }
774
775    /// Perform an HTTP GET request.
776    pub fn http_get(
777        &self,
778        url: &str,
779        headers: &FxHashMap<String, String>,
780    ) -> Result<HttpResponse, HttpError> {
781        self.host.http_get(url, headers)
782    }
783
784    /// Perform an HTTP POST request.
785    pub fn http_post(
786        &self,
787        url: &str,
788        body: &str,
789        headers: &FxHashMap<String, String>,
790    ) -> Result<HttpResponse, HttpError> {
791        self.host.http_post(url, body, headers)
792    }
793
794    // ==================== Pane Management ====================
795
796    /// Add a query pane with the given PromQL query and optional title.
797    pub fn add_query_pane(&self, query: &str, title: Option<&str>) {
798        self.host.add_query_pane(query, title);
799    }
800
801    /// Add a logs pane.
802    pub fn add_logs_pane(&self) {
803        self.host.add_logs_pane();
804    }
805
806    /// Add a tracing pane, optionally pre-filled with a trace ID.
807    pub fn add_tracing_pane(&self, trace_id: Option<&str>) {
808        self.host.add_tracing_pane(trace_id);
809    }
810
811    /// Add a terminal pane (native only, no-op on WASM).
812    pub fn add_terminal_pane(&self) {
813        self.host.add_terminal_pane();
814    }
815
816    /// Add a SQL pane.
817    pub fn add_sql_pane(&self) {
818        self.host.add_sql_pane();
819    }
820
821    /// Close the currently focused pane.
822    pub fn close_focused_pane(&self) {
823        self.host.close_focused_pane();
824    }
825
826    /// Focus pane in the given direction ("left", "right", "up", "down").
827    pub fn focus_pane(&self, direction: &str) {
828        self.host.focus_pane(direction);
829    }
830
831    // ==================== Time Range ====================
832
833    /// Set time range to a preset (e.g., "5m", "15m", "1h", "6h", "24h", "7d").
834    pub fn set_time_range_preset(&self, preset: &str) {
835        self.host.set_time_range_preset(preset);
836    }
837
838    /// Set absolute time range (start and end in seconds since Unix epoch).
839    pub fn set_time_range_absolute(&self, start_secs: f64, end_secs: f64) {
840        self.host.set_time_range_absolute(start_secs, end_secs);
841    }
842
843    /// Get the current time range as (start_secs, end_secs).
844    pub fn get_time_range(&self) -> (f64, f64) {
845        self.host.get_time_range()
846    }
847
848    // ==================== Custom Panes ====================
849
850    /// Register a custom table pane type.
851    pub fn register_custom_table_pane(&self, config: CustomTableConfig) {
852        self.host.register_custom_table_pane(config);
853    }
854
855    /// Add an instance of a custom table pane.
856    pub fn add_custom_table_pane(&self, pane_type: &str) {
857        self.host.add_custom_table_pane(pane_type);
858    }
859
860    /// Update data for a custom table pane.
861    pub fn update_custom_table_data(&self, pane_id: usize, data: CustomTableData) {
862        self.host.update_custom_table_data(pane_id, data);
863    }
864
865    /// Update data for all custom table panes of a given type.
866    pub fn update_custom_table_data_by_type(&self, pane_type: &str, data: CustomTableData) {
867        self.host.update_custom_table_data_by_type(pane_type, data);
868    }
869
870    // ==================== Custom Chart Panes ====================
871
872    /// Register a custom chart pane type.
873    pub fn register_custom_chart_pane(&self, config: CustomChartConfig) {
874        self.host.register_custom_chart_pane(config);
875    }
876
877    /// Add an instance of a custom chart pane.
878    pub fn add_custom_chart_pane(&self, pane_type: &str) {
879        self.host.add_custom_chart_pane(pane_type);
880    }
881
882    /// Update data for all custom chart panes of a given type.
883    pub fn update_custom_chart_data_by_type(&self, pane_type: &str, data: CustomChartData) {
884        self.host.update_custom_chart_data_by_type(pane_type, data);
885    }
886
887    // ==================== Custom Stat Panes ====================
888
889    /// Register a custom stat pane type.
890    pub fn register_stat_pane(&self, config: StatPaneConfig) {
891        self.host.register_stat_pane(config);
892    }
893
894    /// Add an instance of a stat pane.
895    pub fn add_stat_pane(&self, pane_type: &str) {
896        self.host.add_stat_pane(pane_type);
897    }
898
899    /// Update data for all stat panes of a given type.
900    pub fn update_stat_data_by_type(&self, pane_type: &str, data: StatPaneData) {
901        self.host.update_stat_data_by_type(pane_type, data);
902    }
903
904    // ==================== Custom Gauge Panes ====================
905
906    /// Register a custom gauge pane type.
907    pub fn register_gauge_pane(&self, config: GaugePaneConfig) {
908        self.host.register_gauge_pane(config);
909    }
910
911    /// Add an instance of a gauge pane.
912    pub fn add_gauge_pane(&self, pane_type: &str) {
913        self.host.add_gauge_pane(pane_type);
914    }
915
916    /// Update data for all gauge panes of a given type.
917    pub fn update_gauge_data_by_type(&self, pane_type: &str, data: GaugePaneData) {
918        self.host.update_gauge_data_by_type(pane_type, data);
919    }
920
921    // ==================== Focused Pane ====================
922
923    /// Get information about the currently focused pane.
924    pub fn get_focused_pane_info(&self) -> Option<FocusedPaneInfo> {
925        self.host.get_focused_pane_info()
926    }
927}
928
929/// Reference-counted plugin context.
930pub type PluginContextRef = Arc<PluginContext>;
931
932#[cfg(test)]
933mod tests {
934    use super::*;
935
936    #[test]
937    fn test_notification_level_parse() {
938        assert_eq!(NotificationLevel::parse("error"), NotificationLevel::Error);
939        assert_eq!(NotificationLevel::parse("warn"), NotificationLevel::Warning);
940        assert_eq!(
941            NotificationLevel::parse("warning"),
942            NotificationLevel::Warning
943        );
944        assert_eq!(NotificationLevel::parse("info"), NotificationLevel::Info);
945        // Unknown values default to Info
946        assert_eq!(NotificationLevel::parse("unknown"), NotificationLevel::Info);
947        assert_eq!(NotificationLevel::parse(""), NotificationLevel::Info);
948        assert_eq!(NotificationLevel::parse("ERROR"), NotificationLevel::Info); // case sensitive
949    }
950
951    #[test]
952    fn test_log_level_parse() {
953        assert_eq!(LogLevel::parse("debug"), LogLevel::Debug);
954        assert_eq!(LogLevel::parse("info"), LogLevel::Info);
955        assert_eq!(LogLevel::parse("warn"), LogLevel::Warn);
956        assert_eq!(LogLevel::parse("warning"), LogLevel::Warn);
957        assert_eq!(LogLevel::parse("error"), LogLevel::Error);
958        // Unknown values default to Info
959        assert_eq!(LogLevel::parse("unknown"), LogLevel::Info);
960        assert_eq!(LogLevel::parse(""), LogLevel::Info);
961        // Case-insensitive parsing
962        assert_eq!(LogLevel::parse("DEBUG"), LogLevel::Debug);
963        assert_eq!(LogLevel::parse("WARN"), LogLevel::Warn);
964        assert_eq!(LogLevel::parse("Error"), LogLevel::Error);
965    }
966
967    #[test]
968    fn test_theme_default() {
969        assert_eq!(Theme::default(), Theme::Dark);
970    }
971
972    #[test]
973    fn test_http_response_clone() {
974        let mut headers = FxHashMap::default();
975        headers.insert("Content-Type".to_string(), "application/json".to_string());
976
977        let response = HttpResponse {
978            status: 200,
979            body: "test body".to_string(),
980            headers,
981        };
982
983        let cloned = response.clone();
984        assert_eq!(cloned.status, 200);
985        assert_eq!(cloned.body, "test body");
986        assert_eq!(
987            cloned.headers.get("Content-Type"),
988            Some(&"application/json".to_string())
989        );
990    }
991
992    #[test]
993    fn test_http_error_clone() {
994        let error = HttpError {
995            message: "Network error".to_string(),
996        };
997
998        let cloned = error.clone();
999        assert_eq!(cloned.message, "Network error");
1000    }
1001}