Skip to main content

sl_types/
viewer_uri.rs

1//! Viewer URI related types
2//!
3//! see <https://wiki.secondlife.com/wiki/Viewer_URI_Name_Space>
4
5#[cfg(feature = "chumsky")]
6use chumsky::{Parser, prelude::just};
7#[cfg(feature = "chumsky")]
8use std::ops::Deref as _;
9
10#[cfg(feature = "chumsky")]
11use crate::utils::url_text_component_parser;
12
13/// represents the various script trigger modes for the script_trigger_lbutton
14/// key binding
15#[derive(
16    Debug,
17    Clone,
18    PartialEq,
19    Eq,
20    PartialOrd,
21    Ord,
22    strum::FromRepr,
23    strum::EnumIs,
24    serde::Serialize,
25    serde::Deserialize,
26)]
27pub enum ScriptTriggerMode {
28    /// "first_person" or 0
29    FirstPerson = 0,
30    /// "third_person" or 1
31    ThirdPerson = 1,
32    /// "edit_avatar" or 2
33    EditAvatar = 2,
34    /// "sitting" or 3
35    Sitting = 3,
36}
37
38/// parse script trigger mode
39///
40/// # Errors
41///
42/// returns and error if the string could not be parsed
43#[cfg(feature = "chumsky")]
44#[must_use]
45pub fn script_trigger_mode_parser<'src>()
46-> impl Parser<'src, &'src str, ScriptTriggerMode, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
47{
48    just("first_person")
49        .to(ScriptTriggerMode::FirstPerson)
50        .or(just("third_person").to(ScriptTriggerMode::ThirdPerson))
51        .or(just("edit_avatar").to(ScriptTriggerMode::EditAvatar))
52        .or(just("sitting").to(ScriptTriggerMode::Sitting))
53        .labelled("script trigger mode")
54}
55
56impl std::fmt::Display for ScriptTriggerMode {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Self::FirstPerson => write!(f, "first_person"),
60            Self::ThirdPerson => write!(f, "third_person"),
61            Self::EditAvatar => write!(f, "edit_avatar"),
62            Self::Sitting => write!(f, "sitting"),
63        }
64    }
65}
66
67/// error when trying to parse a string as a ScriptTriggerMode
68#[derive(Debug, Clone)]
69pub struct ScriptTriggerModeParseError {
70    /// the value that could not be parsed
71    value: String,
72}
73
74impl std::fmt::Display for ScriptTriggerModeParseError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "Could not parse as ScriptTriggerMode: {}", self.value)
77    }
78}
79
80impl std::error::Error for ScriptTriggerModeParseError {}
81
82impl std::str::FromStr for ScriptTriggerMode {
83    type Err = ScriptTriggerModeParseError;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        match s {
87            "first_person" | "0" => Ok(Self::FirstPerson),
88            "third_person" | "1" => Ok(Self::ThirdPerson),
89            "edit_avatar" | "2" => Ok(Self::EditAvatar),
90            "sitting" | "3" => Ok(Self::Sitting),
91            _ => Err(ScriptTriggerModeParseError {
92                value: s.to_owned(),
93            }),
94        }
95    }
96}
97
98/// represents a Viewer URI
99#[derive(Debug, Clone, PartialEq, Eq, strum::EnumIs, serde::Serialize, serde::Deserialize)]
100pub enum ViewerUri {
101    /// a link to this location
102    Location(crate::map::Location),
103    /// opens the agent profile
104    AgentAbout(crate::key::AgentKey),
105    /// displays the info dialog for the agent
106    AgentInspect(crate::key::AgentKey),
107    /// starts an IM session with the agent
108    AgentInstantMessage(crate::key::AgentKey),
109    /// displays teleport offer dialog for the agent
110    AgentOfferTeleport(crate::key::AgentKey),
111    /// displays pay resident dialog
112    AgentPay(crate::key::AgentKey),
113    /// displays friendship offer dialog
114    AgentRequestFriend(crate::key::AgentKey),
115    /// adds agent to block list
116    AgentMute(crate::key::AgentKey),
117    /// removes agent from block list
118    AgentUnmute(crate::key::AgentKey),
119    /// replaces the URL with the agent's display and user names
120    AgentCompleteName(crate::key::AgentKey),
121    /// replaces the URL with the agent's display name
122    AgentDisplayName(crate::key::AgentKey),
123    /// replaces the URL with the agent's username
124    AgentUsername(crate::key::AgentKey),
125    /// show appearance
126    AppearanceShow,
127    /// request a L$ balance update from the server
128    BalanceRequest,
129    /// send a chat message to the given channel, won't work with DEBUG_CHANNEL
130    Chat {
131        /// the channel to send the message on, can not be DEBUG_CHANNEL
132        channel: crate::chat::ChatChannel,
133        /// the text to send
134        text: String,
135    },
136    /// open a floater describing the classified ad
137    ClassifiedAbout(crate::key::ClassifiedKey),
138    /// open a floater describing the event
139    EventAbout(crate::search::EventId),
140    /// open a floater describing the experience
141    ExperienceProfile(crate::key::ExperienceKey),
142    /// open the group profile
143    GroupAbout(crate::key::GroupKey),
144    /// displays the info dialog for the group
145    GroupInspect(crate::key::GroupKey),
146    /// open the create group dialog
147    GroupCreate,
148    /// open the group list to which the current avatar belongs
149    GroupListShow,
150    /// open help
151    Help {
152        /// optional help topic
153        help_query: Option<String>,
154    },
155    /// offer inventory
156    InventorySelect(crate::key::InventoryKey),
157    /// show inventory
158    InventoryShow,
159    /// key binding
160    KeyBindingMovementWalkTo,
161    /// key binding
162    KeyBindingMovementTeleportTo,
163    /// key binding
164    KeyBindingMovementPushForward,
165    /// key binding
166    KeyBindingMovementPushBackward,
167    /// key binding
168    KeyBindingMovementTurnLeft,
169    /// key binding
170    KeyBindingMovementTurnRight,
171    /// key binding
172    KeyBindingMovementSlideLeft,
173    /// key binding
174    KeyBindingMovementSlideRight,
175    /// key binding
176    KeyBindingMovementJump,
177    /// key binding
178    KeyBindingMovementPushDown,
179    /// key binding
180    KeyBindingMovementRunForward,
181    /// key binding
182    KeyBindingMovementRunBackward,
183    /// key binding
184    KeyBindingMovementRunLeft,
185    /// key binding
186    KeyBindingMovementRunRight,
187    /// key binding
188    KeyBindingMovementToggleRun,
189    /// key binding
190    KeyBindingMovementToggleFly,
191    /// key binding
192    KeyBindingMovementToggleSit,
193    /// key binding
194    KeyBindingMovementStopMoving,
195    /// key binding
196    KeyBindingCameraLookUp,
197    /// key binding
198    KeyBindingCameraLookDown,
199    /// key binding
200    KeyBindingCameraMoveForward,
201    /// key binding
202    KeyBindingCameraMoveBackward,
203    /// key binding
204    KeyBindingCameraMoveForwardFast,
205    /// key binding
206    KeyBindingCameraMoveBackwardFast,
207    /// key binding
208    KeyBindingCameraSpinOver,
209    /// key binding
210    KeyBindingCameraSpinUnder,
211    /// key binding
212    KeyBindingCameraPanUp,
213    /// key binding
214    KeyBindingCameraPanDown,
215    /// key binding
216    KeyBindingCameraPanLeft,
217    /// key binding
218    KeyBindingCameraPanRight,
219    /// key binding
220    KeyBindingCameraPanIn,
221    /// key binding
222    KeyBindingCameraPanOut,
223    /// key binding
224    KeyBindingCameraSpinAroundCounterClockwise,
225    /// key binding
226    KeyBindingCameraSpinAroundClockwise,
227    /// key binding
228    KeyBindingCameraMoveForwardSitting,
229    /// key binding
230    KeyBindingCameraMoveBackwardSitting,
231    /// key binding
232    KeyBindingCameraSpinOverSitting,
233    /// key binding
234    KeyBindingCameraSpinUnderSitting,
235    /// key binding
236    KeyBindingCameraSpinAroundCounterClockwiseSitting,
237    /// key binding
238    KeyBindingCameraSpinAroundClockwiseSitting,
239    /// key binding
240    KeyBindingEditingAvatarSpinCounterClockwise,
241    /// key binding
242    KeyBindingEditingAvatarSpinClockwise,
243    /// key binding
244    KeyBindingEditingAvatarSpinOver,
245    /// key binding
246    KeyBindingEditingAvatarSpinUnder,
247    /// key binding
248    KeyBindingEditingAvatarMoveForward,
249    /// key binding
250    KeyBindingEditingAvatarMoveBackward,
251    /// key binding
252    KeyBindingSoundAndMediaTogglePauseMedia,
253    /// key binding
254    KeyBindingSoundAndMediaToggleEnableMedia,
255    /// key binding
256    KeyBindingSoundAndMediaVoiceFollowKey,
257    /// key binding
258    KeyBindingSoundAndMediaToggleVoice,
259    /// key binding
260    KeyBindingStartChat,
261    /// key binding
262    KeyBindingStartGesture,
263    /// key binding
264    KeyBindingScriptTriggerLButton(ScriptTriggerMode),
265    /// login on launch
266    Login {
267        /// account first name
268        first_name: String,
269        /// account last name
270        last_name: String,
271        /// secure session id
272        session: String,
273        /// login location
274        login_location: Option<String>,
275    },
276    /// track a friend with the permission on the world map
277    MapTrackAvatar(crate::key::FriendKey),
278    /// display an info dialog for the object sending this message
279    ObjectInstantMessage {
280        /// key of the object
281        object_key: crate::key::ObjectKey,
282        /// name of the object
283        object_name: String,
284        /// owner of the object
285        owner: crate::key::OwnerKey,
286        /// object location
287        location: crate::map::Location,
288    },
289    /// open the named floater
290    OpenFloater(String),
291    /// open a floater describing a parcel
292    Parcel(crate::key::ParcelKey),
293    /// open a search floater with matching results
294    Search {
295        /// search category
296        category: crate::search::SearchCategory,
297        /// search term
298        search_term: String,
299    },
300    /// open an inventory share/IM window for agent
301    ShareWithAvatar(crate::key::AgentKey),
302    /// teleport to this location
303    Teleport(crate::map::Location),
304    /// start a private voice session with this avatar
305    VoiceCallAvatar(crate::key::AgentKey),
306    /// replace outfit with contents of folder specified by key (UUID)
307    WearFolderByInventoryFolderKey(crate::key::InventoryFolderKey),
308    /// replace outfit with contents of named library folder
309    WearFolderByLibraryFolderName(String),
310    /// open the world map with this destination selected
311    WorldMap(crate::map::Location),
312}
313
314impl ViewerUri {
315    /// this returns whether the given ViewerUri can only be called from internal
316    /// browsers/chat/... or if external programs (like browsers) can use them too
317    #[must_use]
318    pub const fn internal_only(&self) -> bool {
319        matches!(self, Self::Location(_) | Self::Login { .. })
320    }
321}
322
323impl std::fmt::Display for ViewerUri {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        match self {
326            Self::Location(location) => {
327                write!(
328                    f,
329                    "secondlife:///{}/{}/{}/{}",
330                    percent_encoding::percent_encode(
331                        location.region_name().as_ref().as_bytes(),
332                        percent_encoding::NON_ALPHANUMERIC
333                    ),
334                    location.x(),
335                    location.y(),
336                    location.z()
337                )
338            }
339            Self::AgentAbout(agent_key) => {
340                write!(f, "secondlife:///app/agent/{agent_key}/about")
341            }
342            Self::AgentInspect(agent_key) => {
343                write!(f, "secondlife:///app/agent/{agent_key}/inspect")
344            }
345            Self::AgentInstantMessage(agent_key) => {
346                write!(f, "secondlife:///app/agent/{agent_key}/im")
347            }
348            Self::AgentOfferTeleport(agent_key) => {
349                write!(f, "secondlife:///app/agent/{agent_key}/offerteleport")
350            }
351            Self::AgentPay(agent_key) => {
352                write!(f, "secondlife:///app/agent/{agent_key}/pay")
353            }
354            Self::AgentRequestFriend(agent_key) => {
355                write!(f, "secondlife:///app/agent/{agent_key}/requestfriend")
356            }
357            Self::AgentMute(agent_key) => {
358                write!(f, "secondlife:///app/agent/{agent_key}/mute")
359            }
360            Self::AgentUnmute(agent_key) => {
361                write!(f, "secondlife:///app/agent/{agent_key}/unmute")
362            }
363            Self::AgentCompleteName(agent_key) => {
364                write!(f, "secondlife:///app/agent/{agent_key}/completename")
365            }
366            Self::AgentDisplayName(agent_key) => {
367                write!(f, "secondlife:///app/agent/{agent_key}/displayname")
368            }
369            Self::AgentUsername(agent_key) => {
370                write!(f, "secondlife:///app/agent/{agent_key}/username")
371            }
372            Self::AppearanceShow => {
373                write!(f, "secondlife:///app/appearance/show")
374            }
375            Self::BalanceRequest => {
376                write!(f, "secondlife:///app/balance/request")
377            }
378            Self::Chat { channel, text } => {
379                write!(
380                    f,
381                    "secondlife:///app/chat/{}/{}",
382                    channel,
383                    percent_encoding::percent_encode(
384                        text.as_bytes(),
385                        percent_encoding::NON_ALPHANUMERIC
386                    )
387                )
388            }
389            Self::ClassifiedAbout(classified_key) => {
390                write!(f, "secondlife:///app/classified/{classified_key}/about")
391            }
392            Self::EventAbout(event_id) => {
393                write!(f, "secondlife:///app/event/{event_id}/about")
394            }
395            Self::ExperienceProfile(experience_key) => {
396                write!(f, "secondlife:///app/experience/{experience_key}/profile")
397            }
398            Self::GroupAbout(group_key) => {
399                write!(f, "secondlife:///app/group/{group_key}/about")
400            }
401            Self::GroupInspect(group_key) => {
402                write!(f, "secondlife:///app/group/{group_key}/inspect")
403            }
404            Self::GroupCreate => {
405                write!(f, "secondlife:///app/group/create")
406            }
407            Self::GroupListShow => {
408                write!(f, "secondlife:///app/group/list/show")
409            }
410            Self::Help { help_query } => {
411                if let Some(help_query) = help_query {
412                    write!(
413                        f,
414                        "secondlife:///app/help/{}",
415                        percent_encoding::percent_encode(
416                            help_query.as_bytes(),
417                            percent_encoding::NON_ALPHANUMERIC
418                        )
419                    )
420                } else {
421                    write!(f, "secondlife:///app/help")
422                }
423            }
424            Self::InventorySelect(inventory_key) => {
425                write!(f, "secondlife:///app/inventory/{inventory_key}/select")
426            }
427            Self::InventoryShow => {
428                write!(f, "secondlife:///app/inventory/show")
429            }
430            Self::KeyBindingMovementWalkTo => {
431                write!(f, "secondlife:///app/keybinding/walk_to")
432            }
433            Self::KeyBindingMovementTeleportTo => {
434                write!(f, "secondlife:///app/keybinding/teleport_to")
435            }
436            Self::KeyBindingMovementPushForward => {
437                write!(f, "secondlife:///app/keybinding/push_forward")
438            }
439            Self::KeyBindingMovementPushBackward => {
440                write!(f, "secondlife:///app/keybinding/push_backward")
441            }
442            Self::KeyBindingMovementTurnLeft => {
443                write!(f, "secondlife:///app/keybinding/turn_left")
444            }
445            Self::KeyBindingMovementTurnRight => {
446                write!(f, "secondlife:///app/keybinding/turn_right")
447            }
448            Self::KeyBindingMovementSlideLeft => {
449                write!(f, "secondlife:///app/keybinding/slide_left")
450            }
451            Self::KeyBindingMovementSlideRight => {
452                write!(f, "secondlife:///app/keybinding/slide_right")
453            }
454            Self::KeyBindingMovementJump => {
455                write!(f, "secondlife:///app/keybinding/jump")
456            }
457            Self::KeyBindingMovementPushDown => {
458                write!(f, "secondlife:///app/keybinding/push_down")
459            }
460            Self::KeyBindingMovementRunForward => {
461                write!(f, "secondlife:///app/keybinding/run_forward")
462            }
463            Self::KeyBindingMovementRunBackward => {
464                write!(f, "secondlife:///app/keybinding/run_backward")
465            }
466            Self::KeyBindingMovementRunLeft => {
467                write!(f, "secondlife:///app/keybinding/run_left")
468            }
469            Self::KeyBindingMovementRunRight => {
470                write!(f, "secondlife:///app/keybinding/run_right")
471            }
472            Self::KeyBindingMovementToggleRun => {
473                write!(f, "secondlife:///app/keybinding/toggle_run")
474            }
475            Self::KeyBindingMovementToggleFly => {
476                write!(f, "secondlife:///app/keybinding/toggle_fly")
477            }
478            Self::KeyBindingMovementToggleSit => {
479                write!(f, "secondlife:///app/keybinding/toggle_sit")
480            }
481            Self::KeyBindingMovementStopMoving => {
482                write!(f, "secondlife:///app/keybinding/stop_moving")
483            }
484            Self::KeyBindingCameraLookUp => {
485                write!(f, "secondlife:///app/keybinding/look_up")
486            }
487            Self::KeyBindingCameraLookDown => {
488                write!(f, "secondlife:///app/keybinding/look_down")
489            }
490            Self::KeyBindingCameraMoveForward => {
491                write!(f, "secondlife:///app/keybinding/move_forward")
492            }
493            Self::KeyBindingCameraMoveBackward => {
494                write!(f, "secondlife:///app/keybinding/move_backward")
495            }
496            Self::KeyBindingCameraMoveForwardFast => {
497                write!(f, "secondlife:///app/keybinding/move_forward_fast")
498            }
499            Self::KeyBindingCameraMoveBackwardFast => {
500                write!(f, "secondlife:///app/keybinding/move_backward_fast")
501            }
502            Self::KeyBindingCameraSpinOver => {
503                write!(f, "secondlife:///app/keybinding/spin_over")
504            }
505            Self::KeyBindingCameraSpinUnder => {
506                write!(f, "secondlife:///app/keybinding/spin_under")
507            }
508            Self::KeyBindingCameraPanUp => {
509                write!(f, "secondlife:///app/keybinding/pan_up")
510            }
511            Self::KeyBindingCameraPanDown => {
512                write!(f, "secondlife:///app/keybinding/pan_down")
513            }
514            Self::KeyBindingCameraPanLeft => {
515                write!(f, "secondlife:///app/keybinding/pan_left")
516            }
517            Self::KeyBindingCameraPanRight => {
518                write!(f, "secondlife:///app/keybinding/pan_right")
519            }
520            Self::KeyBindingCameraPanIn => {
521                write!(f, "secondlife:///app/keybinding/pan_in")
522            }
523            Self::KeyBindingCameraPanOut => {
524                write!(f, "secondlife:///app/keybinding/pan_out")
525            }
526            Self::KeyBindingCameraSpinAroundCounterClockwise => {
527                write!(f, "secondlife:///app/keybinding/spin_around_ccw")
528            }
529            Self::KeyBindingCameraSpinAroundClockwise => {
530                write!(f, "secondlife:///app/keybinding/spin_around_cw")
531            }
532            Self::KeyBindingCameraMoveForwardSitting => {
533                write!(f, "secondlife:///app/keybinding/move_forward_sitting")
534            }
535            Self::KeyBindingCameraMoveBackwardSitting => {
536                write!(f, "secondlife:///app/keybinding/move_backward_sitting")
537            }
538            Self::KeyBindingCameraSpinOverSitting => {
539                write!(f, "secondlife:///app/keybinding/spin_over_sitting")
540            }
541            Self::KeyBindingCameraSpinUnderSitting => {
542                write!(f, "secondlife:///app/keybinding/spin_under_sitting")
543            }
544            Self::KeyBindingCameraSpinAroundCounterClockwiseSitting => {
545                write!(f, "secondlife:///app/keybinding/spin_around_ccw_sitting")
546            }
547            Self::KeyBindingCameraSpinAroundClockwiseSitting => {
548                write!(f, "secondlife:///app/keybinding/spin_around_cw_sitting")
549            }
550            Self::KeyBindingEditingAvatarSpinCounterClockwise => {
551                write!(f, "secondlife:///app/keybinding/avatar_spin_ccw")
552            }
553            Self::KeyBindingEditingAvatarSpinClockwise => {
554                write!(f, "secondlife:///app/keybinding/avatar_spin_cw")
555            }
556            Self::KeyBindingEditingAvatarSpinOver => {
557                write!(f, "secondlife:///app/keybinding/avatar_spin_over")
558            }
559            Self::KeyBindingEditingAvatarSpinUnder => {
560                write!(f, "secondlife:///app/keybinding/avatar_spin_under")
561            }
562            Self::KeyBindingEditingAvatarMoveForward => {
563                write!(f, "secondlife:///app/keybinding/avatar_move_forward")
564            }
565            Self::KeyBindingEditingAvatarMoveBackward => {
566                write!(f, "secondlife:///app/keybinding/avatar_move_backward")
567            }
568            Self::KeyBindingSoundAndMediaTogglePauseMedia => {
569                write!(f, "secondlife:///app/keybinding/toggle_pause_media")
570            }
571            Self::KeyBindingSoundAndMediaToggleEnableMedia => {
572                write!(f, "secondlife:///app/keybinding/toggle_enable_media")
573            }
574            Self::KeyBindingSoundAndMediaVoiceFollowKey => {
575                write!(f, "secondlife:///app/keybinding/voice_follow_key")
576            }
577            Self::KeyBindingSoundAndMediaToggleVoice => {
578                write!(f, "secondlife:///app/keybinding/toggle_voice")
579            }
580            Self::KeyBindingStartChat => {
581                write!(f, "secondlife:///app/keybinding/start_chat")
582            }
583            Self::KeyBindingStartGesture => {
584                write!(f, "secondlife:///app/keybinding/start_gesture")
585            }
586            Self::KeyBindingScriptTriggerLButton(script_trigger_mode) => {
587                write!(
588                    f,
589                    "secondlife:///app/keybinding/script_trigger_lbutton?mode={script_trigger_mode}"
590                )
591            }
592            Self::Login {
593                first_name,
594                last_name,
595                session,
596                login_location,
597            } => {
598                write!(
599                    f,
600                    "secondlife::///app/login?first={}&last={}&session={}{}",
601                    percent_encoding::percent_encode(
602                        first_name.as_bytes(),
603                        percent_encoding::NON_ALPHANUMERIC
604                    ),
605                    percent_encoding::percent_encode(
606                        last_name.as_bytes(),
607                        percent_encoding::NON_ALPHANUMERIC
608                    ),
609                    session,
610                    if let Some(login_location) = login_location {
611                        format!(
612                            "&location={}",
613                            percent_encoding::percent_encode(
614                                login_location.as_bytes(),
615                                percent_encoding::NON_ALPHANUMERIC
616                            ),
617                        )
618                    } else {
619                        "".to_string()
620                    },
621                )
622            }
623            Self::MapTrackAvatar(friend_key) => {
624                write!(f, "secondlife:///app/maptrackavatar/{friend_key}")
625            }
626            Self::ObjectInstantMessage {
627                object_key,
628                object_name,
629                owner,
630                location,
631            } => {
632                write!(
633                    f,
634                    "secondlife::///app/objectim/{}/?object_name={}&{}&slurl={}/{}/{}/{}",
635                    object_key,
636                    percent_encoding::percent_encode(
637                        object_name.as_bytes(),
638                        percent_encoding::NON_ALPHANUMERIC
639                    ),
640                    match owner {
641                        crate::key::OwnerKey::Agent(agent_key) => {
642                            format!("owner={agent_key}")
643                        }
644                        crate::key::OwnerKey::Group(group_key) => {
645                            format!("owner={group_key}?groupowned=true")
646                        }
647                    },
648                    percent_encoding::percent_encode(
649                        location.region_name.as_ref().as_bytes(),
650                        percent_encoding::NON_ALPHANUMERIC
651                    ),
652                    location.x,
653                    location.y,
654                    location.z,
655                )
656            }
657            Self::OpenFloater(floater_name) => {
658                write!(
659                    f,
660                    "secondlife:///app/openfloater/{}",
661                    percent_encoding::percent_encode(
662                        floater_name.as_bytes(),
663                        percent_encoding::NON_ALPHANUMERIC
664                    )
665                )
666            }
667            Self::Parcel(parcel_key) => {
668                write!(f, "secondlife:///app/parcel/{parcel_key}/about")
669            }
670            Self::Search {
671                category,
672                search_term,
673            } => {
674                write!(
675                    f,
676                    "secondlife:///app/search/{}/{}",
677                    category,
678                    percent_encoding::percent_encode(
679                        search_term.as_bytes(),
680                        percent_encoding::NON_ALPHANUMERIC
681                    )
682                )
683            }
684            Self::ShareWithAvatar(agent_key) => {
685                write!(f, "secondlife::///app/sharewithavatar/{agent_key}")
686            }
687            Self::Teleport(location) => {
688                write!(
689                    f,
690                    "secondlife:///teleport/{}/{}/{}/{}",
691                    percent_encoding::percent_encode(
692                        location.region_name().as_ref().as_bytes(),
693                        percent_encoding::NON_ALPHANUMERIC
694                    ),
695                    location.x(),
696                    location.y(),
697                    location.z()
698                )
699            }
700            Self::VoiceCallAvatar(agent_key) => {
701                write!(f, "secondlife:///app/voicecallavatar/{agent_key}")
702            }
703            Self::WearFolderByInventoryFolderKey(inventory_folder_key) => {
704                write!(
705                    f,
706                    "secondlife:///app/wear_folder?folder_id={inventory_folder_key}"
707                )
708            }
709            Self::WearFolderByLibraryFolderName(library_folder_name) => {
710                write!(
711                    f,
712                    "secondlife:///app/wear_folder?folder_name={}",
713                    percent_encoding::percent_encode(
714                        library_folder_name.as_bytes(),
715                        percent_encoding::NON_ALPHANUMERIC
716                    ),
717                )
718            }
719            Self::WorldMap(location) => {
720                write!(
721                    f,
722                    "secondlife:///app/worldmap/{}/{}/{}/{}",
723                    percent_encoding::percent_encode(
724                        location.region_name().as_ref().as_bytes(),
725                        percent_encoding::NON_ALPHANUMERIC
726                    ),
727                    location.x(),
728                    location.y(),
729                    location.z()
730                )
731            }
732        }
733    }
734}
735
736// TODO: FromStr instance
737
738/// parse a viewer app agent URI
739///
740/// # Errors
741///
742/// returns an error if the string could not be parsed
743#[cfg(feature = "chumsky")]
744#[must_use]
745pub fn viewer_app_agent_uri_parser<'src>()
746-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
747    just("secondlife:///app/agent/")
748        .ignore_then(
749            crate::key::agent_key_parser()
750                .then_ignore(just("/about"))
751                .map(ViewerUri::AgentAbout)
752                .or(crate::key::agent_key_parser()
753                    .then_ignore(just("/inspect"))
754                    .map(ViewerUri::AgentInspect))
755                .or(crate::key::agent_key_parser()
756                    .then_ignore(just("/im"))
757                    .map(ViewerUri::AgentInstantMessage))
758                .or(crate::key::agent_key_parser()
759                    .then_ignore(just("/offerteleport"))
760                    .map(ViewerUri::AgentOfferTeleport))
761                .or(crate::key::agent_key_parser()
762                    .then_ignore(just("/pay"))
763                    .map(ViewerUri::AgentPay))
764                .or(crate::key::agent_key_parser()
765                    .then_ignore(just("/requestfriend"))
766                    .map(ViewerUri::AgentRequestFriend))
767                .or(crate::key::agent_key_parser()
768                    .then_ignore(just("/mute"))
769                    .map(ViewerUri::AgentMute))
770                .or(crate::key::agent_key_parser()
771                    .then_ignore(just("/unmute"))
772                    .map(ViewerUri::AgentUnmute))
773                .or(crate::key::agent_key_parser()
774                    .then_ignore(just("/completename"))
775                    .map(ViewerUri::AgentCompleteName))
776                .or(crate::key::agent_key_parser()
777                    .then_ignore(just("/displayname"))
778                    .map(ViewerUri::AgentDisplayName))
779                .or(crate::key::agent_key_parser()
780                    .then_ignore(just("/username"))
781                    .map(ViewerUri::AgentUsername)),
782        )
783        .labelled("viewer app agent URI")
784}
785
786/// parse a viewer app appearance URI
787///
788/// # Errors
789///
790/// returns an error if the string could not be parsed
791#[cfg(feature = "chumsky")]
792#[must_use]
793pub fn viewer_app_appearance_uri_parser<'src>()
794-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
795    just("secondlife:///app/appearance/show")
796        .to(ViewerUri::AppearanceShow)
797        .labelled("viewer app appearance URI")
798}
799
800/// parse a viewer app balance URI
801///
802/// # Errors
803///
804/// returns an error if the string could not be parsed
805#[cfg(feature = "chumsky")]
806#[must_use]
807pub fn viewer_app_balance_uri_parser<'src>()
808-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
809    just("secondlife:///app/balance/request")
810        .to(ViewerUri::BalanceRequest)
811        .labelled("viewer app balance URI")
812}
813
814/// parse a viewer app chat URI
815///
816/// # Errors
817///
818/// returns an error if the string could not be parsed
819#[cfg(feature = "chumsky")]
820#[must_use]
821pub fn viewer_app_chat_uri_parser<'src>()
822-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
823    just("secondlife:///app/chat/")
824        .ignore_then(
825            crate::chat::chat_channel_parser()
826                .then_ignore(just('/'))
827                .then(url_text_component_parser()),
828        )
829        .map(|(channel, text)| ViewerUri::Chat {
830            channel,
831            text: text.to_string(),
832        })
833        .labelled("viewer app chat URI")
834}
835
836/// parse a viewer app classified URI
837///
838/// # Errors
839///
840/// returns an error if the string could not be parsed
841#[cfg(feature = "chumsky")]
842#[must_use]
843pub fn viewer_app_classified_uri_parser<'src>()
844-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
845    just("secondlife:///app/classified/")
846        .ignore_then(
847            crate::key::classified_key_parser()
848                .then_ignore(just("/about"))
849                .map(ViewerUri::ClassifiedAbout),
850        )
851        .labelled("viewer app classified URI")
852}
853
854/// parse a viewer app event URI
855///
856/// # Errors
857///
858/// returns an error if the string could not be parsed
859#[cfg(feature = "chumsky")]
860#[must_use]
861pub fn viewer_app_event_uri_parser<'src>()
862-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
863    just("secondlife:///app/event/")
864        .ignore_then(
865            crate::search::event_id_parser()
866                .then_ignore(just("/about"))
867                .map(ViewerUri::EventAbout),
868        )
869        .labelled("viewer app event URI")
870}
871
872/// parse a viewer app experience URI
873///
874/// # Errors
875///
876/// returns an error if the string could not be parsed
877#[cfg(feature = "chumsky")]
878#[must_use]
879pub fn viewer_app_experience_uri_parser<'src>()
880-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
881    just("secondlife:///app/experience/")
882        .ignore_then(
883            crate::key::experience_key_parser()
884                .then_ignore(just("/profile"))
885                .map(ViewerUri::ExperienceProfile),
886        )
887        .labelled("viewer app experience URI")
888}
889
890/// parse a viewer app group URI
891///
892/// # Errors
893///
894/// returns an error if the string could not be parsed
895#[cfg(feature = "chumsky")]
896#[must_use]
897pub fn viewer_app_group_uri_parser<'src>()
898-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
899    just("secondlife:///app/group/")
900        .ignore_then(
901            crate::key::group_key_parser()
902                .then_ignore(just("/about"))
903                .map(ViewerUri::GroupAbout)
904                .or(crate::key::group_key_parser()
905                    .then_ignore(just("/inspect"))
906                    .map(ViewerUri::GroupInspect))
907                .or(just("create").to(ViewerUri::GroupCreate))
908                .or(just("list/show").to(ViewerUri::GroupListShow)),
909        )
910        .labelled("viewer app group URI")
911}
912
913/// parse a viewer app help URI
914///
915/// # Errors
916///
917/// returns an error if the string could not be parsed
918#[cfg(feature = "chumsky")]
919#[must_use]
920pub fn viewer_app_help_uri_parser<'src>()
921-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
922    just("secondlife:///app/help/")
923        .ignore_then(just('/').ignore_then(url_text_component_parser()).or_not())
924        .map(|help_query| ViewerUri::Help { help_query })
925        .labelled("viewer app help URI")
926}
927
928/// parse a viewer app inventory URI
929///
930/// # Errors
931///
932/// returns an error if the string could not be parsed
933#[cfg(feature = "chumsky")]
934#[must_use]
935pub fn viewer_app_inventory_uri_parser<'src>()
936-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
937    just("secondlife:///app/inventory/")
938        .ignore_then(
939            crate::key::inventory_key_parser()
940                .then_ignore(just("/select"))
941                .map(ViewerUri::InventorySelect)
942                .or(just("/show").to(ViewerUri::InventoryShow)),
943        )
944        .labelled("viewer app inventory URI")
945}
946
947/// parse a viewer app keybinding URI
948///
949/// # Errors
950///
951/// returns an error if the string could not be parsed
952#[cfg(feature = "chumsky")]
953#[must_use]
954pub fn viewer_app_keybinding_uri_parser<'src>()
955-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
956    just("secondlife:///app/keybinding/")
957        .ignore_then(
958            url_text_component_parser()
959                .try_map(|s, span| match s.deref() {
960                    "walk_to" => Ok(ViewerUri::KeyBindingMovementWalkTo),
961                    "teleport_to" => Ok(ViewerUri::KeyBindingMovementTeleportTo),
962                    "push_forward" => Ok(ViewerUri::KeyBindingMovementPushForward),
963                    "push_backward" => Ok(ViewerUri::KeyBindingMovementPushBackward),
964                    "turn_left" => Ok(ViewerUri::KeyBindingMovementTurnLeft),
965                    "turn_right" => Ok(ViewerUri::KeyBindingMovementTurnRight),
966                    "slide_left" => Ok(ViewerUri::KeyBindingMovementSlideLeft),
967                    "slide_right" => Ok(ViewerUri::KeyBindingMovementSlideRight),
968                    "jump" => Ok(ViewerUri::KeyBindingMovementJump),
969                    "push_down" => Ok(ViewerUri::KeyBindingMovementPushDown),
970                    "run_forward" => Ok(ViewerUri::KeyBindingMovementRunForward),
971                    "run_backward" => Ok(ViewerUri::KeyBindingMovementRunBackward),
972                    "run_left" => Ok(ViewerUri::KeyBindingMovementRunLeft),
973                    "run_right" => Ok(ViewerUri::KeyBindingMovementRunRight),
974                    "toggle_run" => Ok(ViewerUri::KeyBindingMovementToggleRun),
975                    "toggle_fly" => Ok(ViewerUri::KeyBindingMovementToggleFly),
976                    "toggle_sit" => Ok(ViewerUri::KeyBindingMovementToggleSit),
977                    "stop_moving" => Ok(ViewerUri::KeyBindingMovementStopMoving),
978                    "look_up" => Ok(ViewerUri::KeyBindingCameraLookUp),
979                    "look_down" => Ok(ViewerUri::KeyBindingCameraLookDown),
980                    "move_forward_fast" => Ok(ViewerUri::KeyBindingCameraMoveForwardFast),
981                    "move_backward_fast" => Ok(ViewerUri::KeyBindingCameraMoveBackwardFast),
982                    "move_forward_sitting" => Ok(ViewerUri::KeyBindingCameraMoveForwardSitting),
983                    "move_backward_sittingk" => Ok(ViewerUri::KeyBindingCameraMoveBackwardSitting),
984                    "move_forward" => Ok(ViewerUri::KeyBindingCameraMoveForward),
985                    "move_backward" => Ok(ViewerUri::KeyBindingCameraMoveBackward),
986                    "spin_over_sitting" => Ok(ViewerUri::KeyBindingCameraSpinOverSitting),
987                    "spin_under_sitting" => Ok(ViewerUri::KeyBindingCameraSpinUnderSitting),
988                    "spin_over" => Ok(ViewerUri::KeyBindingCameraSpinOver),
989                    "spin_under" => Ok(ViewerUri::KeyBindingCameraSpinUnder),
990                    "pan_up" => Ok(ViewerUri::KeyBindingCameraPanUp),
991                    "pan_down" => Ok(ViewerUri::KeyBindingCameraPanDown),
992                    "pan_left" => Ok(ViewerUri::KeyBindingCameraPanLeft),
993                    "pan_right" => Ok(ViewerUri::KeyBindingCameraPanRight),
994                    "pan_in" => Ok(ViewerUri::KeyBindingCameraPanIn),
995                    "pan_out" => Ok(ViewerUri::KeyBindingCameraPanOut),
996                    "spin_around_ccw_sitting" => {
997                        Ok(ViewerUri::KeyBindingCameraSpinAroundCounterClockwiseSitting)
998                    }
999                    "spin_around_cw_sitting" => {
1000                        Ok(ViewerUri::KeyBindingCameraSpinAroundClockwiseSitting)
1001                    }
1002                    "spin_around_ccw" => Ok(ViewerUri::KeyBindingCameraSpinAroundCounterClockwise),
1003                    "spin_around_cw" => Ok(ViewerUri::KeyBindingCameraSpinAroundClockwise),
1004                    "edit_avatar_spin_ccw" => {
1005                        Ok(ViewerUri::KeyBindingEditingAvatarSpinCounterClockwise)
1006                    }
1007                    "edit_avatar_spin_cw" => Ok(ViewerUri::KeyBindingEditingAvatarSpinClockwise),
1008                    "edit_avatar_spin_over" => Ok(ViewerUri::KeyBindingEditingAvatarSpinOver),
1009                    "edit_avatar_spin_under" => Ok(ViewerUri::KeyBindingEditingAvatarSpinUnder),
1010                    "edit_avatar_move_forward" => Ok(ViewerUri::KeyBindingEditingAvatarMoveForward),
1011                    "edit_avatar_move_backward" => {
1012                        Ok(ViewerUri::KeyBindingEditingAvatarMoveBackward)
1013                    }
1014                    "toggle_pause_media" => Ok(ViewerUri::KeyBindingSoundAndMediaTogglePauseMedia),
1015                    "toggle_enable_media" => {
1016                        Ok(ViewerUri::KeyBindingSoundAndMediaToggleEnableMedia)
1017                    }
1018                    "voice_follow_key" => Ok(ViewerUri::KeyBindingSoundAndMediaVoiceFollowKey),
1019                    "toggle_voice" => Ok(ViewerUri::KeyBindingSoundAndMediaToggleVoice),
1020                    "start_chat" => Ok(ViewerUri::KeyBindingStartChat),
1021                    "start_gesture" => Ok(ViewerUri::KeyBindingStartGesture),
1022                    _ => Err(chumsky::error::Rich::custom(
1023                        span,
1024                        format!("Not a valid keybinding: {s}"),
1025                    )),
1026                })
1027                .or(just("/script_trigger_lbutton")
1028                    .ignore_then(script_trigger_mode_parser())
1029                    .map(ViewerUri::KeyBindingScriptTriggerLButton)),
1030        )
1031        .labelled("viewer app keybinding URI")
1032}
1033
1034/// parse a viewer app login URI
1035///
1036/// # Errors
1037///
1038/// returns an error if the string could not be parsed
1039#[cfg(feature = "chumsky")]
1040#[must_use]
1041pub fn viewer_app_login_uri_parser<'src>()
1042-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1043    just("secondlife:///app/login?first=")
1044        .ignore_then(url_text_component_parser())
1045        .then(just("?last=").ignore_then(url_text_component_parser()))
1046        .then(just("?session=").ignore_then(url_text_component_parser()))
1047        .then(
1048            just("?location=")
1049                .ignore_then(url_text_component_parser())
1050                .or_not(),
1051        )
1052        .map(
1053            |(((first_name, last_name), session), login_location)| ViewerUri::Login {
1054                first_name,
1055                last_name,
1056                session,
1057                login_location,
1058            },
1059        )
1060        .labelled("viewer app login URI")
1061}
1062
1063/// parse a viewer app maptrackavatar URI
1064///
1065/// # Errors
1066///
1067/// returns an error if the string could not be parsed
1068#[cfg(feature = "chumsky")]
1069#[must_use]
1070pub fn viewer_app_maptrackavatar_uri_parser<'src>()
1071-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1072    just("secondlife:///app/maptrackavatar/")
1073        .ignore_then(crate::key::friend_key_parser().map(ViewerUri::MapTrackAvatar))
1074        .labelled("viewer app maptrackavatar URI")
1075}
1076
1077/// parse a viewer app objectim URI
1078///
1079/// # Errors
1080///
1081/// returns an error if the string could not be parsed
1082#[cfg(feature = "chumsky")]
1083#[must_use]
1084pub fn viewer_app_objectim_uri_parser<'src>()
1085-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1086    just("secondlife:///app/objectim/")
1087        .ignore_then(crate::key::object_key_parser())
1088        .then_ignore(just('/').or_not())
1089        .then(just("?name=").ignore_then(url_text_component_parser()))
1090        .then(
1091            just("&owner=")
1092                .ignore_then(crate::key::group_key_parser())
1093                .then_ignore(just("&groupowned=true"))
1094                .map(crate::key::OwnerKey::Group)
1095                .or(just("&owner=")
1096                    .ignore_then(crate::key::agent_key_parser())
1097                    .map(crate::key::OwnerKey::Agent)),
1098        )
1099        .then(just("&slurl=").ignore_then(crate::map::url_encoded_location_parser()))
1100        .map(
1101            |(((object_key, object_name), owner), location)| ViewerUri::ObjectInstantMessage {
1102                object_key,
1103                object_name,
1104                owner,
1105                location,
1106            },
1107        )
1108        .labelled("viewer app objectim URI")
1109}
1110
1111/// parse a viewer app openfloater URI
1112///
1113/// # Errors
1114///
1115/// returns an error if the string could not be parsed
1116#[cfg(feature = "chumsky")]
1117#[must_use]
1118pub fn viewer_app_openfloater_uri_parser<'src>()
1119-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1120    just("secondlife:///app/openfloater/")
1121        .ignore_then(url_text_component_parser().map(ViewerUri::OpenFloater))
1122        .labelled("viewer app openfloater URI")
1123}
1124
1125/// parse a viewer app parcel URI
1126///
1127/// # Errors
1128///
1129/// returns an error if the string could not be parsed
1130#[cfg(feature = "chumsky")]
1131#[must_use]
1132pub fn viewer_app_parcel_uri_parser<'src>()
1133-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1134    just("secondlife:///app/parcel/")
1135        .ignore_then(crate::key::parcel_key_parser().map(ViewerUri::Parcel))
1136        .labelled("viewer app parcel URI")
1137}
1138
1139/// parse a viewer app search URI
1140///
1141/// # Errors
1142///
1143/// returns an error if the string could not be parsed
1144#[cfg(feature = "chumsky")]
1145#[must_use]
1146pub fn viewer_app_search_uri_parser<'src>()
1147-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1148    just("secondlife:///app/search/")
1149        .ignore_then(crate::search::search_category_parser())
1150        .then_ignore(just('/'))
1151        .then(url_text_component_parser())
1152        .map(|(category, search_term)| ViewerUri::Search {
1153            category,
1154            search_term,
1155        })
1156        .labelled("viewer app search URI")
1157}
1158
1159/// parse a viewer app sharewithavatar URI
1160///
1161/// # Errors
1162///
1163/// returns an error if the string could not be parsed
1164#[cfg(feature = "chumsky")]
1165#[must_use]
1166pub fn viewer_app_sharewithavatar_uri_parser<'src>()
1167-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1168    just("secondlife:///app/sharewithavatar/")
1169        .ignore_then(crate::key::agent_key_parser().map(ViewerUri::ShareWithAvatar))
1170        .labelled("viewer app sharewithavatar URI")
1171}
1172/// parse a viewer app teleport URI
1173///
1174/// # Errors
1175///
1176/// returns an error if the string could not be parsed
1177#[cfg(feature = "chumsky")]
1178#[must_use]
1179pub fn viewer_app_teleport_uri_parser<'src>()
1180-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1181    just("secondlife:///app/teleport/")
1182        .ignore_then(crate::map::url_location_parser().map(ViewerUri::Teleport))
1183        .labelled("viewer app teleport URI")
1184}
1185
1186/// parse a viewer app voicecallavatar URI
1187///
1188/// # Errors
1189///
1190/// returns an error if the string could not be parsed
1191#[cfg(feature = "chumsky")]
1192#[must_use]
1193pub fn viewer_app_voicecallavatar_uri_parser<'src>()
1194-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1195    just("secondlife:///app/voicecallavatar/")
1196        .ignore_then(crate::key::agent_key_parser().map(ViewerUri::VoiceCallAvatar))
1197        .labelled("viewer app voicecallavatar URI")
1198}
1199
1200/// parse a viewer app wear_folder URI
1201///
1202/// # Errors
1203///
1204/// returns an error if the string could not be parsed
1205#[cfg(feature = "chumsky")]
1206#[must_use]
1207pub fn viewer_app_wear_folder_uri_parser<'src>()
1208-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1209    just("secondlife:///app/wear_folder")
1210        .ignore_then(
1211            just("?folder_id=")
1212                .ignore_then(crate::key::inventory_folder_key_parser())
1213                .map(ViewerUri::WearFolderByInventoryFolderKey)
1214                .or(just("?folder_name=")
1215                    .ignore_then(url_text_component_parser())
1216                    .map(ViewerUri::WearFolderByLibraryFolderName)),
1217        )
1218        .labelled("viewer app wear_folder URI")
1219}
1220
1221/// parse a viewer app worldmap URI
1222///
1223/// # Errors
1224///
1225/// returns an error if the string could not be parsed
1226#[cfg(feature = "chumsky")]
1227#[must_use]
1228pub fn viewer_app_worldmap_uri_parser<'src>()
1229-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1230    just("secondlife:///app/worldmap/")
1231        .ignore_then(crate::map::url_location_parser().map(ViewerUri::WorldMap))
1232        .labelled("viewer app worldmap URI")
1233}
1234
1235/// parse a viewer app URI
1236///
1237/// # Errors
1238///
1239/// returns an error if the string could not be parsed
1240#[cfg(feature = "chumsky")]
1241#[must_use]
1242pub fn viewer_app_uri_parser<'src>()
1243-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1244    viewer_app_agent_uri_parser()
1245        .or(viewer_app_appearance_uri_parser())
1246        .or(viewer_app_balance_uri_parser())
1247        .or(viewer_app_chat_uri_parser())
1248        .or(viewer_app_classified_uri_parser())
1249        .or(viewer_app_event_uri_parser())
1250        .or(viewer_app_experience_uri_parser())
1251        .or(viewer_app_group_uri_parser())
1252        .or(viewer_app_help_uri_parser())
1253        .or(viewer_app_inventory_uri_parser())
1254        .or(viewer_app_keybinding_uri_parser())
1255        .or(viewer_app_login_uri_parser())
1256        .or(viewer_app_maptrackavatar_uri_parser())
1257        .or(viewer_app_objectim_uri_parser())
1258        .or(viewer_app_openfloater_uri_parser())
1259        .or(viewer_app_parcel_uri_parser())
1260        .or(viewer_app_search_uri_parser())
1261        .or(viewer_app_sharewithavatar_uri_parser())
1262        .or(viewer_app_teleport_uri_parser())
1263        .or(viewer_app_voicecallavatar_uri_parser())
1264        .or(viewer_app_wear_folder_uri_parser())
1265        .or(viewer_app_worldmap_uri_parser())
1266        .labelled("viewer app URI")
1267}
1268
1269/// parse a viewer location URI
1270///
1271/// # Errors
1272///
1273/// returns an error if the string could not be parsed
1274#[cfg(feature = "chumsky")]
1275#[must_use]
1276pub fn viewer_location_uri_parser<'src>()
1277-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1278    just("secondlife:///")
1279        .ignore_then(crate::map::url_location_parser())
1280        .map(ViewerUri::Location)
1281        .labelled("viewer location URI")
1282}
1283
1284/// parse a viewer URI
1285///
1286/// # Errors
1287///
1288/// returns an error if the string could not be parsed
1289#[cfg(feature = "chumsky")]
1290#[must_use]
1291#[expect(
1292    clippy::module_name_repetitions,
1293    reason = "the parse is used outside this module"
1294)]
1295pub fn viewer_uri_parser<'src>()
1296-> impl Parser<'src, &'src str, ViewerUri, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1297    viewer_app_uri_parser()
1298        .or(viewer_location_uri_parser())
1299        .labelled("viewer URI")
1300}