Skip to main content

gitu/
app.rs

1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::ffi::OsStr;
4use std::io::Read;
5use std::io::Write;
6use std::ops::DerefMut;
7use std::process::Child;
8use std::process::Command;
9use std::process::Stdio;
10use std::rc::Rc;
11use std::sync::Arc;
12use std::sync::RwLock;
13use std::time::Duration;
14
15use arboard::Clipboard;
16use crossterm::event;
17use crossterm::event::Event;
18use crossterm::event::KeyCode;
19use crossterm::event::KeyModifiers;
20use crossterm::event::MouseButton;
21use crossterm::event::MouseEvent;
22use crossterm::event::MouseEventKind;
23use git2::Repository;
24
25use crate::cli;
26use crate::cmd_log::CmdLog;
27use crate::cmd_log::CmdLogEntry;
28use crate::config::Config;
29use crate::error::Error;
30use crate::file_watcher::FileWatcher;
31use crate::item_data::Rev;
32use crate::items::RenderParams;
33use crate::menu::Menu;
34use crate::menu::PendingMenu;
35use crate::ops::Op;
36use crate::picker::PickerData;
37use crate::picker::PickerState;
38use crate::prompt;
39use crate::rebase_todo::RebaseTodo;
40use crate::screen;
41use crate::screen::Screen;
42use crate::screen::pager::Paged;
43use crate::term::Term;
44use crate::text_input;
45use crate::ui;
46
47use super::Res;
48
49pub(crate) struct State {
50    pub repo: Rc<Repository>,
51    pub config: Arc<Config>,
52    pub pending_keys: Vec<(KeyModifiers, KeyCode)>,
53    pub quit: bool,
54    /// What gitu exits with. Non-zero only to tell git, which is waiting on
55    /// gitu as its sequence editor, to call the rebase off.
56    pub exit_code: i32,
57    pub screens: Vec<Screen>,
58    pub pending_menu: Option<PendingMenu>,
59    pending_cmd: Option<(Child, Arc<RwLock<CmdLogEntry>>)>,
60    enable_async_cmds: bool,
61    pub current_cmd_log: CmdLog,
62    pub prompt: prompt::Prompt,
63    pub picker: Option<PickerState>,
64    /// The commit `select_commit` named, read by [`App::pick_commit`].
65    pub picked_commit: Option<String>,
66    /// Renderer features chosen in-session, overlaying the user's own
67    /// configuration. Never written to disk: quitting returns them to it.
68    pub features: Rc<[String]>,
69    /// How much of a file to ask git for around each change (`-U8`, `-W`),
70    /// chosen in-session. `None` is git's own default.
71    pub context: Option<Rc<str>>,
72    pub clipboard: Option<Clipboard>,
73    needs_redraw: bool,
74    file_watcher: Option<FileWatcher>,
75    inhibit_close_menu: bool,
76}
77
78pub struct App {
79    pub(crate) state: State,
80}
81
82impl App {
83    pub fn create(
84        repo: Rc<Repository>,
85        size: (u16, u16),
86        args: &cli::Args,
87        config: Arc<Config>,
88        enable_async_cmds: bool,
89        piped: Option<Paged>,
90    ) -> Res<Self> {
91        let params = RenderParams {
92            size,
93            features: Rc::from([]),
94            context: None,
95        };
96        let screens = match args.command {
97            // `--pager`: git's output is already in hand, with the command it ran.
98            _ if let Some(paged) = piped => {
99                vec![screen::pager::create(
100                    Arc::clone(&config),
101                    Rc::clone(&repo),
102                    params,
103                    paged,
104                )?]
105            }
106            Some(cli::Commands::Show { ref reference }) => {
107                vec![screen::show::create(
108                    Arc::clone(&config),
109                    Rc::clone(&repo),
110                    params.clone(),
111                    reference.clone(),
112                    None,
113                )?]
114            }
115            Some(cli::Commands::Blame { ref rev, ref file }) => {
116                vec![screen::blame::create(
117                    Arc::clone(&config),
118                    Rc::clone(&repo),
119                    params.clone(),
120                    file.clone(),
121                    rev.clone(),
122                    None,
123                )?]
124            }
125            Some(cli::Commands::Rebase { ref upstream }) => {
126                let todo = RebaseTodo::capture(&repo, OsStr::new(upstream), &[])?;
127                vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
128            }
129            Some(cli::Commands::SequenceEditor { ref file }) => {
130                let todo = RebaseTodo::read(&repo, file)?;
131                vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
132            }
133            // `completion` prints its script and exits in `main`, before the app is ever built.
134            Some(cli::Commands::Completion { .. }) => {
135                unreachable!("completion is handled before the app starts")
136            }
137            None => vec![screen::status::create(
138                Arc::clone(&config),
139                Rc::clone(&repo),
140                params,
141            )?],
142        };
143
144        let clipboard = Clipboard::new()
145            .inspect_err(|e| log::warn!("Couldn't initialize clipboard: {e}"))
146            .ok();
147        let prompt = prompt::Prompt::with_history(repo.path().join("gitu"));
148
149        let mut app = Self {
150            state: State {
151                repo,
152                config,
153                pending_keys: vec![],
154                enable_async_cmds,
155                quit: false,
156                exit_code: 0,
157                screens,
158                pending_cmd: None,
159                pending_menu: None,
160                current_cmd_log: CmdLog::new(),
161                prompt,
162                picker: None,
163                picked_commit: None,
164                features: Rc::from([]),
165                context: None,
166                clipboard,
167                file_watcher: None,
168                needs_redraw: true,
169                inhibit_close_menu: false,
170            },
171        };
172
173        // The menu a screen imposes is in force from the first key on, not from
174        // the first key that closes a menu.
175        app.close_menu();
176        app.state.file_watcher = app.init_file_watcher()?;
177        Ok(app)
178    }
179
180    fn init_file_watcher(&mut self) -> Res<Option<FileWatcher>> {
181        if !self.state.config.general.refresh_on_file_change.enabled {
182            return Ok(None);
183        }
184
185        let hiding_untracked_files = !self
186            .state
187            .repo
188            .config()
189            .map_err(Error::ReadGitConfig)?
190            .get_bool("status.showUntrackedFiles")
191            .unwrap_or(true);
192
193        if hiding_untracked_files {
194            self.display_info("File watcher disabled (status.showUntrackedFiles is off)");
195
196            return Ok(None);
197        }
198
199        Ok(
200            FileWatcher::new(self.state.repo.workdir().expect("Bare repos unhandled"))
201                .inspect_err(|err| {
202                    self.display_error(err.to_string());
203                    self.display_info("File watcher disabled");
204                })
205                .ok(),
206        )
207    }
208
209    pub fn run(&mut self, term: &mut Term, max_tick_delay: Duration) -> Res<()> {
210        while !self.state.quit {
211            let event = if term.poll_event(max_tick_delay)? {
212                Some(term.read_event()?)
213            } else {
214                None
215            };
216
217            self.update(term, event)?;
218        }
219
220        Ok(())
221    }
222
223    pub fn update(&mut self, term: &mut Term, event: Option<Event>) -> Res<()> {
224        if let Some(e) = event {
225            self.handle_event(term, e)?;
226        }
227
228        if let Some(file_watcher) = &mut self.state.file_watcher
229            && file_watcher.pending_updates()
230        {
231            self.update_screens()?;
232        }
233
234        let handle_pending_cmd_result = self.handle_pending_cmd();
235        self.handle_result(handle_pending_cmd_result)?;
236
237        if self.state.needs_redraw {
238            self.redraw_now(term)?;
239        }
240
241        Ok(())
242    }
243
244    pub fn update_screens(&mut self) -> Res<()> {
245        self.rebuild_screens(Screen::refresh)
246    }
247
248    /// Rebuild every screen, leaving each cursor where it was in the diff rather
249    /// than on the line number it happened to occupy. For a re-render of the
250    /// same content (a renderer feature turned on or off), the rows move but the
251    /// place in the patch does not.
252    pub fn rerender_screens(&mut self) -> Res<()> {
253        self.rebuild_screens(Screen::refresh_keeping_position)
254    }
255
256    fn rebuild_screens(&mut self, rebuild: impl Fn(&mut Screen) -> Res<()>) -> Res<()> {
257        let features = Rc::clone(&self.state.features);
258        for screen in &mut self.state.screens {
259            screen.features = Rc::clone(&features);
260            screen.context = self.state.context.clone();
261            rebuild(screen)?;
262        }
263
264        self.stage_redraw();
265        Ok(())
266    }
267
268    pub fn handle_event(&mut self, term: &mut Term, event: Event) -> Res<()> {
269        match event {
270            Event::Resize(w, h) => {
271                for screen in self.state.screens.iter_mut() {
272                    screen.resize(w, h)?;
273                }
274
275                self.stage_redraw();
276                Ok(())
277            }
278            Event::Mouse(ref mouse) => {
279                if self.state.config.general.mouse_support
280                    && self.handle_mouse_input(term, mouse)?
281                {
282                    self.stage_redraw();
283                }
284                Ok(())
285            }
286            Event::Key(mut key) => {
287                if self.state.pending_cmd.is_none() {
288                    self.state.current_cmd_log.clear();
289                }
290
291                if self.state.pending_cmd.is_none() {
292                    self.state.current_cmd_log.clear();
293                }
294
295                // The key received changes as shift is pressed: '/' becomes '?'
296                // on a US keyboard, and tab becomes backtab. The modifier is
297                // redundant there, and on the other keys it is the only sign
298                // shift was held.
299                if matches!(key.code, KeyCode::Char(_) | KeyCode::BackTab) {
300                    key.modifiers = key.modifiers.difference(KeyModifiers::SHIFT);
301                }
302
303                if self.state.picker.is_some() {
304                    self.handle_picker_input(key);
305                } else if self.state.prompt.state.focused {
306                    self.state.prompt.handle_key(key);
307                } else {
308                    self.handle_key_input(term, key)?;
309                }
310
311                self.stage_redraw();
312                Ok(())
313            }
314            _ => Ok(()),
315        }
316    }
317
318    pub fn redraw_now(&mut self, term: &mut Term) -> Res<()> {
319        if self.state.screens.last_mut().is_some() {
320            ui::ui(term, &mut self.state)?;
321            self.state.needs_redraw = false;
322        };
323
324        Ok(())
325    }
326
327    /// Prints the screen, and says so, if it fits on the terminal with a row
328    /// to spare for the shell's prompt: less's `--quit-if-one-screen`.
329    pub fn print_if_one_screen(&self, term: &mut Term) -> Res<bool> {
330        let screen = self.state.screens.last().unwrap();
331        let (_, rows) = term.size().map_err(Error::Term)?;
332        if !screen.fits_in(rows.saturating_sub(1).into()) {
333            return Ok(false);
334        }
335
336        screen::print_screen(term, screen)?;
337        Ok(true)
338    }
339
340    pub fn stage_redraw(&mut self) {
341        self.state.needs_redraw = true;
342    }
343
344    fn handle_key_input(&mut self, term: &mut Term, key: event::KeyEvent) -> Res<()> {
345        if key.kind != event::KeyEventKind::Press {
346            return Ok(());
347        }
348
349        let menu = match &self.state.pending_menu {
350            None => self.base_menu().unwrap_or(Menu::Root),
351            Some(menu) if menu.menu == Menu::Help => Menu::Root,
352            Some(menu) => menu.menu,
353        };
354
355        self.state.pending_keys.push((key.modifiers, key.code));
356
357        let matching_bindings = self
358            .state
359            .config
360            .bindings
361            .match_bindings(&menu, &self.state.pending_keys)
362            .collect::<Vec<_>>();
363
364        match matching_bindings[..] {
365            [binding] => {
366                if binding.keys == self.state.pending_keys {
367                    let op = binding.op.clone();
368                    self.state.pending_keys.clear();
369                    self.handle_op(op, term)?;
370                }
371            }
372            [] => self.state.pending_keys.clear(),
373            [_, ..] => (),
374        }
375
376        Ok(())
377    }
378
379    fn handle_mouse_input(&mut self, term: &mut Term, mouse: &MouseEvent) -> Res<bool> {
380        match mouse.kind {
381            MouseEventKind::Down(MouseButton::Left) => {
382                let click_y = mouse.row as usize;
383                if self.screen().is_valid_screen_line(click_y) {
384                    let old_selected_item_id = self.screen().get_selected_item().id;
385                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
386                    let new_selected_item = self.screen().get_selected_item();
387
388                    if old_selected_item_id == new_selected_item.id {
389                        // If the item clicked was already the current item, then try to
390                        // toggle it if it's a section or show it.
391                        if new_selected_item.data.is_section() {
392                            self.handle_op(Op::ToggleSection, term)?;
393                        } else {
394                            self.handle_op(Op::Show, term)?;
395                        }
396                    }
397                }
398            }
399            MouseEventKind::Down(MouseButton::Right) => {
400                let click_y = mouse.row as usize;
401                if self.screen().is_valid_screen_line(click_y) {
402                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
403                    self.handle_op(Op::Show, term)?;
404                }
405            }
406            MouseEventKind::ScrollUp => {
407                let scroll_lines = self.state.config.general.mouse_scroll_lines;
408                if scroll_lines > 0 {
409                    self.screen_mut().scroll_view_up(scroll_lines);
410                }
411            }
412            MouseEventKind::ScrollDown => {
413                let scroll_lines = self.state.config.general.mouse_scroll_lines;
414                if scroll_lines > 0 {
415                    self.screen_mut().scroll_view_down(scroll_lines);
416                }
417            }
418            _ => return Ok(false),
419        }
420
421        Ok(true)
422    }
423
424    pub(crate) fn handle_op(&mut self, op: Op, term: &mut Term) -> Res<()> {
425        let item_data = self.screen().selected_target();
426        let implementation = op.clone().implementation();
427
428        if let Some(mut action) = implementation.get_action(&item_data) {
429            let result = Rc::get_mut(&mut action).unwrap()(self, term);
430            self.handle_result(result)?;
431            if !self.state.inhibit_close_menu {
432                self.close_menu();
433            }
434            self.state.inhibit_close_menu = false;
435        }
436
437        Ok(())
438    }
439
440    fn handle_result<T>(&mut self, result: Res<T>) -> Res<()> {
441        match result {
442            Ok(_) => Ok(()),
443            Err(Error::NoMoreEvents) => Err(Error::NoMoreEvents),
444            Err(Error::PromptAborted) => Ok(()),
445            Err(error) => {
446                self.state
447                    .current_cmd_log
448                    .push(CmdLogEntry::Error(error.to_string()));
449
450                Ok(())
451            }
452        }
453    }
454
455    /// What a screen opened now should render with: the viewport it will get,
456    /// and the features currently selected.
457    pub(crate) fn render_params(&self, size: (u16, u16)) -> RenderParams {
458        RenderParams {
459            size,
460            features: Rc::clone(&self.state.features),
461            context: self.state.context.clone(),
462        }
463    }
464
465    pub fn close_menu(&mut self) {
466        let hide = self.screen().menu.is_some() && !self.screen().show_menu;
467        self.state.pending_menu = self.base_menu().map(|menu| PendingMenu {
468            is_hidden: hide,
469            ..PendingMenu::init(menu)
470        });
471    }
472
473    /// The menu to fall back to once a pending one closes: a screen with its own
474    /// keymap (the interactive rebase todo) imposes it for as long as it is up.
475    pub(crate) fn base_menu(&self) -> Option<Menu> {
476        self.screen().menu.or_else(|| root_menu(&self.state.config))
477    }
478
479    pub fn inhibit_close_menu(&mut self) {
480        self.state.inhibit_close_menu = true;
481    }
482
483    pub(crate) fn screen_mut(&mut self) -> &mut Screen {
484        self.state.screens.last_mut().expect("No screen")
485    }
486
487    pub(crate) fn screen(&self) -> &Screen {
488        self.state.screens.last().expect("No screen")
489    }
490
491    /// Displays an `Info` message to the CmdLog.
492    pub fn display_info<S: Into<Cow<'static, str>>>(&mut self, message: S) {
493        self.state
494            .current_cmd_log
495            .push(CmdLogEntry::Info(message.into().into_owned()));
496    }
497
498    /// Displays an `Error` message to the CmdLog.
499    pub fn display_error<S: Into<Cow<'static, str>>>(&mut self, message: S) {
500        self.state
501            .current_cmd_log
502            .push(CmdLogEntry::Error(message.into().into_owned()));
503    }
504
505    /// Runs a `Command` and handles its output.
506    /// Will block awaiting its completion.
507    pub fn run_cmd(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
508        self.run_cmd_async(term, input, cmd)?;
509        self.await_pending_cmd()?;
510        self.handle_pending_cmd()?;
511        Ok(())
512    }
513
514    /// Runs a `Command` and handles its output asynchronously (if async commands are enabled).
515    /// Will return `Ok(())` if one is already running.
516    pub fn run_cmd_async(&mut self, term: &mut Term, input: &[u8], mut cmd: Command) -> Res<()> {
517        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
518
519        if self.state.pending_cmd.is_some() {
520            return Err(Error::CmdAlreadyRunning);
521        }
522
523        cmd.current_dir(self.state.repo.workdir().expect("No workdir"));
524
525        cmd.stdin(Stdio::piped());
526        cmd.stdout(Stdio::piped());
527        cmd.stderr(Stdio::piped());
528
529        let log_entry = self.state.current_cmd_log.push_cmd(&cmd);
530
531        ui::ui(term, &mut self.state)?;
532
533        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
534
535        use std::io::Write;
536        child
537            .stdin
538            .take()
539            .unwrap()
540            .write_all(input)
541            .map_err(Error::Term)?;
542
543        self.state.pending_cmd = Some((child, log_entry));
544
545        if !self.state.enable_async_cmds {
546            self.await_pending_cmd()?;
547        }
548
549        Ok(())
550    }
551
552    fn await_pending_cmd(&mut self) -> Res<()> {
553        if let Some((child, _)) = &mut self.state.pending_cmd {
554            child.wait().map_err(Error::CouldntAwaitCmd)?;
555        }
556        Ok(())
557    }
558
559    /// Handles any pending_cmd in State without blocking. Returns `true` if a cmd was handled.
560    fn handle_pending_cmd(&mut self) -> Res<()> {
561        let Some((ref mut child, ref mut log_rwlock)) = self.state.pending_cmd else {
562            return Ok(());
563        };
564
565        let Some(status) = child.try_wait().map_err(Error::CouldntAwaitCmd)? else {
566            return Ok(());
567        };
568
569        log::debug!("pending cmd finished with {status:?}");
570
571        let result = write_child_output_to_log(log_rwlock, child, status);
572        self.state.pending_cmd = None;
573        self.update_screens()?;
574        result?;
575
576        Ok(())
577    }
578
579    pub fn run_cmd_interactive(&mut self, term: &mut Term, mut cmd: Command) -> Res<()> {
580        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
581
582        if self.state.pending_cmd.is_some() {
583            return Err(Error::CmdAlreadyRunning);
584        }
585
586        cmd.current_dir(self.state.repo.workdir().ok_or(Error::NoRepoWorkdir)?);
587
588        self.state
589            .current_cmd_log
590            .push_cmd_with_output(&cmd, "\n".into());
591        self.redraw_now(term)?;
592
593        eprint!("\r");
594
595        // Redirect stderr so we can capture it via `Child::wait_with_output()`
596        cmd.stderr(Stdio::piped());
597
598        term.reset_term_stay_on_alt_screeen(&self.state.config)
599            .map_err(Error::Term)?;
600
601        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
602
603        // Drop stdin as `Child::wait_with_output` would
604        drop(child.stdin.take());
605
606        let mut stderr = Vec::new();
607
608        tee(
609            child.stderr.as_mut(),
610            &mut [&mut std::io::stderr(), &mut stderr],
611        )
612        .map_err(Error::Term)?;
613
614        let status = child.wait().map_err(Error::CouldntAwaitCmd)?;
615
616        term.setup_term(&self.state.config).map_err(Error::Term)?;
617
618        let out_utf8 = String::from_utf8(strip_ansi_escapes::strip(stderr.clone()))
619            .expect("Error turning command output to String")
620            .into();
621
622        self.state.current_cmd_log.clear();
623        self.state
624            .current_cmd_log
625            .push_cmd_with_output(&cmd, out_utf8);
626
627        term.clear().map_err(Error::Term)?;
628        self.update_screens()?;
629
630        if !status.success() {
631            return Err(Error::CmdBadExit(
632                format!(
633                    "{} {}",
634                    cmd.get_program().to_string_lossy(),
635                    cmd.get_args()
636                        .map(|arg| arg.to_string_lossy())
637                        .collect::<String>()
638                ),
639                status.code(),
640            ));
641        }
642
643        Ok(())
644    }
645
646    pub fn hide_menu(&mut self) {
647        if let Some(ref mut menu) = self.state.pending_menu {
648            menu.is_hidden = true;
649        }
650    }
651
652    fn unhide_menu(&mut self) {
653        if let Some(ref mut menu) = self.state.pending_menu {
654            menu.is_hidden = false;
655        }
656    }
657
658    pub(crate) fn selected_rev(&self) -> Option<Rev> {
659        self.screen().get_selected_item().data.rev()
660    }
661
662    pub(crate) fn prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
663        let default = (params.create_default_value)(self);
664
665        // A prompt with nothing to say says nothing: the key that opened it
666        // already said what is being answered. Prefilled, the default is the
667        // answer being edited rather than the one an empty answer falls back
668        // to — it is on the line, so naming it again says nothing either.
669        let prompt_text = match (params.prompt, &default) {
670            ("", _) => Cow::Borrowed(""),
671            (prompt, Some(default)) if !params.prefill => {
672                format!("{prompt} (default {default}):").into()
673            }
674            (prompt, _) => format!("{prompt}:").into(),
675        };
676
677        if params.hide_menu {
678            self.hide_menu();
679        }
680
681        self.state.prompt.set(prompt::PromptData { prompt_text });
682
683        if params.prefill
684            && let Some(default) = default
685        {
686            self.state.prompt.state.value = default;
687            self.state.prompt.state.move_to(usize::MAX);
688        }
689        self.state.prompt.start_history(params.history)?;
690
691        let result = self.handle_prompt(term, params);
692
693        self.unhide_menu();
694        self.state.prompt.reset();
695
696        result
697    }
698
699    fn handle_prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
700        self.redraw_now(term)?;
701
702        loop {
703            let event = term.read_event()?;
704            self.handle_event(term, event)?;
705
706            if self.state.prompt.state.status == text_input::Status::Done {
707                let value = get_prompt_result(params, self)?;
708                self.state.prompt.remember(&value)?;
709                return Ok(value);
710            } else if self.state.prompt.state.status == text_input::Status::Aborted {
711                return Err(Error::PromptAborted);
712            }
713
714            self.redraw_now(term)?;
715        }
716    }
717
718    pub fn confirm(&mut self, term: &mut Term, prompt: &'static str) -> Res<()> {
719        self.hide_menu();
720        self.state.prompt.set(prompt::PromptData {
721            prompt_text: prompt.into(),
722        });
723
724        let result = self.handle_confirm(term);
725
726        self.unhide_menu();
727        self.state.prompt.reset();
728
729        result
730    }
731
732    fn handle_confirm(&mut self, term: &mut Term) -> Res<()> {
733        self.redraw_now(term)?;
734        loop {
735            let event = term.read_event()?;
736            self.handle_event(term, event)?;
737
738            match self.state.prompt.state.value.as_str() {
739                "y" => {
740                    return Ok(());
741                }
742                "" => (),
743                _ => {
744                    return Err(Error::PromptAborted);
745                }
746            }
747
748            self.redraw_now(term)?;
749        }
750    }
751
752    /// Show a picker and wait for user to select an item or cancel.
753    ///
754    /// Returns:
755    /// - `Ok(Some(data))` - User selected an item
756    /// - `Ok(None)` - User cancelled (Esc or Ctrl-C)
757    /// - `Err(e)` - An error occurred
758    ///
759    /// # Example
760    /// ```ignore
761    /// let items = vec![
762    ///     PickerItem::new("main", PickerData::Item("main".to_string())),
763    ///     PickerItem::new("develop", PickerData::Item("develop".to_string())),
764    /// ];
765    /// let picker = PickerState::new("Select branch", items, false);
766    ///
767    /// match app.pick(term, picker)? {
768    ///     Some(PickerData::Item(name)) => {
769    ///         // User selected a branch
770    ///         println!("Selected: {}", name);
771    ///     }
772    ///     Some(PickerData::CustomInput(_)) => {
773    ///         // Should not happen when allow_custom_input is false
774    ///     }
775    ///     None => {
776    ///         // User cancelled
777    ///         println!("Cancelled");
778    ///     }
779    /// }
780    /// ```
781    pub fn pick(&mut self, term: &mut Term, picker_state: PickerState) -> Res<Option<PickerData>> {
782        self.state.picker = Some(picker_state);
783        let result = self.handle_picker(term);
784
785        self.state.picker = None;
786
787        result
788    }
789
790    /// Choose a commit by moving around a log view: the screen is pushed with
791    /// its own keymap, and the loop runs until `select_commit` names one or the
792    /// screen is closed.
793    pub fn pick_commit(&mut self, term: &mut Term) -> Res<Option<String>> {
794        let params = self.render_params(self.screen().size);
795        let selected = match &self.screen().get_selected_item().data {
796            crate::item_data::ItemData::Commit { oid, .. } => Some(oid.clone()),
797            _ => None,
798        };
799        self.state.screens.push(screen::log::create(
800            Arc::clone(&self.state.config),
801            Rc::clone(&self.state.repo),
802            params,
803            COMMIT_PICK_LIMIT,
804            None,
805            None,
806        )?);
807        self.screen_mut().menu = Some(Menu::CommitPicker);
808        self.close_menu();
809
810        // Start on the commit that was already selected, if there was one.
811        if let Some(selected) = selected {
812            self.screen_mut().select_matching(|data| {
813                matches!(data, crate::item_data::ItemData::Commit { oid, .. } if *oid == selected)
814            });
815        }
816
817        let depth = self.state.screens.len();
818        self.redraw_now(term)?;
819
820        loop {
821            let event = term.read_event()?;
822            self.handle_event(term, event)?;
823
824            if let Some(oid) = self.state.picked_commit.take() {
825                self.state.screens.truncate(depth - 1);
826                self.close_menu();
827                return Ok(Some(oid));
828            }
829            if self.state.screens.len() < depth {
830                return Ok(None); // The screen was closed: nothing picked.
831            }
832
833            self.redraw_now(term)?;
834        }
835    }
836
837    fn handle_picker(&mut self, term: &mut Term) -> Res<Option<PickerData>> {
838        self.redraw_now(term)?;
839
840        loop {
841            let event = term.read_event()?;
842            self.handle_event(term, event)?;
843
844            if let Some(ref picker) = self.state.picker {
845                if picker.is_done() {
846                    // User selected an item
847                    return Ok(picker.selected().map(|item| item.data.clone()));
848                } else if picker.is_cancelled() {
849                    // User cancelled - this is not an error
850                    return Ok(None);
851                }
852            }
853
854            self.redraw_now(term)?;
855        }
856    }
857
858    fn handle_picker_input(&mut self, key: event::KeyEvent) {
859        if key.kind != event::KeyEventKind::Press {
860            return;
861        }
862
863        if let Some(ref mut picker) = self.state.picker {
864            let bindings = &self.state.config.picker_bindings;
865            let key_combo = vec![(key.modifiers, key.code)];
866
867            if bindings.next.iter().any(|b| b == &key_combo) {
868                picker.next();
869            } else if bindings.previous.iter().any(|b| b == &key_combo) {
870                picker.previous();
871            } else if bindings.done.iter().any(|b| b == &key_combo) {
872                picker.done();
873            } else if bindings.cancel.iter().any(|b| b == &key_combo) {
874                picker.cancel();
875            } else {
876                // Text input - delegate to text state
877                picker.input_state.handle_key_event(key);
878                picker.update_filter();
879            }
880        }
881    }
882}
883
884fn get_prompt_result(params: &PromptParams, app: &mut App) -> Res<String> {
885    let input = app.state.prompt.state.value.as_str();
886    if params.prefill {
887        return Ok(input.to_owned());
888    }
889
890    let default_value = (params.create_default_value)(app);
891
892    let value = match (input, &default_value) {
893        ("", None) => "",
894        ("", Some(selected)) => selected,
895        (value, _) => value,
896    };
897
898    Ok(value.to_string())
899}
900
901fn tee(maybe_input: Option<&mut impl Read>, outputs: &mut [&mut dyn Write]) -> std::io::Result<()> {
902    let Some(input) = maybe_input else {
903        return Ok(());
904    };
905
906    let mut buf = [0u8; 1024];
907
908    loop {
909        let num_read = input.read(&mut buf)?;
910        if num_read == 0 {
911            break;
912        }
913
914        let buf = &buf[..num_read];
915        for output in &mut *outputs {
916            output.write_all(buf)?;
917        }
918    }
919
920    Ok(())
921}
922
923fn rebase_todo_screen(
924    config: &Arc<Config>,
925    repo: &Rc<Repository>,
926    params: RenderParams,
927    todo: RebaseTodo,
928) -> Res<Screen> {
929    screen::rebase_todo::create(
930        Arc::clone(config),
931        Rc::clone(repo),
932        params,
933        Rc::new(RefCell::new(todo)),
934    )
935}
936
937/// How far back [`App::pick_commit`] lists commits.
938const COMMIT_PICK_LIMIT: usize = 256;
939
940pub(crate) fn root_menu(config: &Config) -> Option<Menu> {
941    if config.general.always_show_help.enabled {
942        Some(Menu::Help)
943    } else {
944        None
945    }
946}
947
948fn write_child_output_to_log(
949    log_rwlock: &mut Arc<RwLock<CmdLogEntry>>,
950    child: &mut Child,
951    status: std::process::ExitStatus,
952) -> Res<()> {
953    let mut log = log_rwlock.write().unwrap();
954
955    let CmdLogEntry::Cmd { args, out: out_log } = log.deref_mut() else {
956        unreachable!("pending_cmd is always CmdLogEntry::Cmd variant");
957    };
958
959    drop(child.stdin.take());
960
961    let mut out_bytes = vec![];
962    log::debug!("Reading stderr");
963
964    child
965        .stderr
966        .take()
967        .unwrap()
968        .read_to_end(&mut out_bytes)
969        .map_err(Error::CouldntReadCmdOutput)?;
970
971    child
972        .stdout
973        .take()
974        .unwrap()
975        .read_to_end(&mut out_bytes)
976        .map_err(Error::CouldntReadCmdOutput)?;
977
978    let out_string = String::from_utf8_lossy(&out_bytes).to_string();
979    *out_log = Some(out_string.into());
980
981    if !status.success() {
982        return Err(Error::CmdBadExit(args.to_string(), status.code()));
983    }
984
985    Ok(())
986}
987
988type DefaultFn = Box<dyn Fn(&App) -> Option<String>>;
989
990pub(crate) struct PromptParams {
991    pub prompt: &'static str,
992    pub create_default_value: DefaultFn,
993    pub hide_menu: bool,
994    /// Put the default value in the prompt for the user to edit, rather than
995    /// keeping it as what an empty answer means.
996    pub prefill: bool,
997    pub history: Option<prompt::HistoryKind>,
998}
999
1000impl Default for PromptParams {
1001    fn default() -> Self {
1002        Self {
1003            prompt: "",
1004            create_default_value: Box::new(|_| None),
1005            hide_menu: true,
1006            prefill: false,
1007            history: None,
1008        }
1009    }
1010}