hyperion/api/json/
message.rs

1use std::{
2    path::PathBuf,
3    process::{Command, Stdio},
4};
5
6use serde_derive::{Deserialize, Serialize};
7use validator::Validate;
8
9use crate::{api::types::PriorityInfo, component::ComponentName, models::Color as RgbColor};
10
11/// Change color adjustement values
12#[derive(Debug, Deserialize, Validate)]
13pub struct Adjustment {
14    #[validate(nested)]
15    pub adjustment: ChannelAdjustment,
16}
17
18#[derive(Debug, Serialize, Deserialize, Validate)]
19#[serde(rename_all = "camelCase")]
20pub struct ChannelAdjustment {
21    pub id: Option<String>,
22    pub white: RgbColor,
23    pub red: RgbColor,
24    pub green: RgbColor,
25    pub blue: RgbColor,
26    pub cyan: RgbColor,
27    pub magenta: RgbColor,
28    pub yellow: RgbColor,
29    #[validate(range(min = 0, max = 100))]
30    pub backlight_threshold: u32,
31    pub backlight_colored: bool,
32    #[validate(range(min = 0, max = 100))]
33    pub brightness: u32,
34    #[validate(range(min = 0, max = 100))]
35    pub brightness_compensation: u32,
36    #[validate(range(min = 0.1, max = 5.0))]
37    pub gamma_red: f32,
38    #[validate(range(min = 0.1, max = 5.0))]
39    pub gamma_green: f32,
40    #[validate(range(min = 0.1, max = 5.0))]
41    pub gamma_blue: f32,
42}
43
44impl From<crate::models::ChannelAdjustment> for ChannelAdjustment {
45    fn from(adj: crate::models::ChannelAdjustment) -> Self {
46        Self {
47            id: Some(adj.id),
48            white: adj.white,
49            red: adj.red,
50            green: adj.green,
51            blue: adj.blue,
52            cyan: adj.cyan,
53            magenta: adj.magenta,
54            yellow: adj.yellow,
55            backlight_threshold: adj.backlight_threshold,
56            backlight_colored: adj.backlight_colored,
57            brightness: adj.brightness,
58            brightness_compensation: adj.brightness_compensation,
59            gamma_red: adj.gamma_red,
60            gamma_green: adj.gamma_green,
61            gamma_blue: adj.gamma_blue,
62        }
63    }
64}
65
66#[derive(Debug, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub enum AuthorizeCommand {
69    RequestToken,
70    CreateToken,
71    RenameToken,
72    DeleteToken,
73    GetTokenList,
74    Logout,
75    Login,
76    TokenRequired,
77    AdminRequired,
78    NewPasswordRequired,
79    NewPassword,
80    AnswerRequest,
81    GetPendingTokenRequests,
82}
83
84#[derive(Debug, Deserialize, Validate)]
85#[serde(rename_all = "camelCase")]
86pub struct Authorize {
87    pub subcommand: AuthorizeCommand,
88    #[validate(length(min = 8))]
89    pub password: Option<String>,
90    #[validate(length(min = 8))]
91    pub new_password: Option<String>,
92    #[validate(length(min = 36))]
93    pub token: Option<String>,
94    #[validate(length(min = 5))]
95    pub comment: Option<String>,
96    #[validate(length(min = 5, max = 5))]
97    pub id: Option<String>,
98    pub accept: Option<bool>,
99}
100
101#[derive(Debug, Deserialize, Validate)]
102pub struct Clear {
103    #[validate(range(min = -1, max = 253))]
104    pub priority: i32,
105}
106
107#[derive(Debug, Deserialize, Validate)]
108pub struct Color {
109    #[validate(range(min = 1, max = 253))]
110    pub priority: i32,
111    /// Duration in miliseconds
112    #[validate(range(min = 0))]
113    pub duration: Option<i32>,
114    /// Origin for the command
115    #[validate(length(min = 4, max = 20))]
116    pub origin: Option<String>,
117    pub color: RgbColor,
118}
119
120#[derive(Debug, Deserialize)]
121pub struct ComponentStatus {
122    pub component: ComponentName,
123    pub state: bool,
124}
125
126#[derive(Debug, Deserialize, Validate)]
127pub struct ComponentState {
128    pub componentstate: ComponentStatus,
129}
130
131#[derive(Debug, Deserialize)]
132#[serde(rename_all = "lowercase")]
133pub enum ConfigCommand {
134    SetConfig,
135    GetConfig,
136    GetSchema,
137    Reload,
138}
139
140#[derive(Debug, Deserialize, Validate)]
141pub struct Config {
142    pub subcommand: ConfigCommand,
143    #[serde(default)]
144    pub config: serde_json::Map<String, serde_json::Value>,
145}
146
147#[derive(Debug, Deserialize)]
148pub struct ImageData(#[serde(deserialize_with = "crate::serde::from_base64")] pub Vec<u8>);
149
150#[derive(Debug, Deserialize, Validate)]
151#[serde(rename_all = "camelCase")]
152pub struct EffectCreate {
153    pub name: String,
154    pub script: String,
155    pub args: serde_json::Map<String, serde_json::Value>,
156    pub image_data: Option<ImageData>,
157}
158
159#[derive(Debug, Deserialize, Validate)]
160#[serde(rename_all = "camelCase")]
161pub struct EffectDelete {
162    pub name: String,
163}
164
165#[derive(Debug, Deserialize)]
166pub struct EffectRequest {
167    /// Effect name
168    pub name: String,
169    /// Effect parameters
170    #[serde(default)]
171    pub args: serde_json::Map<String, serde_json::Value>,
172}
173
174/// Trigger an effect by name
175#[derive(Debug, Deserialize, Validate)]
176#[serde(rename_all = "camelCase")]
177pub struct Effect {
178    #[validate(range(min = 1, max = 253))]
179    pub priority: i32,
180    #[validate(range(min = 0))]
181    pub duration: Option<i32>,
182    #[validate(length(min = 4, max = 20))]
183    pub origin: Option<String>,
184    pub effect: EffectRequest,
185    pub python_script: Option<String>,
186    pub image_data: Option<ImageData>,
187}
188
189#[derive(Debug, Deserialize)]
190#[serde(rename_all = "lowercase")]
191#[derive(Default)]
192pub enum ImageFormat {
193    #[default]
194    Auto,
195}
196
197#[derive(Debug, Deserialize, Validate)]
198pub struct Image {
199    #[validate(range(min = 1, max = 253))]
200    pub priority: i32,
201    #[validate(length(min = 4, max = 20))]
202    pub origin: Option<String>,
203    #[validate(range(min = 0))]
204    pub duration: Option<i32>,
205    pub imagewidth: u32,
206    pub imageheight: u32,
207    #[serde(deserialize_with = "crate::serde::from_base64")]
208    pub imagedata: Vec<u8>,
209    #[serde(default)]
210    pub format: ImageFormat,
211    #[validate(range(min = 25, max = 2000))]
212    pub scale: Option<i32>,
213    pub name: Option<String>,
214}
215
216#[derive(Debug, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub enum InstanceCommand {
219    CreateInstance,
220    DeleteInstance,
221    StartInstance,
222    StopInstance,
223    SaveName,
224    SwitchTo,
225}
226
227#[derive(Debug, Deserialize, Validate)]
228pub struct Instance {
229    pub subcommand: InstanceCommand,
230    #[validate(range(min = 0, max = 255))]
231    pub instance: Option<i32>,
232    #[validate(length(min = 5))]
233    pub name: Option<String>,
234}
235
236#[derive(Debug, Deserialize)]
237#[serde(rename_all = "lowercase")]
238pub enum LedColorsSubcommand {
239    #[serde(rename = "ledstream-stop")]
240    LedStreamStop,
241    #[serde(rename = "ledstream-start")]
242    LedStreamStart,
243    TestLed,
244    #[serde(rename = "imagestream-start")]
245    ImageStreamStart,
246    #[serde(rename = "imagestream-stop")]
247    ImageStreamStop,
248}
249
250#[derive(Debug, Deserialize, Validate)]
251pub struct LedColors {
252    pub subcommand: LedColorsSubcommand,
253    pub oneshot: Option<bool>,
254    #[validate(range(min = 50))]
255    pub interval: Option<u32>,
256}
257
258#[derive(Debug, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub enum LedDeviceCommand {
261    Discover,
262    GetProperties,
263    Identify,
264}
265
266#[derive(Debug, Deserialize, Validate)]
267pub struct LedDevice {
268    pub subcommand: LedDeviceCommand,
269    pub led_device_type: String,
270    pub params: Option<serde_json::Map<String, serde_json::Value>>,
271}
272
273#[derive(Debug, Deserialize)]
274#[serde(rename_all = "lowercase")]
275pub enum LoggingCommand {
276    Stop,
277    Start,
278    Update,
279}
280
281#[derive(Debug, Deserialize, Validate)]
282pub struct Logging {
283    pub subcommand: LoggingCommand,
284    pub oneshot: Option<bool>,
285    pub interval: Option<u32>,
286}
287
288#[derive(Debug, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum MappingType {
291    MulticolorMean,
292    UnicolorMean,
293}
294
295#[derive(Debug, Deserialize, Validate)]
296#[serde(rename_all = "camelCase")]
297pub struct Processing {
298    pub mapping_type: MappingType,
299}
300
301#[derive(Debug, Deserialize, Validate)]
302pub struct ServerInfoRequest {
303    pub subscribe: Option<Vec<serde_json::Value>>,
304}
305
306#[derive(Debug, Deserialize, Validate)]
307pub struct SourceSelect {
308    #[validate(range(min = 0, max = 255))]
309    pub priority: i32,
310    pub auto: Option<bool>,
311}
312
313#[derive(Debug, Serialize, Deserialize)]
314pub enum VideoMode {
315    #[serde(rename = "2D")]
316    Mode2D,
317    #[serde(rename = "3DSBS")]
318    Mode3DSBS,
319    #[serde(rename = "3DTAB")]
320    Mode3DTAB,
321}
322
323#[derive(Debug, Deserialize, Validate)]
324#[serde(rename_all = "camelCase")]
325pub struct VideoModeRequest {
326    pub video_mode: VideoMode,
327}
328
329/// Incoming Hyperion JSON command
330#[derive(Debug, Deserialize)]
331#[serde(rename_all = "lowercase", tag = "command")]
332pub enum HyperionCommand {
333    Adjustment(Adjustment),
334    Authorize(Authorize),
335    Clear(Clear),
336    /// Deprecated
337    ClearAll,
338    Color(Color),
339    ComponentState(ComponentState),
340    Config(Config),
341    #[serde(rename = "create-effect")]
342    EffectCreate(EffectCreate),
343    #[serde(rename = "delete-effect")]
344    EffectDelete(EffectDelete),
345    Effect(Effect),
346    Image(Image),
347    Instance(Instance),
348    LedColors(LedColors),
349    LedDevice(LedDevice),
350    Logging(Logging),
351    Processing(Processing),
352    ServerInfo(ServerInfoRequest),
353    SourceSelect(SourceSelect),
354    SysInfo,
355    VideoMode(VideoModeRequest),
356}
357
358/// Incoming Hyperion JSON message
359#[derive(Debug, Deserialize)]
360pub struct HyperionMessage {
361    /// Request identifier
362    pub tan: Option<i32>,
363    #[serde(flatten)]
364    pub command: HyperionCommand,
365}
366
367impl Validate for HyperionMessage {
368    fn validate(&self) -> Result<(), validator::ValidationErrors> {
369        match &self.command {
370            HyperionCommand::Adjustment(adjustment) => adjustment.validate(),
371            HyperionCommand::Authorize(authorize) => authorize.validate(),
372            HyperionCommand::Clear(clear) => clear.validate(),
373            HyperionCommand::ClearAll => Ok(()),
374            HyperionCommand::Color(color) => color.validate(),
375            HyperionCommand::ComponentState(component_state) => component_state.validate(),
376            HyperionCommand::Config(config) => config.validate(),
377            HyperionCommand::EffectCreate(effect_create) => effect_create.validate(),
378            HyperionCommand::EffectDelete(effect_delete) => effect_delete.validate(),
379            HyperionCommand::Effect(effect) => effect.validate(),
380            HyperionCommand::Image(image) => image.validate(),
381            HyperionCommand::Instance(instance) => instance.validate(),
382            HyperionCommand::LedColors(led_colors) => led_colors.validate(),
383            HyperionCommand::LedDevice(led_device) => led_device.validate(),
384            HyperionCommand::Logging(logging) => logging.validate(),
385            HyperionCommand::Processing(processing) => processing.validate(),
386            HyperionCommand::ServerInfo(server_info) => server_info.validate(),
387            HyperionCommand::SourceSelect(source_select) => source_select.validate(),
388            HyperionCommand::SysInfo => Ok(()),
389            HyperionCommand::VideoMode(video_mode) => video_mode.validate(),
390        }
391    }
392}
393
394/// Effect definition details
395#[derive(Clone, Debug, Serialize, Deserialize)]
396pub struct EffectDefinition {
397    /// User-friendly name of the effect
398    pub name: String,
399    /// Path to the effect definition file
400    pub file: String,
401    /// Path to the script to run
402    pub script: String,
403    /// Extra script arguments
404    pub args: serde_json::Value,
405}
406
407impl From<&crate::effects::EffectDefinition> for EffectDefinition {
408    fn from(value: &crate::effects::EffectDefinition) -> Self {
409        Self {
410            name: value.name.clone(),
411            file: value.file.to_string_lossy().to_string(),
412            script: value.script.clone(),
413            args: value.args.clone(),
414        }
415    }
416}
417
418#[derive(Debug, Clone, Copy, Serialize)]
419pub enum LedDeviceClass {
420    Dummy,
421    PhilipsHue,
422    #[serde(rename = "Ws2812SPI")]
423    Ws2812Spi,
424    #[serde(rename = "file")]
425    File,
426}
427
428#[derive(Debug, Serialize)]
429pub struct LedDevicesInfo {
430    pub available: Vec<LedDeviceClass>,
431}
432
433impl Default for LedDevicesInfo {
434    fn default() -> Self {
435        Self::new()
436    }
437}
438
439impl LedDevicesInfo {
440    pub fn new() -> Self {
441        use LedDeviceClass::*;
442
443        Self {
444            available: vec![Dummy, PhilipsHue, Ws2812Spi, File],
445        }
446    }
447}
448
449#[derive(Debug, Clone)]
450pub enum GrabberClass {
451    AmLogic,
452    DirectX,
453    Dispmanx,
454    Framebuffer,
455    OSX,
456    Qt,
457    V4L2 { device: PathBuf },
458    X11,
459    Xcb,
460}
461
462impl std::fmt::Display for GrabberClass {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        match self {
465            GrabberClass::AmLogic => write!(f, "AmLogic"),
466            GrabberClass::DirectX => write!(f, "DirectX"),
467            GrabberClass::Dispmanx => write!(f, "Dispmanx"),
468            GrabberClass::Framebuffer => write!(f, "FrameBuffer"),
469            GrabberClass::OSX => write!(f, "OSX FrameGrabber"),
470            GrabberClass::Qt => write!(f, "Qt"),
471            GrabberClass::V4L2 { device } => write!(f, "V4L2:{}", device.display()),
472            GrabberClass::X11 => write!(f, "X11"),
473            GrabberClass::Xcb => write!(f, "Xcb"),
474        }
475    }
476}
477
478impl serde::ser::Serialize for GrabberClass {
479    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
480    where
481        S: serde::Serializer,
482    {
483        serializer.serialize_str(&self.to_string())
484    }
485}
486
487#[derive(Debug, Serialize)]
488pub struct GrabbersInfo {
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub active: Option<GrabberClass>,
491    pub available: Vec<GrabberClass>,
492}
493
494impl Default for GrabbersInfo {
495    fn default() -> Self {
496        Self::new()
497    }
498}
499
500impl GrabbersInfo {
501    pub fn new() -> Self {
502        Self {
503            // TODO: Report active grabber
504            active: None,
505            // TODO: Add grabbers when they are implemented
506            available: vec![], // TODO: Add v4l2_properties for available V4L2 devices
507        }
508    }
509}
510
511/// Hyperion server info
512#[derive(Debug, Serialize)]
513pub struct ServerInfo {
514    /// Priority information
515    pub priorities: Vec<PriorityInfo>,
516    pub priorities_autoselect: bool,
517    /// Color adjustment information
518    pub adjustment: Vec<ChannelAdjustment>,
519    /// Effect information
520    pub effects: Vec<EffectDefinition>,
521    /// LED device information
522    #[serde(rename = "ledDevices")]
523    pub led_devices: LedDevicesInfo,
524    /// Grabber information
525    pub grabbers: GrabbersInfo,
526    /// Current video mode
527    #[serde(rename = "videomode")]
528    pub video_mode: VideoMode,
529    // TODO: components field
530    // TODO: imageToLedMappingType field
531    // TODO: sessions field
532    #[serde(rename = "instance")]
533    pub instances: Vec<InstanceInfo>,
534    // TODO: leds field
535    pub hostname: String,
536    // TODO: (legacy) transform field
537    // TODO: (legacy) activeEffects field
538    // TODO: (legacy) activeLedColor field
539}
540
541/// Hyperion build info
542#[derive(Default, Debug, Serialize)]
543pub struct BuildInfo {
544    /// Version number
545    pub version: String,
546    /// Build time
547    pub time: String,
548}
549
550impl BuildInfo {
551    pub fn new() -> Self {
552        Self {
553            version: version(),
554            ..Default::default()
555        }
556    }
557}
558
559#[derive(Debug, Serialize)]
560pub struct InstanceInfo {
561    pub friendly_name: String,
562    pub instance: i32,
563    pub running: bool,
564}
565
566impl From<&crate::models::Instance> for InstanceInfo {
567    fn from(config: &crate::models::Instance) -> Self {
568        Self {
569            friendly_name: config.friendly_name.clone(),
570            instance: config.id,
571            // TODO: Runtime state might differ from config enabled
572            running: config.enabled,
573        }
574    }
575}
576
577#[derive(Debug, Serialize)]
578pub struct HyperionResponse {
579    success: bool,
580    #[serde(skip_serializing_if = "Option::is_none")]
581    tan: Option<i32>,
582    #[serde(skip_serializing_if = "Option::is_none")]
583    error: Option<String>,
584    #[serde(skip_serializing_if = "Option::is_none", flatten)]
585    info: Option<HyperionResponseInfo>,
586}
587
588#[derive(Default, Debug, Serialize)]
589#[serde(rename_all = "camelCase")]
590pub struct SystemInfo {
591    pub kernel_type: String,
592    pub kernel_version: String,
593    pub architecture: String,
594    pub cpu_model_name: String,
595    pub cpu_model_type: String,
596    pub cpu_hardware: String,
597    pub cpu_revision: String,
598    pub word_size: String,
599    pub product_type: String,
600    pub product_version: String,
601    pub pretty_name: String,
602    pub host_name: String,
603    pub domain_name: String,
604    pub qt_version: String,
605    pub py_version: String,
606}
607
608impl SystemInfo {
609    pub fn new() -> Self {
610        // TODO: Fill in other fields
611        Self {
612            kernel_type: if cfg!(target_os = "windows") {
613                "winnt".to_owned()
614            } else if cfg!(target_os = "linux") {
615                Command::new("uname")
616                    .args(["-s"])
617                    .stdout(Stdio::piped())
618                    .output()
619                    .ok()
620                    .and_then(|output| String::from_utf8(output.stdout).ok())
621                    .map(|output| output.trim().to_ascii_lowercase())
622                    .unwrap_or_default()
623            } else {
624                String::new()
625            },
626            kernel_version: if cfg!(target_os = "linux") {
627                Command::new("uname")
628                    .args(["-r"])
629                    .stdout(Stdio::piped())
630                    .output()
631                    .ok()
632                    .and_then(|output| String::from_utf8(output.stdout).ok())
633                    .map(|output| output.trim().to_ascii_lowercase())
634                    .unwrap_or_default()
635            } else {
636                String::new()
637            },
638            host_name: hostname(),
639            ..Default::default()
640        }
641    }
642}
643
644#[derive(Default, Debug, Serialize)]
645#[serde(rename_all = "camelCase")]
646pub struct HyperionInfo {
647    pub version: String,
648    pub build: String,
649    pub gitremote: String,
650    pub time: String,
651    pub id: uuid::Uuid,
652    pub read_only_mode: bool,
653}
654
655impl HyperionInfo {
656    pub fn new(id: uuid::Uuid) -> Self {
657        // TODO: Fill in other fields
658        Self {
659            // We emulate hyperion.ng 2.0.0-alpha.8
660            version: "2.0.0-alpha.8".to_owned(),
661            build: version(),
662            id,
663            read_only_mode: false,
664            ..Default::default()
665        }
666    }
667}
668
669#[derive(Debug, Serialize)]
670pub struct SysInfo {
671    pub system: SystemInfo,
672    pub hyperion: HyperionInfo,
673}
674
675impl SysInfo {
676    pub fn new(id: uuid::Uuid) -> Self {
677        Self {
678            system: SystemInfo::new(),
679            hyperion: HyperionInfo::new(id),
680        }
681    }
682}
683
684/// Hyperion JSON response
685#[derive(Debug, Serialize)]
686#[serde(tag = "command", content = "info")]
687#[allow(clippy::large_enum_variant)]
688pub enum HyperionResponseInfo {
689    /// Server information response
690    #[serde(rename = "serverinfo")]
691    ServerInfo(ServerInfo),
692    /// AdminRequired response
693    #[serde(rename = "authorize-adminRequired")]
694    AdminRequired {
695        /// true if admin authentication is required
696        #[serde(rename = "adminRequired")]
697        admin_required: bool,
698    },
699    /// TokenRequired response
700    #[serde(rename = "authorize-tokenRequired")]
701    TokenRequired {
702        /// true if an auth token required
703        required: bool,
704    },
705    /// SysInfo response
706    #[serde(rename = "sysinfo")]
707    SysInfo(SysInfo),
708    /// SwitchTo response
709    #[serde(rename = "instance-switchTo")]
710    SwitchTo {
711        #[serde(skip_serializing_if = "Option::is_none")]
712        instance: Option<i32>,
713    },
714}
715
716impl HyperionResponse {
717    pub fn with_tan(mut self, tan: Option<i32>) -> Self {
718        self.tan = tan;
719        self
720    }
721
722    fn success_info(info: HyperionResponseInfo) -> Self {
723        Self {
724            success: true,
725            tan: None,
726            error: None,
727            info: Some(info),
728        }
729    }
730
731    /// Return a success response
732    pub fn success() -> Self {
733        Self {
734            success: true,
735            tan: None,
736            error: None,
737            info: None,
738        }
739    }
740
741    /// Return an error response
742    pub fn error(error: impl std::fmt::Display) -> Self {
743        Self {
744            success: false,
745            tan: None,
746            error: Some(error.to_string()),
747            info: None,
748        }
749    }
750
751    /// Return an error response
752    pub fn error_info(error: impl std::fmt::Display, info: HyperionResponseInfo) -> Self {
753        Self {
754            success: false,
755            tan: None,
756            error: Some(error.to_string()),
757            info: Some(info),
758        }
759    }
760
761    /// Return a server information response
762    pub fn server_info(
763        priorities: Vec<PriorityInfo>,
764        adjustment: Vec<ChannelAdjustment>,
765        effects: Vec<EffectDefinition>,
766        instances: Vec<InstanceInfo>,
767    ) -> Self {
768        Self::success_info(HyperionResponseInfo::ServerInfo(ServerInfo {
769            priorities,
770            // TODO: Actual autoselect value
771            priorities_autoselect: true,
772            adjustment,
773            effects,
774            led_devices: LedDevicesInfo::new(),
775            grabbers: GrabbersInfo::new(),
776            // TODO: Actual video mode
777            video_mode: VideoMode::Mode2D,
778            instances,
779            hostname: hostname(),
780        }))
781    }
782
783    pub fn admin_required(admin_required: bool) -> Self {
784        Self::success_info(HyperionResponseInfo::AdminRequired { admin_required })
785    }
786
787    pub fn token_required(required: bool) -> Self {
788        Self::success_info(HyperionResponseInfo::TokenRequired { required })
789    }
790
791    pub fn sys_info(id: uuid::Uuid) -> Self {
792        // TODO: Properly fill out this response
793        Self::success_info(HyperionResponseInfo::SysInfo(SysInfo::new(id)))
794    }
795
796    pub fn switch_to(id: Option<i32>) -> Self {
797        if let Some(id) = id {
798            // Switch successful
799            Self::success_info(HyperionResponseInfo::SwitchTo { instance: Some(id) })
800        } else {
801            Self::error_info(
802                "selected hyperion instance not found",
803                HyperionResponseInfo::SwitchTo { instance: None },
804            )
805        }
806    }
807}
808
809fn hostname() -> String {
810    hostname::get()
811        .map(|s| s.to_string_lossy().to_string())
812        .unwrap_or_else(|_| "<unknown hostname>".to_owned())
813}
814
815fn version() -> String {
816    git_version::git_version!(
817        prefix = "hyperion.rs-",
818        args = ["--always", "--tags"],
819        fallback = env!("HYPERION_RS_GIT_VERSION")
820    )
821    .to_owned()
822}