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