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