euroscope 0.0.1

Safe, idiomatic Rust interface for writing EuroScope plugins
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! Owned handles and iteration.
//!
//! EuroScope's `Select*`/`GetCorrelated*`/`Register*` methods return handles by
//! value; the shim heap-copies them so Rust can hold one past a single
//! statement. An owned handle frees its copy on drop and derefs to the borrowed
//! handle, so every query method is available transparently.

use std::ops::Deref;

use euroscope_sys::{EsHandle, PluginPtr};

use crate::{Context, SectorElementType, utils::cstr_lossy};

/// An owned handle wrapper: heap-frees on drop, derefs to the borrowed handle.
macro_rules! owned {
    ($owned:ident, $handle:ident, $free:ident) => {
        #[doc = concat!("An owned [`", stringify!($handle), "`](crate::", stringify!($handle),
                            ") (a heap copy). Frees on drop; derefs to the borrowed handle.")]
        pub struct $owned {
            inner: crate::$handle<'static>,
        }

        impl $owned {
            // Not every owned type is produced via a selector (some only via
            // iterators, which construct the struct directly), so this may be
            // unused for a given instantiation.
            #[allow(dead_code)]
            pub(crate) fn from_owned(raw: EsHandle) -> Option<Self> {
                (!raw.is_null()).then(|| Self {
                    inner: crate::$handle::from_raw(raw),
                })
            }
        }

        impl Deref for $owned {
            type Target = crate::$handle<'static>;

            fn deref(&self) -> &Self::Target {
                &self.inner
            }
        }

        #[expect(clippy::missing_trait_methods, reason = "We don't need pin_drop")]
        impl Drop for $owned {
            fn drop(&mut self) {
                // SAFETY: `inner` owns a heap handle the shim allocated for us.
                unsafe { euroscope_sys::$free(self.inner.as_ptr()) }
            }
        }
    };
}

/// An iterator over a `SelectFirst`/`SelectNext(current)` sequence, yielding
/// owned handles.
macro_rules! iter {
    ($iter:ident, $owned:ident, $handle:ident, $free:ident, $first:ident, $next:ident) => {
        #[doc = concat!("Iterator over all `", stringify!($handle),
                                                                    "`s, yielding owned handles.")]
        pub struct $iter {
            plugin: PluginPtr,
            next: Option<EsHandle>,
        }

        impl $iter {
            pub(crate) fn new(plugin: PluginPtr) -> Self {
                // SAFETY: `plugin` is EuroScope's live CPlugIn*.
                let first = unsafe { euroscope_sys::$first(plugin) };
                Self {
                    plugin,
                    next: (!first.is_null()).then_some(first),
                }
            }
        }

        #[expect(
            clippy::missing_trait_methods,
            reason = "We don't need the extra methods for this type"
        )]
        impl Iterator for $iter {
            type Item = $owned;

            fn next(&mut self) -> Option<$owned> {
                let cur = self.next.take()?;
                // Advance before yielding `cur`: SelectNext needs `cur` alive,
                // and `cur` is then handed to the yielded owned handle.
                // SAFETY: `plugin`/`cur` live; shim returns a heap copy or null.
                let following = unsafe { euroscope_sys::$next(self.plugin, cur) };
                self.next = (!following.is_null()).then_some(following);
                Some($owned {
                    inner: crate::$handle::from_raw(cur),
                })
            }
        }

        #[expect(clippy::missing_trait_methods, reason = "We don't need pin_drop")]
        impl Drop for $iter {
            fn drop(&mut self) {
                if let Some(p) = self.next.take() {
                    // SAFETY: `p` is a heap handle the shim allocated.
                    unsafe { euroscope_sys::$free(p) }
                }
            }
        }
    };
}

owned!(OwnedFlightPlan, FlightPlan, es_flightplan_free);
iter!(
    FlightPlans,
    OwnedFlightPlan,
    FlightPlan,
    es_flightplan_free,
    es_plugin_flightplan_select_first,
    es_plugin_flightplan_select_next
);

owned!(OwnedRadarTarget, RadarTarget, es_radartarget_free);
iter!(
    RadarTargets,
    OwnedRadarTarget,
    RadarTarget,
    es_radartarget_free,
    es_plugin_radartarget_select_first,
    es_plugin_radartarget_select_next
);

owned!(OwnedController, Controller, es_controller_free);
iter!(
    Controllers,
    OwnedController,
    Controller,
    es_controller_free,
    es_plugin_controller_select_first,
    es_plugin_controller_select_next
);

owned!(OwnedFlightPlanList, FlightPlanList, es_fplist_free);

owned!(
    OwnedGroundToAirChannel,
    GroundToAirChannel,
    es_gtachannel_free
);
iter!(
    GroundToAirChannels,
    OwnedGroundToAirChannel,
    GroundToAirChannel,
    es_gtachannel_free,
    es_plugin_gtachannel_select_first,
    es_plugin_gtachannel_select_next
);

owned!(OwnedSectorElement, SectorElement, es_sectorelement_free);

