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