Skip to main content

smux/
fzf.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use anyhow::{Context, Result, bail};
7
8use crate::config::{PickerBindings, PickerPreviewSettings};
9use crate::process::{CommandRunner, default_runner};
10use crate::ui::DisplayStyle;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum EntryKind {
14    Session,
15    Directory,
16    Project,
17    InvalidProject,
18}
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum SelectAction {
22    Open,
23    Delete,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct Entry {
28    pub kind: EntryKind,
29    pub label: String,
30    pub value: String,
31    pub preview: Option<String>,
32}
33
34impl Entry {
35    pub fn session(style: DisplayStyle, value: String) -> Self {
36        Self {
37            kind: EntryKind::Session,
38            label: style.session_label(&value),
39            value,
40            preview: None,
41        }
42    }
43
44    pub fn directory(style: DisplayStyle, value: String) -> Self {
45        Self {
46            kind: EntryKind::Directory,
47            label: style.directory_label(&value),
48            value,
49            preview: None,
50        }
51    }
52
53    pub fn project(style: DisplayStyle, value: String, preview: Option<String>) -> Self {
54        Self {
55            kind: EntryKind::Project,
56            label: style.project_label(&value),
57            value,
58            preview,
59        }
60    }
61
62    pub fn invalid_project(
63        style: DisplayStyle,
64        value: String,
65        error: &str,
66        preview: Option<String>,
67    ) -> Self {
68        Self {
69            kind: EntryKind::InvalidProject,
70            label: style.invalid_project_label(&value, error),
71            value,
72            preview,
73        }
74    }
75
76    fn encode(&self) -> String {
77        let kind = match self.kind {
78            EntryKind::Session => "session",
79            EntryKind::Directory => "folder",
80            EntryKind::Project => "project",
81            EntryKind::InvalidProject => "project-broken",
82        };
83
84        let preview = self
85            .preview
86            .as_deref()
87            .unwrap_or_default()
88            .replace(['\t', '\n', '\r'], " ");
89        format!("{kind}\t{}\t{}\t{preview}", self.value, self.label)
90    }
91
92    fn decode(line: &str) -> Result<Self> {
93        let mut parts = line.splitn(4, '\t');
94        let kind = parts.next().context("missing entry kind")?;
95        let value = parts.next().context("missing entry value")?.to_owned();
96        let label = parts.next().context("missing entry label")?.to_owned();
97        let preview = parts
98            .next()
99            .filter(|value| !value.is_empty())
100            .map(ToOwned::to_owned);
101
102        let kind = match kind {
103            "session" => EntryKind::Session,
104            "folder" => EntryKind::Directory,
105            "project" => EntryKind::Project,
106            "project-broken" => EntryKind::InvalidProject,
107            other => bail!("unknown picker entry kind: {other}"),
108        };
109
110        Ok(Self {
111            kind,
112            label,
113            value,
114            preview,
115        })
116    }
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct Selection {
121    pub action: SelectAction,
122    pub entry: Entry,
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct Choice {
127    pub kind: String,
128    pub label: String,
129    pub value: String,
130}
131
132impl Choice {
133    pub fn new(kind: impl Into<String>, label: String, value: String) -> Self {
134        Self {
135            kind: kind.into(),
136            label,
137            value,
138        }
139    }
140
141    fn encode(&self) -> String {
142        format!("{}\t{}\t{}", self.kind, self.value, self.label)
143    }
144
145    fn decode(line: &str) -> Result<Self> {
146        let mut parts = line.splitn(3, '\t');
147        let kind = parts.next().context("missing choice kind")?.to_owned();
148        let value = parts.next().context("missing choice value")?.to_owned();
149        let label = parts.next().context("missing choice label")?.to_owned();
150        Ok(Self { kind, label, value })
151    }
152}
153
154pub fn select(
155    entries: Vec<Entry>,
156    bindings: &PickerBindings,
157    preview: &PickerPreviewSettings,
158) -> Result<Option<Selection>> {
159    select_with_runner(default_runner(), entries, "smux> ", bindings, preview)
160}
161
162pub fn select_value(prompt: &str, choices: Vec<Choice>) -> Result<Option<String>> {
163    select_value_with_runner(default_runner(), prompt, choices)
164}
165
166struct TempInputFile {
167    path: PathBuf,
168}
169
170impl TempInputFile {
171    fn new(contents: &str) -> Result<Self> {
172        let mut path = std::env::temp_dir();
173        let nanos = SystemTime::now()
174            .duration_since(UNIX_EPOCH)
175            .context("system clock should be after unix epoch")?
176            .as_nanos();
177        path.push(format!("smux-fzf-{}-{nanos}.tsv", std::process::id()));
178        fs::write(&path, contents)
179            .with_context(|| format!("failed to write {}", path.display()))?;
180        Ok(Self { path })
181    }
182
183    fn shell_quoted_path(&self) -> String {
184        shell_quote(&self.path)
185    }
186}
187
188impl Drop for TempInputFile {
189    fn drop(&mut self) {
190        let _ = fs::remove_file(&self.path);
191    }
192}
193
194fn shell_quote(path: &Path) -> String {
195    shell_quote_str(&path.to_string_lossy())
196}
197
198fn shell_quote_str(value: &str) -> String {
199    format!("'{}'", value.replace('\'', "'\\''"))
200}
201
202fn cat_command(file: &TempInputFile) -> String {
203    format!("cat {}", file.shell_quoted_path())
204}
205
206fn filter_command(file: &TempInputFile, kind: &str) -> String {
207    if kind == "project" {
208        format!(
209            "awk -F '\\t' '$1 == \"project\" || $1 == \"project-broken\"' {}",
210            file.shell_quoted_path()
211        )
212    } else {
213        format!(
214            "awk -F '\\t' '$1 == \"{kind}\"' {}",
215            file.shell_quoted_path()
216        )
217    }
218}
219
220fn add_common_picker_args(args: &mut Vec<String>, prompt: &str, header: &str, bindings: &str) {
221    args.extend([
222        "--ansi".to_owned(),
223        "--delimiter".to_owned(),
224        "\t".to_owned(),
225        "--layout".to_owned(),
226        "reverse".to_owned(),
227        "--header".to_owned(),
228        header.to_owned(),
229        "--bind".to_owned(),
230        "tab:down,btab:up".to_owned(),
231        "--bind".to_owned(),
232        bindings.to_owned(),
233        "--with-nth".to_owned(),
234        "3".to_owned(),
235        "--nth".to_owned(),
236        "1,2,4".to_owned(),
237        "--prompt".to_owned(),
238        prompt.to_owned(),
239        "--no-sort".to_owned(),
240    ]);
241}
242
243fn add_preview_args(args: &mut Vec<String>, preview: &PickerPreviewSettings) {
244    let session_preview = preview.sessions.as_deref().unwrap_or(
245        "tmux list-panes -s -t \"$SMUX_PREVIEW_SESSION\" -F \"#{window_index}\t#{window_name}\t#{?window_active,1,0}\t#{pane_index}\t#{?pane_active,1,0}\t#{pane_current_command}\t#{pane_current_path}\" 2>/dev/null | awk -F '\t' 'BEGIN { current = \"\"; first_pane = 1; esc = sprintf(\"%c\", 27); reset = esc \"[0m\"; window_box = esc \"[38;2;26;33;36;48;2;149;192;202m\"; window_active_box = esc \"[38;2;26;33;36;48;2;81;156;174m\"; pane_box = esc \"[38;2;26;33;36;48;2;231;198;100m\"; pane_active_box = esc \"[38;2;26;33;36;48;2;243;150;96m\"; window_name = esc \"[38;2;149;192;202m\"; window_name_active = esc \"[1;38;2;118;204;224m\"; pane_text = esc \"[38;2;231;198;100m\"; pane_text_active = esc \"[1;38;2;243;150;96m\"; path_color = esc \"[2;38;2;149;192;202m\" } { if ($1 != current) { if (NR > 1) print \"\"; box = ($3 == \"1\" ? window_active_box : window_box); name = ($3 == \"1\" ? window_name_active : window_name); printf \"%s %s %s %s%s%s\\n\", box, $1, reset, name, $2, reset; current = $1; first_pane = 1 } pbox = ($5 == \"1\" ? pane_active_box : pane_box); ptext = ($5 == \"1\" ? pane_text_active : pane_text); printf \"\\n  %s %s %s %s%s%s\\n\", pbox, $4, reset, ptext, $6, reset; printf \"    %s%s%s\\n\", path_color, $7, reset }'",
246    );
247    let folder_preview = preview.folders.as_deref().unwrap_or(
248        "if command -v eza >/dev/null 2>&1; then eza --tree --level=2 --color=always --icons=always \"$SMUX_PREVIEW_PATH\"; else ls -la \"$SMUX_PREVIEW_PATH\"; fi",
249    );
250    let project_preview = preview.projects.as_deref().unwrap_or(
251        "if command -v bat >/dev/null 2>&1; then bat --style=plain --color=always --language=toml \"$SMUX_PREVIEW_FILE\"; else sed -n '1,200p' \"$SMUX_PREVIEW_FILE\"; fi",
252    );
253    let session_preview = shell_quote_str(session_preview);
254    let folder_preview = shell_quote_str(folder_preview);
255    let project_preview = shell_quote_str(project_preview);
256    let preview_command = format!(
257        "SMUX_SESSION_PREVIEW={session_preview} SMUX_FOLDER_PREVIEW={folder_preview} SMUX_PROJECT_PREVIEW={project_preview} sh -c 'kind=\"$1\"; value=\"$2\"; extra=\"$3\"; case \"$kind\" in session) SMUX_PREVIEW_KIND=\"$kind\" SMUX_PREVIEW_SESSION=\"$value\" sh -lc \"$SMUX_SESSION_PREVIEW\" ;; folder) SMUX_PREVIEW_PATH=\"$value\" SMUX_PREVIEW_KIND=\"$kind\" sh -lc \"$SMUX_FOLDER_PREVIEW\" ;; project|project-broken) if [ -n \"$extra\" ] && [ -f \"$extra\" ]; then SMUX_PREVIEW_KIND=\"$kind\" SMUX_PREVIEW_FILE=\"$extra\" sh -lc \"$SMUX_PROJECT_PREVIEW\"; else printf \"No preview available\\n\"; fi ;; *) printf \"No preview available\\n\" ;; esac' _ {{1}} {{2}} {{4}}"
258    );
259    args.extend([
260        "--preview".to_owned(),
261        preview_command,
262        "--preview-window".to_owned(),
263        "right:55%".to_owned(),
264    ]);
265}
266
267fn select_value_with_runner(
268    runner: Arc<dyn CommandRunner>,
269    prompt: &str,
270    choices: Vec<Choice>,
271) -> Result<Option<String>> {
272    let mut args = Vec::new();
273    let input = choices
274        .into_iter()
275        .map(|choice| choice.encode())
276        .collect::<Vec<_>>()
277        .join("\n")
278        + "\n";
279    let input_file = TempInputFile::new(&input)?;
280    let all_command = cat_command(&input_file);
281    let template_command = filter_command(&input_file, "template");
282    add_common_picker_args(
283        &mut args,
284        prompt,
285        "ctrl-x all  ctrl-t templates",
286        &format!(
287            "ctrl-x:change-prompt(template> )+clear-query+reload({all_command}),ctrl-t:change-prompt(template> )+clear-query+reload({template_command})"
288        ),
289    );
290    let output = runner
291        .run_capture_with_input("fzf", &args, &input)
292        .context("failed to launch fzf")?;
293
294    if output.status.code == Some(130) {
295        return Ok(None);
296    }
297
298    if !output.status.success {
299        bail!("fzf exited with status {:?}", output.status.code);
300    }
301
302    let selection = String::from_utf8(output.stdout).context("fzf output was not valid utf-8")?;
303    let selection = selection.trim_end();
304
305    if selection.is_empty() {
306        return Ok(None);
307    }
308
309    Ok(Some(Choice::decode(selection)?.value))
310}
311
312fn select_with_runner(
313    runner: Arc<dyn CommandRunner>,
314    entries: Vec<Entry>,
315    prompt: &str,
316    bindings: &PickerBindings,
317    preview: &PickerPreviewSettings,
318) -> Result<Option<Selection>> {
319    let mut args = Vec::new();
320    let input = entries
321        .into_iter()
322        .map(|entry| entry.encode())
323        .collect::<Vec<_>>()
324        .join("\n")
325        + "\n";
326    let input_file = TempInputFile::new(&input)?;
327    let all_command = cat_command(&input_file);
328    let session_command = filter_command(&input_file, "session");
329    let folder_command = filter_command(&input_file, "folder");
330    let project_command = filter_command(&input_file, "project");
331    add_common_picker_args(
332        &mut args,
333        prompt,
334        &format!(
335            "enter open  {delete} kill session  {reset} all  {sessions} sessions  {folders} folders  {projects} projects",
336            delete = bindings.delete_session,
337            reset = bindings.reset,
338            sessions = bindings.sessions,
339            folders = bindings.folders,
340            projects = bindings.projects,
341        ),
342        &format!(
343            "{reset}:change-prompt(smux> )+clear-query+reload({all_command}),{sessions}:change-prompt(session> )+clear-query+reload({session_command}),{folders}:change-prompt(folder> )+clear-query+reload({folder_command}),{projects}:change-prompt(project> )+clear-query+reload({project_command})",
344            reset = bindings.reset,
345            sessions = bindings.sessions,
346            folders = bindings.folders,
347            projects = bindings.projects,
348        ),
349    );
350    add_preview_args(&mut args, preview);
351    args.extend(["--expect".to_owned(), bindings.delete_session.clone()]);
352    let output = runner
353        .run_capture_with_input("fzf", &args, &input)
354        .context("failed to launch fzf")?;
355
356    if output.status.code == Some(130) {
357        return Ok(None);
358    }
359
360    if !output.status.success {
361        bail!("fzf exited with status {:?}", output.status.code);
362    }
363
364    let selection = String::from_utf8(output.stdout).context("fzf output was not valid utf-8")?;
365    let selection = selection.trim_end();
366
367    if selection.is_empty() {
368        return Ok(None);
369    }
370
371    let mut lines = selection.lines();
372    let first = lines
373        .next()
374        .context("fzf selection output was unexpectedly empty")?;
375    let (action, encoded_entry) = match lines.next() {
376        Some(encoded_entry) if !first.is_empty() => {
377            let action = match first {
378                key if key == bindings.delete_session => SelectAction::Delete,
379                other => bail!("unknown picker action: {other}"),
380            };
381            (action, encoded_entry)
382        }
383        Some(encoded_entry) => (SelectAction::Open, encoded_entry),
384        None => (SelectAction::Open, first),
385    };
386
387    Ok(Some(Selection {
388        action,
389        entry: Entry::decode(encoded_entry)?,
390    }))
391}
392
393#[cfg(test)]
394mod tests {
395    use std::sync::Arc;
396
397    use crate::process::{CommandOutput, CommandStatus, FakeCommandRunner};
398
399    use super::{
400        Choice, Entry, EntryKind, SelectAction, select_value_with_runner, select_with_runner,
401    };
402    use crate::config::{IconMode, PickerBindings, PickerPreviewSettings};
403    use crate::ui::DisplayStyle;
404
405    #[test]
406    fn entry_round_trip() {
407        let entry = Entry {
408            kind: EntryKind::Directory,
409            label: "dir      /tmp/example".to_owned(),
410            value: "/tmp/example".to_owned(),
411            preview: None,
412        };
413
414        let decoded = Entry::decode(&entry.encode()).expect("entry should decode");
415        assert_eq!(decoded, entry);
416    }
417
418    #[test]
419    fn selector_passes_entries_to_fzf() {
420        let runner = Arc::new(FakeCommandRunner::new());
421        runner.push_capture(Ok(CommandOutput {
422            status: CommandStatus {
423                success: true,
424                code: Some(0),
425            },
426            stdout: b"folder\t/tmp/example\tdir      /tmp/example\n".to_vec(),
427            stderr: Vec::new(),
428        }));
429
430        let result = select_with_runner(
431            runner.clone(),
432            vec![Entry::directory(
433                DisplayStyle::from_icon_mode(IconMode::Never),
434                "/tmp/example".to_owned(),
435            )],
436            "smux> ",
437            &PickerBindings::default(),
438            &PickerPreviewSettings::default(),
439        )
440        .expect("selection should succeed");
441
442        assert!(result.is_some());
443        let recorded = runner.recorded();
444        assert_eq!(recorded[0].program, "fzf");
445        assert!(recorded[0].args.contains(&"--ansi".to_owned()));
446        assert!(recorded[0].args.contains(&"reverse".to_owned()));
447        assert!(recorded[0].args.contains(&"3".to_owned()));
448        assert!(recorded[0].args.contains(&"1,2,4".to_owned()));
449        assert!(recorded[0].args.contains(&"--expect".to_owned()));
450        assert!(recorded[0].args.contains(&"--preview".to_owned()));
451        assert!(recorded[0].args.contains(&"ctrl-x".to_owned()));
452        assert!(
453            recorded[0]
454                .args
455                .iter()
456                .any(|arg| arg.contains("enter open  ctrl-x kill session"))
457        );
458        assert!(
459            recorded[0]
460                .args
461                .iter()
462                .any(|arg| arg.contains("ctrl-c:change-prompt(smux> )+clear-query+reload("))
463        );
464        assert!(
465            recorded[0]
466                .args
467                .iter()
468                .any(|arg| arg.contains("ctrl-p projects"))
469        );
470        assert!(
471            recorded[0]
472                .args
473                .iter()
474                .any(|arg| arg.contains("ctrl-p:change-prompt(project> )+clear-query+reload("))
475        );
476        assert_eq!(
477            recorded[0].stdin.as_deref(),
478            Some("folder\t/tmp/example\tdir      /tmp/example\t\n")
479        );
480        let selection = result.expect("selection should be present");
481        assert_eq!(selection.action, SelectAction::Open);
482        assert_eq!(selection.entry.kind, EntryKind::Directory);
483    }
484
485    #[test]
486    fn selector_supports_delete_action_for_sessions() {
487        let runner = Arc::new(FakeCommandRunner::new());
488        runner.push_capture(Ok(CommandOutput {
489            status: CommandStatus {
490                success: true,
491                code: Some(0),
492            },
493            stdout: b"ctrl-x\nsession\tdemo\tsession  demo\n".to_vec(),
494            stderr: Vec::new(),
495        }));
496
497        let result = select_with_runner(
498            runner,
499            vec![Entry::session(
500                DisplayStyle::from_icon_mode(IconMode::Never),
501                "demo".to_owned(),
502            )],
503            "smux> ",
504            &PickerBindings::default(),
505            &PickerPreviewSettings::default(),
506        )
507        .expect("selection should succeed")
508        .expect("selection should be present");
509
510        assert_eq!(result.action, SelectAction::Delete);
511        assert_eq!(result.entry.kind, EntryKind::Session);
512        assert_eq!(result.entry.value, "demo");
513    }
514
515    #[test]
516    fn selector_treats_empty_expect_key_as_open() {
517        let runner = Arc::new(FakeCommandRunner::new());
518        runner.push_capture(Ok(CommandOutput {
519            status: CommandStatus {
520                success: true,
521                code: Some(0),
522            },
523            stdout: b"\nfolder\t/tmp/example\tdir      /tmp/example\n".to_vec(),
524            stderr: Vec::new(),
525        }));
526
527        let result = select_with_runner(
528            runner,
529            vec![Entry::directory(
530                DisplayStyle::from_icon_mode(IconMode::Never),
531                "/tmp/example".to_owned(),
532            )],
533            "smux> ",
534            &PickerBindings::default(),
535            &PickerPreviewSettings::default(),
536        )
537        .expect("selection should succeed")
538        .expect("selection should be present");
539
540        assert_eq!(result.action, SelectAction::Open);
541        assert_eq!(result.entry.kind, EntryKind::Directory);
542        assert_eq!(result.entry.value, "/tmp/example");
543    }
544
545    #[test]
546    fn selector_uses_configured_picker_bindings() {
547        let runner = Arc::new(FakeCommandRunner::new());
548        runner.push_capture(Ok(CommandOutput {
549            status: CommandStatus {
550                success: true,
551                code: Some(0),
552            },
553            stdout: b"\nfolder\t/tmp/example\tdir      /tmp/example\n".to_vec(),
554            stderr: Vec::new(),
555        }));
556
557        let bindings = PickerBindings {
558            reset: "alt-a".to_owned(),
559            sessions: "alt-s".to_owned(),
560            folders: "alt-f".to_owned(),
561            projects: "alt-p".to_owned(),
562            delete_session: "alt-x".to_owned(),
563        };
564
565        let _ = select_with_runner(
566            runner.clone(),
567            vec![Entry::directory(
568                DisplayStyle::from_icon_mode(IconMode::Never),
569                "/tmp/example".to_owned(),
570            )],
571            "smux> ",
572            &bindings,
573            &PickerPreviewSettings::default(),
574        )
575        .expect("selection should succeed");
576
577        let recorded = runner.recorded();
578        assert!(recorded[0].args.contains(&"alt-x".to_owned()));
579        assert!(
580            recorded[0]
581                .args
582                .iter()
583                .any(|arg| arg.contains("alt-a:change-prompt(smux> )+clear-query+reload("))
584        );
585        assert!(
586            recorded[0]
587                .args
588                .iter()
589                .any(|arg| arg.contains("alt-s:change-prompt(session> )+clear-query+reload("))
590        );
591        assert!(
592            recorded[0]
593                .args
594                .iter()
595                .any(|arg| arg.contains("alt-f:change-prompt(folder> )+clear-query+reload("))
596        );
597        assert!(
598            recorded[0]
599                .args
600                .iter()
601                .any(|arg| arg.contains("alt-p:change-prompt(project> )+clear-query+reload("))
602        );
603    }
604
605    #[test]
606    fn selector_uses_configured_folder_preview_command() {
607        let runner = Arc::new(FakeCommandRunner::new());
608        runner.push_capture(Ok(CommandOutput {
609            status: CommandStatus {
610                success: true,
611                code: Some(0),
612            },
613            stdout: b"\nfolder\t/tmp/example\tdir      /tmp/example\t\n".to_vec(),
614            stderr: Vec::new(),
615        }));
616
617        let _ = select_with_runner(
618            runner.clone(),
619            vec![Entry::directory(
620                DisplayStyle::from_icon_mode(IconMode::Never),
621                "/tmp/example".to_owned(),
622            )],
623            "smux> ",
624            &PickerBindings::default(),
625            &PickerPreviewSettings {
626                folders: Some("eza --tree --level=2 \"$SMUX_PREVIEW_PATH\"".to_owned()),
627                ..Default::default()
628            },
629        )
630        .expect("selection should succeed");
631
632        let recorded = runner.recorded();
633        assert!(
634            recorded[0]
635                .args
636                .iter()
637                .any(|arg| arg.contains("eza --tree --level=2"))
638        );
639    }
640
641    #[test]
642    fn template_selector_returns_selected_value() {
643        let runner = Arc::new(FakeCommandRunner::new());
644        runner.push_capture(Ok(CommandOutput {
645            status: CommandStatus {
646                success: true,
647                code: Some(0),
648            },
649            stdout: b"template\trust\ttemplate rust\n".to_vec(),
650            stderr: Vec::new(),
651        }));
652
653        let result = select_value_with_runner(
654            runner.clone(),
655            "template> ",
656            vec![
657                Choice::new(
658                    "template",
659                    "template default".to_owned(),
660                    "default".to_owned(),
661                ),
662                Choice::new("template", "template rust".to_owned(), "rust".to_owned()),
663            ],
664        )
665        .expect("selection should succeed");
666
667        assert_eq!(result.as_deref(), Some("rust"));
668        let recorded = runner.recorded();
669        assert!(recorded[0].args.contains(&"--ansi".to_owned()));
670        assert!(recorded[0].args.contains(&"reverse".to_owned()));
671        assert!(recorded[0].args.contains(&"3".to_owned()));
672        assert!(recorded[0].args.contains(&"1,2,4".to_owned()));
673        assert!(
674            recorded[0]
675                .args
676                .iter()
677                .any(|arg| arg.contains("ctrl-t templates"))
678        );
679        assert!(
680            recorded[0]
681                .args
682                .iter()
683                .any(|arg| arg.contains("ctrl-t:change-prompt(template> )+clear-query+reload("))
684        );
685        assert_eq!(
686            recorded[0].stdin.as_deref(),
687            Some("template\tdefault\ttemplate default\ntemplate\trust\ttemplate rust\n")
688        );
689    }
690}