/// Iterator over sector-file elements of a given [`SectorElementType`], yielding
/// owned handles. Its selectors carry the element-type filter, so it does not
/// use the generic iterator macro.
pub struct SectorElements {
    plugin: PluginPtr,
    element_type: i32,
    next: Option<EsHandle>,
}

impl SectorElements {
    pub(crate) fn new(plugin: PluginPtr, element_type: i32) -> Self {
        // SAFETY: `plugin` is EuroScope's live CPlugIn*.
        let first =
            unsafe { euroscope_sys::es_plugin_sectorelement_select_first(plugin, element_type) };
        Self {
            plugin,
            element_type,
            next: (!first.is_null()).then_some(first),
        }
    }
}

#[expect(
    clippy::missing_trait_methods,
    reason = "We don't need the extra methods for this type"
)]
impl Iterator for SectorElements {
    type Item = OwnedSectorElement;

    fn next(&mut self) -> Option<OwnedSectorElement> {
        let cur = self.next.take()?;
        // SAFETY: `plugin`/`cur` live; shim returns a heap copy or null.
        let following = unsafe {
            euroscope_sys::es_plugin_sectorelement_select_next(self.plugin, cur, self.element_type)
        };
        self.next = (!following.is_null()).then_some(following);
        Some(OwnedSectorElement {
            inner: crate::SectorElement::from_raw(cur),
        })
    }
}

#[expect(clippy::missing_trait_methods, reason = "We don't need pin_drop")]
impl Drop for SectorElements {
    fn drop(&mut self) {
        if let Some(p) = self.next.take() {
            // SAFETY: `p` is a heap handle the shim allocated.
            unsafe { euroscope_sys::es_sectorelement_free(p) }
        }
    }
}

owned!(
    OwnedRadarTargetPosition,
    RadarTargetPosition,
    es_rtposdata_free
);

/// Iterator over a radar target's position history (newest first, up to the ~20
/// snapshots EuroScope keeps), yielding owned position snapshots.
pub struct PositionHistory {
    radar_target: EsHandle,
    next: Option<EsHandle>,
}

impl PositionHistory {
    pub(crate) fn new(radar_target: EsHandle) -> Self {
        // SAFETY: `radar_target` is a live CRadarTarget*.
        let first = unsafe { euroscope_sys::es_radartarget_current_position(radar_target) };
        Self {
            radar_target,
            next: (!first.is_null()).then_some(first),
        }
    }
}

#[expect(
    clippy::missing_trait_methods,
    reason = "We don't need the extra methods for this type"
)]
impl Iterator for PositionHistory {
    type Item = OwnedRadarTargetPosition;

    fn next(&mut self) -> Option<OwnedRadarTargetPosition> {
        let cur = self.next.take()?;
        // GetPreviousPosition needs `cur` alive; it is (about to be yielded).
        // SAFETY: `radar_target`/`cur` are live; shim returns a heap copy or null.
        let following =
            unsafe { euroscope_sys::es_radartarget_previous_position(self.radar_target, cur) };
        self.next = (!following.is_null()).then_some(following);
        Some(OwnedRadarTargetPosition {
            inner: crate::RadarTargetPosition::from_raw(cur),
        })
    }
}

#[expect(clippy::missing_trait_methods, reason = "We don't need pin_drop")]
impl Drop for PositionHistory {
    fn drop(&mut self) {
        if let Some(p) = self.next.take() {
            // SAFETY: `p` is an owned position snapshot from the shim.
            unsafe { euroscope_sys::es_rtposdata_free(p) }
        }
    }
}

/// Selection, iteration, registration and settings — the parts of `CPlugIn`
/// that produce owned handles or scalars.
impl Context {
    /// Iterate over all flight plans.
    pub fn flight_plans(&self) -> FlightPlans {
        FlightPlans::new(self.as_ptr())
    }

    /// Iterate over all radar targets.
    pub fn radar_targets(&self) -> RadarTargets {
        RadarTargets::new(self.as_ptr())
    }

    /// Iterate over all online controllers.
    pub fn controllers(&self) -> Controllers {
        Controllers::new(self.as_ptr())
    }

    /// Iterate over all voice ground-to-air channels.
    pub fn ground_to_air_channels(&self) -> GroundToAirChannels {
        GroundToAirChannels::new(self.as_ptr())
    }

    /// Iterate over sector-file elements of the given type. Pass
    /// [`SectorElementType::All`] for every element.
    pub fn sector_file_elements(&self, element_type: SectorElementType) -> SectorElements {
        SectorElements::new(self.as_ptr(), element_type.to_raw())
    }

    /// Select a flight plan by callsign.
    pub fn flight_plan(&self, callsign: &str) -> Option<OwnedFlightPlan> {
        // SAFETY: live CPlugIn*; the CString outlives the call.
        OwnedFlightPlan::from_owned(unsafe {
            euroscope_sys::es_plugin_flightplan_select(self.as_ptr(), cstr_lossy(callsign).as_ptr())
        })
    }

