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