Skip to main content

perforce_cli/cmd/
clients.rs

1use std::{
2    path::{Path, PathBuf},
3    process::{Child, Command, Stdio},
4};
5
6use super::{ExclusiveOption, SubCommand, Unselected};
7
8use crate::global::GlobalOpts;
9use crate::spawn::ParameterizedSpawn;
10
11// ---------------------------------------------------------------------------
12// Name filter (-e / -E)
13// ---------------------------------------------------------------------------
14
15/// The `-e` / `-E` name filter: list only client workspaces whose name
16/// matches the filter pattern.
17///
18/// Created by [`Clients::filter`]; the `case_insensitive` parameter selects
19/// [`Filter::CaseSensitiveMode`] (`-e`) or [`Filter::InCaseSensitiveMode`]
20/// (`-E`).
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum Filter {
23    /// `-e`: match the pattern using the server's normal case-sensitivity
24    /// rules.
25    CaseSensitiveMode(String),
26
27    /// `-E`: match the pattern case-insensitively, even on a case-sensitive
28    /// server.
29    InCaseSensitiveMode(String),
30}
31
32impl ExclusiveOption for Filter {
33    fn inject_args(&self, command: &mut Command) {
34        match self {
35            Filter::CaseSensitiveMode(name) => {
36                command.arg("-e").arg(name);
37            }
38            Filter::InCaseSensitiveMode(name) => {
39                command.arg("-E").arg(name);
40            }
41        }
42    }
43}
44
45// ---------------------------------------------------------------------------
46// Owner filter states (-u user / --me)
47// ---------------------------------------------------------------------------
48
49/// The `-u user` state: list only client workspaces owned by the named user.
50///
51/// Created by [`Clients::user`]; call [`Clients::case_insensitive`] on the
52/// entered state to add the `--user-case-insensitive` suboption.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct User {
55    user: String,
56
57    #[cfg(not(feature = "lt2024_2"))]
58    case_insensitive: bool,
59}
60
61impl ExclusiveOption for User {
62    fn inject_args(&self, command: &mut Command) {
63        command.arg("-u").arg(&self.user);
64        #[cfg(not(feature = "lt2024_2"))]
65        if self.case_insensitive {
66            command.arg("--user-case-insensitive");
67        }
68    }
69}
70
71/// The `--me` state: list only client workspaces owned by the current user
72/// (equivalent to `-u $P4USER`).
73///
74/// Created by [`Clients::filter_me`].
75#[cfg(not(feature = "lt2016_1"))]
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Me {}
78
79#[cfg(not(feature = "lt2016_1"))]
80impl ExclusiveOption for Me {
81    fn inject_args(&self, command: &mut Command) {
82        command.arg("--me");
83    }
84}
85
86// ---------------------------------------------------------------------------
87// List category states (-a / -s serverID)
88// ---------------------------------------------------------------------------
89
90/// The `-a` state: list all client workspaces, not just workspaces bound to
91/// this server.
92///
93/// Created by [`Clients::all`].
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct ListAll {}
96
97impl ExclusiveOption for ListAll {
98    fn inject_args(&self, command: &mut Command) {
99        command.arg("-a");
100    }
101}
102
103/// The `-s serverID` state: list only client workspaces bound to the
104/// specified server.
105///
106/// Created by [`Clients::server_id`].
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ListSpecified {
109    server_id: String,
110}
111
112impl ExclusiveOption for ListSpecified {
113    fn inject_args(&self, command: &mut Command) {
114        command.arg("-s").arg(&self.server_id);
115    }
116}
117
118// ---------------------------------------------------------------------------
119// Mode structs
120// ---------------------------------------------------------------------------
121
122/// The regular listing form: `p4 clients [-t] [-u user | --me] [[-e|-E]
123/// filter] [-m max] [-S stream] [-a | -s serverID]`.
124///
125/// Entered from [`Clients<Unselected>`](Clients) by setting any regular
126/// option or filter, for example [`Clients::display_time`],
127/// [`Clients::filter`], or [`Clients::user`].
128///
129/// The `F`, `U`, and `C` type parameters track the mutually exclusive option
130/// groups at compile time, each starting as [`Unselected`]:
131/// - `F`: the `-e` / `-E` name filter, entered via [`Clients::filter`]
132///   ([`Filter`]).
133/// - `U`: the `-u user` / `--me` owner filter, entered via [`Clients::user`]
134///   ([`User`]) or [`Clients::filter_me`] ([`Me`]).
135/// - `C`: the `-a` / `-s serverID` list category, entered via
136///   [`Clients::all`] ([`ListAll`]) or [`Clients::server_id`]
137///   ([`ListSpecified`]).
138///
139/// Each group can be set at most once; the remaining options stay available
140/// afterwards.
141#[derive(Debug, Clone)]
142pub struct RegularMode<F = Unselected, U = Unselected, C = Unselected> {
143    display_time: bool,
144
145    limit: Option<u64>,
146
147    stream: Option<String>,
148
149    name_filter: F,
150
151    user_filter: U,
152
153    list_category: C,
154}
155
156/// The unloaded listing form: `p4 clients -U`.
157///
158/// List only client workspaces unloaded with `p4 unload`. This form is
159/// entered via [`Clients::unloaded`] and accepts no other options.
160#[derive(Debug, Clone, Default)]
161pub struct UnloadedMode {}
162
163// ---------------------------------------------------------------------------
164// Command struct
165// ---------------------------------------------------------------------------
166
167#[cfg_attr(
168    feature = "lt2015_1",
169    doc = "`p4 [g-opts] clients [-t] [ -u user ] [ [-e|-E] filter -m max ] -S stream [-a | -s serverID]`"
170)]
171#[cfg_attr(
172    all(feature = "lt2016_1", not(feature = "lt2015_1")),
173    doc = "`p4 [g-opts] clients [-t] [-u user] [[-e|-E] filter] [-m max] [-S stream] [-a | -s serverID]`"
174)]
175#[cfg_attr(
176    all(feature = "lt2024_2", not(feature = "lt2016_1")),
177    doc = "`p4 [g-opts] clients [-t] [-u user | --me] [[-e|-E] filter] [-m max] [-S stream] [-a | -s serverID]`"
178)]
179#[cfg_attr(
180    not(feature = "lt2024_2"),
181    doc = "`p4 [g-opts] clients [-t] [-u user [--user-case-insensitive] | --me] [[-e|-E] filter] [-m max] [-S stream] [-a | -s serverID]`"
182)]
183///
184/// `p4 [g-opts] clients -U`
185///
186/// List all client workspaces currently known to the system.
187///
188/// The command `p4 workspaces` is an alias for `p4 clients`.
189///
190/// The `M` type parameter tracks the command form at compile time, starting
191/// as [`Unselected`] from [`Clients::new`], which spawns the bare `p4
192/// clients` report. Set any regular option or filter (for example
193/// [`Self::display_time`], [`Self::filter`], or [`Self::user`]) to enter
194/// [`RegularMode`], or call [`Self::unloaded`] (`-U`) to enter
195/// [`UnloadedMode`].
196#[derive(Debug, Clone, Default)]
197pub struct Clients<M = Unselected> {
198    bin: PathBuf,
199
200    global_opts: GlobalOpts,
201
202    mode: M,
203}
204
205// ---------------------------------------------------------------------------
206// Constructor
207// ---------------------------------------------------------------------------
208
209impl Clients<Unselected> {
210    /// Creates a new `p4 clients` command with no option selected.
211    ///
212    /// `bin` is the path to the Perforce command-line executable. The
213    /// command spawns the bare `p4 clients` report as is; set any regular
214    /// option or filter to enter [`RegularMode`], or call
215    /// [`Self::unloaded`] to enter [`UnloadedMode`].
216    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
217        Self {
218            bin: bin.into(),
219            global_opts,
220            ..Default::default()
221        }
222    }
223
224    /// # Description
225    ///
226    /// -U
227    ///
228    /// List only client workspaces unloaded with `p4 unload`.
229    ///
230    /// This is a standalone syntax form (`p4 clients -U`) that accepts no
231    /// other options.
232    ///
233    /// Transitions this command to the [`UnloadedMode`] state.
234    pub fn unloaded(self) -> Clients<UnloadedMode> {
235        Clients {
236            bin: self.bin,
237            global_opts: self.global_opts,
238            mode: UnloadedMode {},
239        }
240    }
241
242    /// # Description
243    ///
244    /// -t
245    ///
246    /// Display the time as well as the date of the last update to the
247    /// workspace.
248    ///
249    /// Transitions this command to the [`RegularMode`] state.
250    pub fn display_time(self, v: bool) -> Clients<RegularMode> {
251        Clients {
252            bin: self.bin,
253            global_opts: self.global_opts,
254            mode: RegularMode {
255                display_time: v,
256                limit: None,
257                stream: None,
258                name_filter: Unselected,
259                user_filter: Unselected,
260                list_category: Unselected,
261            },
262        }
263    }
264
265    /// # Description
266    ///
267    /// -m max
268    ///
269    /// List only the first `max` client workspaces.
270    ///
271    /// Transitions this command to the [`RegularMode`] state.
272    pub fn limit(self, v: u64) -> Clients<RegularMode> {
273        Clients {
274            bin: self.bin,
275            global_opts: self.global_opts,
276            mode: RegularMode {
277                display_time: false,
278                limit: Some(v),
279                stream: None,
280                name_filter: Unselected,
281                user_filter: Unselected,
282                list_category: Unselected,
283            },
284        }
285    }
286
287    /// # Description
288    ///
289    /// -S stream
290    ///
291    /// List client workspaces associated with the specified stream.
292    ///
293    /// Transitions this command to the [`RegularMode`] state.
294    pub fn stream(self, v: impl Into<String>) -> Clients<RegularMode> {
295        Clients {
296            bin: self.bin,
297            global_opts: self.global_opts,
298            mode: RegularMode {
299                display_time: false,
300                limit: None,
301                stream: Some(v.into()),
302                name_filter: Unselected,
303                user_filter: Unselected,
304                list_category: Unselected,
305            },
306        }
307    }
308
309    /// # Description
310    ///
311    /// -e filter / -E filter
312    ///
313    /// `v` is the client workspace name filter pattern; lists only client
314    /// workspaces whose name matches it.
315    ///
316    /// When `case_insensitive` is `false` (`-e`), the pattern is matched
317    /// using the server's normal case-sensitivity rules. When
318    /// `case_insensitive` is `true` (`-E`), the pattern is matched
319    /// case-insensitively, even on a case-sensitive server.
320    #[cfg_attr(
321        not(feature = "lt2024_2"),
322        doc = "",
323        doc = "`--client-case-insensitive` is equivalent to `-E`."
324    )]
325    ///
326    /// Transitions this command to the [`Filter`] state.
327    pub fn filter(
328        self,
329        v: impl Into<String>,
330        case_insensitive: bool,
331    ) -> Clients<RegularMode<Filter, Unselected, Unselected>> {
332        Clients {
333            bin: self.bin,
334            global_opts: self.global_opts,
335            mode: RegularMode {
336                display_time: false,
337                limit: None,
338                stream: None,
339                name_filter: if case_insensitive {
340                    Filter::InCaseSensitiveMode(v.into())
341                } else {
342                    Filter::CaseSensitiveMode(v.into())
343                },
344                user_filter: Unselected,
345                list_category: Unselected,
346            },
347        }
348    }
349
350    /// # Description
351    ///
352    #[cfg_attr(feature = "lt2024_2", doc = "-u user")]
353    #[cfg_attr(not(feature = "lt2024_2"), doc = "-u user [--user-case-insensitive]")]
354    ///
355    #[cfg_attr(
356        feature = "lt2024_2",
357        doc = "List only client workspaces owned by `v`."
358    )]
359    #[cfg_attr(
360        not(feature = "lt2024_2"),
361        doc = "List client workspaces that are owned by `v`. This can include `*` wildcards to form a search pattern. If wildcards are used, enclose the search pattern in double quotes (`\"`).",
362        doc = "",
363        doc = "For example:",
364        doc = "",
365        doc = "`p4 clients -u \"O*hara\"`",
366        doc = "",
367        doc = "Call [`Clients::case_insensitive`] on the entered state to add the `--user-case-insensitive` suboption."
368    )]
369    ///
370    /// Transitions this command to the [`User`] state.
371    pub fn user(self, v: impl Into<String>) -> Clients<RegularMode<Unselected, User, Unselected>> {
372        Clients {
373            bin: self.bin,
374            global_opts: self.global_opts,
375            mode: RegularMode {
376                display_time: false,
377                limit: None,
378                stream: None,
379                name_filter: Unselected,
380                user_filter: User {
381                    user: v.into(),
382                    #[cfg(not(feature = "lt2024_2"))]
383                    case_insensitive: false,
384                },
385                list_category: Unselected,
386            },
387        }
388    }
389
390    /// # Description
391    ///
392    /// --me
393    ///
394    /// Equivalent to `-u $P4USER`.
395    ///
396    /// Transitions this command to the [`Me`] state.
397    #[cfg(not(feature = "lt2016_1"))]
398    pub fn filter_me(self) -> Clients<RegularMode<Unselected, Me, Unselected>> {
399        Clients {
400            bin: self.bin,
401            global_opts: self.global_opts,
402            mode: RegularMode {
403                display_time: false,
404                limit: None,
405                stream: None,
406                name_filter: Unselected,
407                user_filter: Me {},
408                list_category: Unselected,
409            },
410        }
411    }
412
413    /// # Description
414    ///
415    /// -a
416    ///
417    /// List all client workspaces, not just workspaces bound to this server.
418    ///
419    /// Transitions this command to the [`ListAll`] state.
420    pub fn all(self) -> Clients<RegularMode<Unselected, Unselected, ListAll>> {
421        Clients {
422            bin: self.bin,
423            global_opts: self.global_opts,
424            mode: RegularMode {
425                display_time: false,
426                limit: None,
427                stream: None,
428                name_filter: Unselected,
429                user_filter: Unselected,
430                list_category: ListAll {},
431            },
432        }
433    }
434
435    /// # Description
436    ///
437    /// -s serverID
438    ///
439    /// List only client workspaces bound to the specified `serverID`. On an
440    /// edge server, the `-s` option defaults to the edge server's serverID.
441    ///
442    /// Transitions this command to the [`ListSpecified`] state.
443    pub fn server_id(
444        self,
445        v: impl Into<String>,
446    ) -> Clients<RegularMode<Unselected, Unselected, ListSpecified>> {
447        Clients {
448            bin: self.bin,
449            global_opts: self.global_opts,
450            mode: RegularMode {
451                display_time: false,
452                limit: None,
453                stream: None,
454                name_filter: Unselected,
455                user_filter: Unselected,
456                list_category: ListSpecified {
457                    server_id: v.into(),
458                },
459            },
460        }
461    }
462}
463
464// ---------------------------------------------------------------------------
465// Shared accessors
466// ---------------------------------------------------------------------------
467
468impl<M> Clients<M> {
469    /// Get the Perforce command path.
470    pub fn get_bin(&self) -> &Path {
471        &self.bin
472    }
473
474    /// Set the Perforce command path.
475    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
476        self.bin = bin.into();
477        self
478    }
479
480    /// Set the Perforce command path.
481    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
482        self.set_bin(bin);
483        self
484    }
485
486    /// # Description
487    ///
488    /// g-opts
489    ///
490    #[cfg_attr(
491        feature = "lt2014_2",
492        doc = "See the [Global Options](GlobalOpts) section."
493    )]
494    #[cfg_attr(
495        all(feature = "lt2015_1", not(feature = "lt2014_2")),
496        doc = "See the [“Global Options”](GlobalOpts) section."
497    )]
498    #[cfg_attr(
499        all(feature = "lt2017_1", not(feature = "lt2015_1")),
500        doc = "See [“Global Options”](GlobalOpts)."
501    )]
502    #[cfg_attr(
503        all(feature = "lt2018_2", not(feature = "lt2017_1")),
504        doc = "See [Global Options](GlobalOpts)."
505    )]
506    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
507    pub fn get_global_opts(&self) -> &GlobalOpts {
508        &self.global_opts
509    }
510
511    /// # Description
512    ///
513    /// g-opts
514    ///
515    #[cfg_attr(
516        feature = "lt2014_2",
517        doc = "See the [Global Options](GlobalOpts) section."
518    )]
519    #[cfg_attr(
520        all(feature = "lt2015_1", not(feature = "lt2014_2")),
521        doc = "See the [“Global Options”](GlobalOpts) section."
522    )]
523    #[cfg_attr(
524        all(feature = "lt2017_1", not(feature = "lt2015_1")),
525        doc = "See [“Global Options”](GlobalOpts)."
526    )]
527    #[cfg_attr(
528        all(feature = "lt2018_2", not(feature = "lt2017_1")),
529        doc = "See [Global Options](GlobalOpts)."
530    )]
531    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
532    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
533        self.global_opts = v;
534        self
535    }
536
537    /// # Description
538    ///
539    /// g-opts
540    ///
541    #[cfg_attr(
542        feature = "lt2014_2",
543        doc = "See the [Global Options](GlobalOpts) section."
544    )]
545    #[cfg_attr(
546        all(feature = "lt2015_1", not(feature = "lt2014_2")),
547        doc = "See the [“Global Options”](GlobalOpts) section."
548    )]
549    #[cfg_attr(
550        all(feature = "lt2017_1", not(feature = "lt2015_1")),
551        doc = "See [“Global Options”](GlobalOpts)."
552    )]
553    #[cfg_attr(
554        all(feature = "lt2018_2", not(feature = "lt2017_1")),
555        doc = "See [Global Options](GlobalOpts)."
556    )]
557    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
558    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
559        self.global_opts = v;
560        self
561    }
562}
563
564// ---------------------------------------------------------------------------
565// Regular form accessors
566// ---------------------------------------------------------------------------
567
568impl<F, U, C> Clients<RegularMode<F, U, C>> {
569    /// # Description
570    ///
571    /// -m max
572    ///
573    /// List only the first `max` client workspaces.
574    pub fn get_limit(&self) -> Option<u64> {
575        self.mode.limit
576    }
577
578    /// # Description
579    ///
580    /// -m max
581    ///
582    /// List only the first `max` client workspaces.
583    pub fn set_limit(&mut self, v: u64) -> &mut Self {
584        self.mode.limit = Some(v);
585        self
586    }
587
588    /// # Description
589    ///
590    /// -m max
591    ///
592    /// List only the first `max` client workspaces.
593    pub fn limit(mut self, v: u64) -> Self {
594        self.mode.limit = Some(v);
595        self
596    }
597
598    /// # Description
599    ///
600    /// -t
601    ///
602    /// Display the time as well as the date of the last update to the
603    /// workspace.
604    pub fn get_display_time(&self) -> bool {
605        self.mode.display_time
606    }
607
608    /// # Description
609    ///
610    /// -t
611    ///
612    /// Display the time as well as the date of the last update to the
613    /// workspace.
614    pub fn set_display_time(&mut self, v: bool) -> &mut Self {
615        self.mode.display_time = v;
616        self
617    }
618
619    /// # Description
620    ///
621    /// -t
622    ///
623    /// Display the time as well as the date of the last update to the
624    /// workspace.
625    pub fn display_time(mut self, v: bool) -> Self {
626        self.mode.display_time = v;
627        self
628    }
629
630    /// # Description
631    ///
632    /// -S stream
633    ///
634    /// List client workspaces associated with the specified stream.
635    pub fn get_stream(&self) -> Option<&String> {
636        self.mode.stream.as_ref()
637    }
638
639    /// # Description
640    ///
641    /// -S stream
642    ///
643    /// List client workspaces associated with the specified stream.
644    pub fn set_stream(&mut self, v: impl Into<String>) -> &mut Self {
645        self.mode.stream = Some(v.into());
646        self
647    }
648
649    /// # Description
650    ///
651    /// -S stream
652    ///
653    /// List client workspaces associated with the specified stream.
654    pub fn stream(mut self, v: impl Into<String>) -> Self {
655        self.mode.stream = Some(v.into());
656        self
657    }
658}
659
660// ---------------------------------------------------------------------------
661// Name filter transitions
662// ---------------------------------------------------------------------------
663
664impl<U, C> Clients<RegularMode<Unselected, U, C>> {
665    /// # Description
666    ///
667    /// -e filter / -E filter
668    ///
669    /// `v` is the client workspace name filter pattern; lists only client
670    /// workspaces whose name matches it.
671    ///
672    /// When `case_insensitive` is `false` (`-e`), the pattern is matched
673    /// using the server's normal case-sensitivity rules. When
674    /// `case_insensitive` is `true` (`-E`), the pattern is matched
675    /// case-insensitively, even on a case-sensitive server.
676    #[cfg_attr(
677        not(feature = "lt2024_2"),
678        doc = "",
679        doc = "`--client-case-insensitive` is equivalent to `-E`."
680    )]
681    ///
682    /// Transitions this command to the [`Filter`] state.
683    pub fn filter(
684        self,
685        v: impl Into<String>,
686        case_insensitive: bool,
687    ) -> Clients<RegularMode<Filter, U, C>> {
688        Clients {
689            bin: self.bin,
690            global_opts: self.global_opts,
691            mode: RegularMode {
692                display_time: self.mode.display_time,
693                limit: self.mode.limit,
694                stream: self.mode.stream,
695                user_filter: self.mode.user_filter,
696                list_category: self.mode.list_category,
697                name_filter: if case_insensitive {
698                    Filter::InCaseSensitiveMode(v.into())
699                } else {
700                    Filter::CaseSensitiveMode(v.into())
701                },
702            },
703        }
704    }
705}
706
707// ---------------------------------------------------------------------------
708// Owner filter transitions
709// ---------------------------------------------------------------------------
710
711impl<F, C> Clients<RegularMode<F, Unselected, C>> {
712    /// # Description
713    ///
714    #[cfg_attr(feature = "lt2024_2", doc = "-u user")]
715    #[cfg_attr(not(feature = "lt2024_2"), doc = "-u user [--user-case-insensitive]")]
716    ///
717    #[cfg_attr(
718        feature = "lt2024_2",
719        doc = "List only client workspaces owned by `v`."
720    )]
721    #[cfg_attr(
722        not(feature = "lt2024_2"),
723        doc = "List client workspaces that are owned by `v`. This can include `*` wildcards to form a search pattern. If wildcards are used, enclose the search pattern in double quotes (`\"`).",
724        doc = "",
725        doc = "For example:",
726        doc = "",
727        doc = "`p4 clients -u \"O*hara\"`",
728        doc = "",
729        doc = "Call [`Clients::case_insensitive`] on the entered state to add the `--user-case-insensitive` suboption."
730    )]
731    ///
732    /// Transitions this command to the [`User`] state.
733    pub fn user(self, v: impl Into<String>) -> Clients<RegularMode<F, User, C>> {
734        Clients {
735            bin: self.bin,
736            global_opts: self.global_opts,
737            mode: RegularMode {
738                display_time: self.mode.display_time,
739                limit: self.mode.limit,
740                stream: self.mode.stream,
741                name_filter: self.mode.name_filter,
742                list_category: self.mode.list_category,
743                user_filter: User {
744                    user: v.into(),
745                    #[cfg(not(feature = "lt2024_2"))]
746                    case_insensitive: false,
747                },
748            },
749        }
750    }
751
752    /// # Description
753    ///
754    /// --me
755    ///
756    /// Equivalent to `-u $P4USER`.
757    ///
758    /// Transitions this command to the [`Me`] state.
759    #[cfg(not(feature = "lt2016_1"))]
760    pub fn filter_me(self) -> Clients<RegularMode<F, Me, C>> {
761        Clients {
762            bin: self.bin,
763            global_opts: self.global_opts,
764            mode: RegularMode {
765                display_time: self.mode.display_time,
766                limit: self.mode.limit,
767                stream: self.mode.stream,
768                name_filter: self.mode.name_filter,
769                list_category: self.mode.list_category,
770                user_filter: Me {},
771            },
772        }
773    }
774}
775
776impl<F, C> Clients<RegularMode<F, User, C>> {
777    /// # Description
778    ///
779    /// --user-case-insensitive
780    ///
781    /// Indicates that the `-u` user value is a case-insensitive search
782    /// pattern, even on a case-sensitive server.
783    ///
784    /// For example:
785    ///
786    /// `p4 clients -u "O*hara" --user-case-insensitive`
787    #[cfg(not(feature = "lt2024_2"))]
788    pub fn case_insensitive(self) -> Clients<RegularMode<F, User, C>> {
789        Clients {
790            bin: self.bin,
791            global_opts: self.global_opts,
792            mode: RegularMode {
793                display_time: self.mode.display_time,
794                limit: self.mode.limit,
795                stream: self.mode.stream,
796                name_filter: self.mode.name_filter,
797                list_category: self.mode.list_category,
798                user_filter: User {
799                    user: self.mode.user_filter.user,
800                    case_insensitive: true,
801                },
802            },
803        }
804    }
805}
806
807// ---------------------------------------------------------------------------
808// List category transitions
809// ---------------------------------------------------------------------------
810
811impl<F, U> Clients<RegularMode<F, U, Unselected>> {
812    /// # Description
813    ///
814    /// -a
815    ///
816    /// List all client workspaces, not just workspaces bound to this server.
817    ///
818    /// Transitions this command to the [`ListAll`] state.
819    pub fn all(self) -> Clients<RegularMode<F, U, ListAll>> {
820        Clients {
821            bin: self.bin,
822            global_opts: self.global_opts,
823            mode: RegularMode {
824                display_time: self.mode.display_time,
825                limit: self.mode.limit,
826                stream: self.mode.stream,
827                name_filter: self.mode.name_filter,
828                user_filter: self.mode.user_filter,
829                list_category: ListAll {},
830            },
831        }
832    }
833
834    /// # Description
835    ///
836    /// -s serverID
837    ///
838    /// List only client workspaces bound to the specified `serverID`. On an
839    /// edge server, the `-s` option defaults to the edge server's serverID.
840    ///
841    /// Transitions this command to the [`ListSpecified`] state.
842    pub fn server_id(self, v: impl Into<String>) -> Clients<RegularMode<F, U, ListSpecified>> {
843        Clients {
844            bin: self.bin,
845            global_opts: self.global_opts,
846            mode: RegularMode {
847                display_time: self.mode.display_time,
848                limit: self.mode.limit,
849                stream: self.mode.stream,
850                name_filter: self.mode.name_filter,
851                user_filter: self.mode.user_filter,
852                list_category: ListSpecified {
853                    server_id: v.into(),
854                },
855            },
856        }
857    }
858}
859
860// ---------------------------------------------------------------------------
861// SubCommand
862// ---------------------------------------------------------------------------
863
864impl SubCommand for Clients<Unselected> {
865    fn name(&self) -> &str {
866        "clients"
867    }
868
869    fn inject_local_args(&self, _: &mut Command) {}
870
871    fn global_opts(&self) -> Option<&GlobalOpts> {
872        Some(&self.global_opts)
873    }
874}
875
876impl<F: ExclusiveOption, U: ExclusiveOption, C: ExclusiveOption> SubCommand
877    for Clients<RegularMode<F, U, C>>
878{
879    fn name(&self) -> &str {
880        "clients"
881    }
882
883    fn inject_local_args(&self, command: &mut Command) {
884        if self.mode.display_time {
885            command.arg("-t");
886        }
887        self.mode.user_filter.inject_args(command);
888        self.mode.name_filter.inject_args(command);
889        if let Some(max) = self.mode.limit {
890            command.arg("-m").arg(max.to_string());
891        }
892        if let Some(ref stream) = self.mode.stream {
893            command.arg("-S").arg(stream);
894        }
895        self.mode.list_category.inject_args(command);
896    }
897
898    fn global_opts(&self) -> Option<&GlobalOpts> {
899        Some(&self.global_opts)
900    }
901}
902
903impl SubCommand for Clients<UnloadedMode> {
904    fn name(&self) -> &str {
905        "clients"
906    }
907
908    fn inject_local_args(&self, command: &mut Command) {
909        command.arg("-U");
910    }
911
912    fn global_opts(&self) -> Option<&GlobalOpts> {
913        Some(&self.global_opts)
914    }
915}
916
917// ---------------------------------------------------------------------------
918// Spawning
919// ---------------------------------------------------------------------------
920
921impl ParameterizedSpawn<()> for Clients<Unselected> {
922    type Output = Child;
923    type Error = std::io::Error;
924
925    /// Spawns `p4 clients` as a child process with piped standard output
926    /// and error streams; use the returned [`Child`] handle to wait for it
927    /// or interact with it.
928    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
929        self.setup_command(&self.bin)
930            .stdout(Stdio::piped())
931            .stderr(Stdio::piped())
932            .spawn()
933    }
934}
935
936impl<F: ExclusiveOption, U: ExclusiveOption, C: ExclusiveOption> ParameterizedSpawn<()>
937    for Clients<RegularMode<F, U, C>>
938{
939    type Output = Child;
940    type Error = std::io::Error;
941
942    /// Spawns `p4 clients` as a child process with piped standard output
943    /// and error streams; use the returned [`Child`] handle to wait for it
944    /// or interact with it.
945    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
946        self.setup_command(&self.bin)
947            .stdout(Stdio::piped())
948            .stderr(Stdio::piped())
949            .spawn()
950    }
951}
952
953impl ParameterizedSpawn<()> for Clients<UnloadedMode> {
954    type Output = Child;
955    type Error = std::io::Error;
956
957    /// Spawns `p4 clients -U` as a child process with piped standard output
958    /// and error streams; use the returned [`Child`] handle to wait for it
959    /// or interact with it.
960    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
961        self.setup_command(&self.bin)
962            .stdout(Stdio::piped())
963            .stderr(Stdio::piped())
964            .spawn()
965    }
966}
967
968#[cfg(test)]
969mod tests {
970    use super::*;
971    use crate::cmd::args_of;
972
973    #[test]
974    fn bin_accessors_update_executable_path() {
975        let mut command = Clients::new("p4", GlobalOpts::new());
976        assert_eq!(command.get_bin(), Path::new("p4"));
977
978        command.set_bin("/usr/local/bin/p4");
979        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
980
981        let command = command.bin("p4.exe");
982        assert_eq!(command.get_bin(), Path::new("p4.exe"));
983    }
984
985    // Compile-time rejection: -e and -E are mutually exclusive by
986    // construction (the Filter enum holds exactly one variant, and no
987    // transition exists on Clients<RegularMode<Filter, ..>>).
988    // Compile-time rejection: -u and --me are mutually exclusive (user and
989    // filter_me exist only on the Unselected owner-filter state).
990    // Compile-time rejection: case_insensitive exists only on the User
991    // state, never on Me or Unselected.
992    // Compile-time rejection: -a and -s are mutually exclusive (all and
993    // server_id exist only on the Unselected list-category state).
994    // Compile-time rejection: -U is only reachable from Clients<Unselected>,
995    // and Clients<UnloadedMode> offers no other options.
996    // Each filter can be set at most once; limit, display_time, stream, and
997    // the other filters stay available afterwards.
998
999    #[test]
1000    fn without_options() {
1001        let clients = Clients::new("p4", GlobalOpts::new());
1002        assert_eq!(args_of(&clients.setup_command("p4")), ["clients"]);
1003    }
1004
1005    #[test]
1006    fn limit() {
1007        let clients = Clients::new("p4", GlobalOpts::new()).limit(5);
1008        assert_eq!(
1009            args_of(&clients.setup_command("p4")),
1010            ["clients", "-m", "5"]
1011        );
1012    }
1013
1014    #[test]
1015    fn display_time() {
1016        let clients = Clients::new("p4", GlobalOpts::new()).display_time(true);
1017        assert_eq!(args_of(&clients.setup_command("p4")), ["clients", "-t"]);
1018    }
1019
1020    #[test]
1021    fn display_time_can_be_unset() {
1022        let clients = Clients::new("p4", GlobalOpts::new()).display_time(false);
1023        assert_eq!(args_of(&clients.setup_command("p4")), ["clients"]);
1024    }
1025
1026    #[test]
1027    fn stream() {
1028        let clients = Clients::new("p4", GlobalOpts::new()).stream("//stream/main");
1029        assert_eq!(
1030            args_of(&clients.setup_command("p4")),
1031            ["clients", "-S", "//stream/main"]
1032        );
1033    }
1034
1035    #[test]
1036    fn filter_case_sensitive() {
1037        let clients = Clients::new("p4", GlobalOpts::new()).filter("p4vBeta*", false);
1038        assert_eq!(
1039            args_of(&clients.setup_command("p4")),
1040            ["clients", "-e", "p4vBeta*"]
1041        );
1042    }
1043
1044    #[test]
1045    fn filter_case_insensitive() {
1046        let clients = Clients::new("p4", GlobalOpts::new()).filter("*Us*rs", true);
1047        assert_eq!(
1048            args_of(&clients.setup_command("p4")),
1049            ["clients", "-E", "*Us*rs"]
1050        );
1051    }
1052
1053    #[test]
1054    fn user() {
1055        let clients = Clients::new("p4", GlobalOpts::new()).user("O*hara");
1056        assert_eq!(
1057            args_of(&clients.setup_command("p4")),
1058            ["clients", "-u", "O*hara"]
1059        );
1060    }
1061
1062    #[cfg(not(feature = "lt2024_2"))]
1063    #[test]
1064    fn user_case_insensitive() {
1065        let clients = Clients::new("p4", GlobalOpts::new())
1066            .user("O*hara")
1067            .case_insensitive();
1068        assert_eq!(
1069            args_of(&clients.setup_command("p4")),
1070            ["clients", "-u", "O*hara", "--user-case-insensitive"]
1071        );
1072    }
1073
1074    #[cfg(not(feature = "lt2016_1"))]
1075    #[test]
1076    fn filter_me() {
1077        let clients = Clients::new("p4", GlobalOpts::new()).filter_me();
1078        assert_eq!(args_of(&clients.setup_command("p4")), ["clients", "--me"]);
1079    }
1080
1081    #[test]
1082    fn all() {
1083        let clients = Clients::new("p4", GlobalOpts::new()).all();
1084        assert_eq!(args_of(&clients.setup_command("p4")), ["clients", "-a"]);
1085    }
1086
1087    #[test]
1088    fn server_id() {
1089        let clients = Clients::new("p4", GlobalOpts::new()).server_id("edge1");
1090        assert_eq!(
1091            args_of(&clients.setup_command("p4")),
1092            ["clients", "-s", "edge1"]
1093        );
1094    }
1095
1096    #[test]
1097    fn unloaded() {
1098        let clients = Clients::new("p4", GlobalOpts::new()).unloaded();
1099        assert_eq!(args_of(&clients.setup_command("p4")), ["clients", "-U"]);
1100    }
1101
1102    #[test]
1103    fn transitions_preserve_other_options() {
1104        let clients = Clients::new("p4", GlobalOpts::new())
1105            .user("O*hara")
1106            .filter("p4v*", false)
1107            .limit(5)
1108            .display_time(true)
1109            .stream("//stream/main")
1110            .all();
1111        assert_eq!(
1112            args_of(&clients.setup_command("p4")),
1113            [
1114                "clients",
1115                "-t",
1116                "-u",
1117                "O*hara",
1118                "-e",
1119                "p4v*",
1120                "-m",
1121                "5",
1122                "-S",
1123                "//stream/main",
1124                "-a"
1125            ]
1126        );
1127
1128        let clients = Clients::new("p4", GlobalOpts::new())
1129            .server_id("edge1")
1130            .filter("*Us*rs", true);
1131        assert_eq!(
1132            args_of(&clients.setup_command("p4")),
1133            ["clients", "-E", "*Us*rs", "-s", "edge1"]
1134        );
1135    }
1136
1137    #[test]
1138    fn setters_update_regular_options() {
1139        let mut clients = Clients::new("p4", GlobalOpts::new()).filter("p4v*", false);
1140        assert_eq!(clients.get_limit(), None);
1141        clients.set_limit(10);
1142        assert_eq!(clients.get_limit(), Some(10));
1143        assert!(!clients.get_display_time());
1144        clients.set_display_time(true);
1145        assert!(clients.get_display_time());
1146        assert_eq!(clients.get_stream(), None);
1147        clients.set_stream("//stream/dev");
1148        assert_eq!(clients.get_stream(), Some(&"//stream/dev".to_string()));
1149    }
1150
1151    #[test]
1152    fn all_options_order() {
1153        let clients = Clients::new("p4", GlobalOpts::new())
1154            .display_time(true)
1155            .user("O*hara")
1156            .filter("*Us*rs", true)
1157            .limit(5)
1158            .stream("//stream/main")
1159            .server_id("edge1");
1160        assert_eq!(
1161            args_of(&clients.setup_command("p4")),
1162            [
1163                "clients",
1164                "-t",
1165                "-u",
1166                "O*hara",
1167                "-E",
1168                "*Us*rs",
1169                "-m",
1170                "5",
1171                "-S",
1172                "//stream/main",
1173                "-s",
1174                "edge1"
1175            ]
1176        );
1177    }
1178
1179    #[test]
1180    fn global_opts_are_injected_once() {
1181        let clients = Clients::new("p4", GlobalOpts::new().port("localhost:1666"));
1182        assert_eq!(
1183            args_of(&clients.setup_command("p4")),
1184            ["-p", "localhost:1666", "clients"]
1185        );
1186
1187        let clients = Clients::new("p4", GlobalOpts::new().port("localhost:1666")).limit(1);
1188        assert_eq!(
1189            args_of(&clients.setup_command("p4")),
1190            ["-p", "localhost:1666", "clients", "-m", "1"]
1191        );
1192
1193        let clients = Clients::new("p4", GlobalOpts::new().port("localhost:1666")).unloaded();
1194        assert_eq!(
1195            args_of(&clients.setup_command("p4")),
1196            ["-p", "localhost:1666", "clients", "-U"]
1197        );
1198    }
1199}