    /// Select a radar target by callsign.
    pub fn radar_target(&self, callsign: &str) -> Option<OwnedRadarTarget> {
        // SAFETY: live CPlugIn*; the CString outlives the call.
        OwnedRadarTarget::from_owned(unsafe {
            euroscope_sys::es_plugin_radartarget_select(
                self.as_ptr(),
                cstr_lossy(callsign).as_ptr(),
            )
        })
    }

    /// Select a controller by callsign.
    pub fn controller(&self, callsign: &str) -> Option<OwnedController> {
        // SAFETY: live CPlugIn*; the CString outlives the call.
        OwnedController::from_owned(unsafe {
            euroscope_sys::es_plugin_controller_select(self.as_ptr(), cstr_lossy(callsign).as_ptr())
        })
    }

    /// Select a controller by position ID.
    pub fn controller_by_position_id(&self, position_id: &str) -> Option<OwnedController> {
        // SAFETY: live CPlugIn*; the CString outlives the call.
        OwnedController::from_owned(unsafe {
            euroscope_sys::es_plugin_controller_select_by_position_id(
                self.as_ptr(),
                cstr_lossy(position_id).as_ptr(),
            )
        })
    }

    /// The logged-in controller ("myself") as a handle. See also
    /// [`my_position`](Self::my_position) for an owned snapshot.
    pub fn controller_myself(&self) -> Option<OwnedController> {
        // SAFETY: live CPlugIn*.
        OwnedController::from_owned(unsafe {
            euroscope_sys::es_plugin_controller_myself(self.as_ptr())
        })
    }

    /// The currently ASEL (selected) flight plan.
    pub fn selected_flight_plan(&self) -> Option<OwnedFlightPlan> {
        // SAFETY: live CPlugIn*.
        OwnedFlightPlan::from_owned(unsafe {
            euroscope_sys::es_plugin_flightplan_select_asel(self.as_ptr())
        })
    }

    /// The currently ASEL (selected) radar target.
    pub fn selected_radar_target(&self) -> Option<OwnedRadarTarget> {
        // SAFETY: live CPlugIn*.
        OwnedRadarTarget::from_owned(unsafe {
            euroscope_sys::es_plugin_radartarget_select_asel(self.as_ptr())
        })
    }

    /// Set the ASEL (selected) aircraft to the given flight plan.
    pub fn set_asel_flight_plan(&self, flight_plan: crate::FlightPlan<'_>) {
        // SAFETY: live CPlugIn*; the handle is valid for this call.
        unsafe { euroscope_sys::es_plugin_set_asel_flightplan(self.as_ptr(), flight_plan.as_ptr()) }
    }

    /// Set the ASEL (selected) aircraft to the given radar target.
    pub fn set_asel_radar_target(&self, radar_target: crate::RadarTarget<'_>) {
        // SAFETY: live CPlugIn*; the handle is valid for this call.
        unsafe {
            euroscope_sys::es_plugin_set_asel_radartarget(self.as_ptr(), radar_target.as_ptr());
        }
    }

    /// Register a new flight-plan list (appears in EuroScope) and return an
    /// owned handle. Keep it in your plugin to add columns and feed aircraft;
    /// [`Plugin::on_refresh_fp_list`](crate::Plugin::on_refresh_fp_list) fires
    /// when it needs content.
    pub fn register_fp_list(&self, name: &str) -> Option<OwnedFlightPlanList> {
        // SAFETY: live CPlugIn*; the CString outlives the call.
        OwnedFlightPlanList::from_owned(unsafe {
            euroscope_sys::es_plugin_register_fp_list(self.as_ptr(), cstr_lossy(name).as_ptr())
        })
    }

    /// The current transition altitude, in feet.
    pub fn transition_altitude(&self) -> i32 {
        // SAFETY: live CPlugIn*.
        unsafe { euroscope_sys::es_plugin_transition_altitude(self.as_ptr()) }
    }

    /// Read a persisted plugin setting by key. `None` if unset/empty.
    pub fn get_setting(&self, key: &str) -> Option<String> {
        // SAFETY: live CPlugIn*; the CString outlives the call; the returned
        // pointer is a borrowed NUL-terminated string we copy immediately.
        unsafe {
            let ptr = euroscope_sys::es_plugin_get_data_from_settings(
                self.as_ptr(),
                cstr_lossy(key).as_ptr(),
            );
            let s = crate::utils::cstr(ptr);
            (!s.is_empty()).then(|| s.to_owned())
        }
    }

    /// Persist a plugin setting (saved into the EuroScope settings file).
    pub fn save_setting(&self, key: &str, description: &str, value: &str) {
        // SAFETY: live CPlugIn*; the CStrings outlive the call.
        unsafe {
            euroscope_sys::es_plugin_save_data_to_settings(
                self.as_ptr(),
                cstr_lossy(key).as_ptr(),
                cstr_lossy(description).as_ptr(),
                cstr_lossy(value).as_ptr(),
            );
        }
    }
}