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