Skip to main content

gitu/
app.rs

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