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 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 pub picked_commit: Option<String>,
71 pub features: Rc<[String]>,
74 pub theme: Option<String>,
76 pub context: Option<Rc<str>>,
79 pub feature_keys: BTreeMap<String, char>,
81 pub clipboard: Option<Clipboard>,
82 pub read_only: bool,
85 needs_redraw: bool,
86 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 _ 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 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 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 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 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 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 pub fn rerender_screens(&mut self) -> Res<()> {
346 self.rebuild_screens(Screen::refresh_keeping_position)
347 }
348
349 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 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 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 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 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 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 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 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 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 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 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 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 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"); 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 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 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"); 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 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(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 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 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) = ¶ms.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 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 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 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 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 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 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); }
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 PickerStatus::Cancelled => return Ok(None),
1084 PickerStatus::Active => (),
1085 }
1086 }
1087
1088 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 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 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 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 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 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 picker.input_state.handle_key_event(key);
1188 picker.update_filter();
1189 }
1190 }
1191 Ok(())
1192 }
1193}
1194
1195fn 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
1259const COMMIT_PICK_LIMIT: usize = 256;
1261
1262const 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
1275struct 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 pub prefill: bool,
1340 pub history: Option<prompt::HistoryKind>,
1341 pub bare: bool,
1344 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}