Skip to content

Commit dde7af8

Browse files
committed
improve editor launch experience in interactive mode
Add terminal suspension and restoration when launching editors from interactive mode, allowing editors like vim to properly take over the terminal. Implement wait-for-completion behavior for interactive sessions while maintaining background launches for CLI usage. Enhance error and info dialogs with improved styling and better layout. Create default preferences file with vim as the default editor when initializing worktrees directory.
1 parent b13bf07 commit dde7af8

File tree

8 files changed

+328
-93
lines changed

8 files changed

+328
-93
lines changed

src/commands/interactive/command.rs

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use std::path::{Path, PathBuf};
22

33
use color_eyre::{Result, eyre::WrapErr};
4-
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
4+
use crossterm::{
5+
event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
6+
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
7+
ExecutableCommand,
8+
};
59
use git2::{
610
Branch, BranchType, Commit, ErrorCode, Oid, Repository, RepositoryState, Status, StatusOptions,
711
};
@@ -17,15 +21,15 @@ use ratatui::{
1721
use super::{
1822
Action, EventSource, Focus, Selection, StatusMessage, WorktreeEntry,
1923
dialog::{
20-
CreateDialog, CreateDialogFocus, Dialog, MergeDialog, MergeDialogFocus, RemoveDialog,
21-
RemoveDialogFocus,
24+
CreateDialog, CreateDialogFocus, Dialog, InfoDialogKind, MergeDialog, MergeDialogFocus,
25+
RemoveDialog, RemoveDialogFocus,
2226
},
2327
view::{DetailData, DialogView, Snapshot},
2428
};
2529
use crate::{
2630
commands::rm::{LocalBranchStatus, RemoveOutcome},
2731
editor::LaunchOutcome,
28-
telemetry::EditorLaunchStatus,
32+
telemetry::{EditorLaunchStatus, log_editor_launch_attempt},
2933
};
3034

3135
#[allow(dead_code)]
@@ -91,6 +95,13 @@ impl ActionPanelState {
9195
}
9296
}
9397

98+
struct EditorLaunchLog {
99+
worktree: String,
100+
path: PathBuf,
101+
status: EditorLaunchStatus,
102+
message: String,
103+
}
104+
94105
pub struct InteractiveCommand<B, E>
95106
where
96107
B: Backend,
@@ -108,6 +119,7 @@ where
108119
pub(crate) default_branch: Option<String>,
109120
pub(crate) status: Option<StatusMessage>,
110121
pub(crate) dialog: Option<Dialog>,
122+
editor_logs: Vec<EditorLaunchLog>,
111123
}
112124

113125
impl<B, E> InteractiveCommand<B, E>
@@ -141,6 +153,7 @@ where
141153
default_branch,
142154
status: None,
143155
dialog: None,
156+
editor_logs: Vec::new(),
144157
}
145158
}
146159

@@ -168,6 +181,10 @@ where
168181
.show_cursor()
169182
.wrap_err("failed to show cursor")?;
170183

184+
for log in std::mem::take(&mut self.editor_logs) {
185+
log_editor_launch_attempt(&log.worktree, &log.path, log.status, &log.message);
186+
}
187+
171188
result
172189
}
173190

@@ -567,7 +584,10 @@ where
567584
self.dialog = None;
568585
return Ok(Some(Selection::RepoRoot));
569586
} else {
570-
self.dialog = Some(Dialog::Info { message });
587+
self.dialog = Some(Dialog::Info {
588+
message,
589+
kind: InfoDialogKind::Info,
590+
});
571591
}
572592
}
573593
Err(err) => {
@@ -988,6 +1008,26 @@ where
9881008
}
9891009
}
9901010

1011+
fn suspend_terminal(&mut self) -> Result<()> {
1012+
// Show cursor, leave alternate screen, and disable raw mode
1013+
// In test environments, these operations may fail, which is okay
1014+
let _ = self.terminal.show_cursor();
1015+
let _ = std::io::stdout().execute(LeaveAlternateScreen);
1016+
let _ = disable_raw_mode();
1017+
Ok(())
1018+
}
1019+
1020+
fn restore_terminal(&mut self) -> Result<()> {
1021+
// Re-enable raw mode, enter alternate screen, and hide cursor
1022+
// In test environments, these operations may fail, which is okay
1023+
let _ = enable_raw_mode();
1024+
let _ = std::io::stdout().execute(EnterAlternateScreen);
1025+
let _ = self.terminal.hide_cursor();
1026+
// Clear the terminal to ensure a clean redraw
1027+
let _ = self.terminal.clear();
1028+
Ok(())
1029+
}
1030+
9911031
fn trigger_open_in_editor<H>(
9921032
&mut self,
9931033
on_open_editor: &mut H,
@@ -997,25 +1037,67 @@ where
9971037
where
9981038
H: FnMut(&str, &Path) -> color_eyre::Result<LaunchOutcome>,
9991039
{
1000-
match on_open_editor(name, path) {
1040+
// Suspend the terminal before launching the editor
1041+
self.suspend_terminal()?;
1042+
1043+
// Launch the editor and wait for it to complete
1044+
let result = on_open_editor(name, path);
1045+
1046+
// Restore the terminal after the editor exits
1047+
self.restore_terminal()?;
1048+
1049+
match result {
10011050
Ok(outcome) => {
1002-
let status = match outcome.status {
1003-
EditorLaunchStatus::Success | EditorLaunchStatus::PreferenceMissing => {
1004-
StatusMessage::info(outcome.message)
1051+
match outcome.status {
1052+
EditorLaunchStatus::Success => {
1053+
self.status = Some(StatusMessage::info(outcome.message.clone()));
1054+
self.dialog = None;
10051055
}
1006-
_ => StatusMessage::error(outcome.message),
1007-
};
1008-
self.status = Some(status);
1056+
EditorLaunchStatus::PreferenceMissing => {
1057+
self.show_info_popup(outcome.message.clone());
1058+
}
1059+
_ => {
1060+
self.show_error_popup(outcome.message.clone());
1061+
}
1062+
}
1063+
1064+
self.editor_logs.push(EditorLaunchLog {
1065+
worktree: name.to_string(),
1066+
path: path.to_path_buf(),
1067+
status: outcome.status,
1068+
message: outcome.message,
1069+
});
10091070
}
10101071
Err(error) => {
1011-
self.status = Some(StatusMessage::error(format!(
1012-
"Failed to open `{name}`: {error}"
1013-
)));
1072+
let message = format!("Failed to open `{name}`: {error}");
1073+
self.show_error_popup(message.clone());
1074+
self.editor_logs.push(EditorLaunchLog {
1075+
worktree: name.to_string(),
1076+
path: path.to_path_buf(),
1077+
status: EditorLaunchStatus::ConfigurationError,
1078+
message,
1079+
});
10141080
}
10151081
}
10161082
Ok(())
10171083
}
10181084

1085+
fn show_info_popup(&mut self, message: String) {
1086+
self.status = None;
1087+
self.dialog = Some(Dialog::Info {
1088+
message,
1089+
kind: InfoDialogKind::Info,
1090+
});
1091+
}
1092+
1093+
fn show_error_popup(&mut self, message: String) {
1094+
self.status = None;
1095+
self.dialog = Some(Dialog::Info {
1096+
message,
1097+
kind: InfoDialogKind::Error,
1098+
});
1099+
}
1100+
10191101
fn move_global_action(&mut self, delta: isize) {
10201102
let len = super::GLOBAL_ACTIONS.len() as isize;
10211103
if len == 0 {
@@ -1090,7 +1172,7 @@ where
10901172
dialog: dialog.into(),
10911173
})
10921174
}
1093-
Some(Dialog::Info { message }) => Some(DialogView::Info { message }),
1175+
Some(Dialog::Info { message, kind }) => Some(DialogView::Info { message, kind }),
10941176
Some(Dialog::Create(dialog)) => Some(DialogView::Create(dialog.into())),
10951177
Some(Dialog::Merge(dialog)) => {
10961178
self.worktrees

src/commands/interactive/dialog.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,10 +526,19 @@ impl From<MergeDialog> for MergeDialogView {
526526
}
527527
}
528528

529+
#[derive(Clone, Copy, Debug)]
530+
pub(crate) enum InfoDialogKind {
531+
Info,
532+
Error,
533+
}
534+
529535
#[derive(Clone, Debug)]
530536
pub(crate) enum Dialog {
531537
Remove(RemoveDialog),
532-
Info { message: String },
538+
Info {
539+
message: String,
540+
kind: InfoDialogKind,
541+
},
533542
Create(CreateDialog),
534543
Merge(MergeDialog),
535544
}

src/commands/interactive/runtime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ pub fn run(repo: &Repo) -> Result<()> {
8383
)),
8484
}
8585
},
86-
|name, path| launch_worktree(repo, name, path),
86+
|name, path| launch_worktree(repo, name, path, true),
8787
);
8888
let cleanup_result = cleanup_terminal();
8989

src/commands/interactive/view.rs

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
use ratatui::{
22
Frame,
3-
layout::{Alignment, Constraint, Direction, Layout, Rect},
3+
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
44
style::{Color, Modifier, Style},
55
text::{Line, Span},
6-
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
6+
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
77
};
88

99
use super::command::ActionPanelState;
1010
use super::{
1111
Action, Focus, StatusMessage,
1212
dialog::{
13-
CreateDialogFocus, CreateDialogView, LineType, MergeDialogFocus, MergeDialogView,
14-
RemoveDialogFocus, RemoveDialogView,
13+
CreateDialogFocus, CreateDialogView, InfoDialogKind, LineType, MergeDialogFocus,
14+
MergeDialogView, RemoveDialogFocus, RemoveDialogView,
1515
},
1616
};
1717

@@ -39,6 +39,7 @@ pub(crate) enum DialogView {
3939
},
4040
Info {
4141
message: String,
42+
kind: InfoDialogKind,
4243
},
4344
Create(CreateDialogView),
4445
Merge {
@@ -93,7 +94,7 @@ impl Snapshot {
9394
DialogView::Remove { name, dialog } => {
9495
self.render_remove(frame, size, name, dialog)
9596
}
96-
DialogView::Info { message } => self.render_info(frame, size, message),
97+
DialogView::Info { message, kind } => self.render_info(frame, size, message, *kind),
9798
DialogView::Create(create) => self.render_create(frame, size, create),
9899
DialogView::Merge { name, dialog } => self.render_merge(frame, size, name, dialog),
99100
}
@@ -301,29 +302,60 @@ impl Snapshot {
301302
frame.render_widget(buttons_block, layout[2]);
302303
}
303304

304-
fn render_info(&self, frame: &mut Frame, area: Rect, message: &str) {
305-
let popup_area = centered_rect(60, 30, area);
305+
fn render_info(&self, frame: &mut Frame, area: Rect, message: &str, kind: InfoDialogKind) {
306+
let popup_area = centered_rect(70, 50, area);
306307
frame.render_widget(Clear, popup_area);
307308

308-
let lines = vec![
309-
Line::from(message.to_owned()),
310-
Line::from(""),
311-
Line::from(Span::styled(
312-
"[ OK ]",
309+
let (title, border_style) = match kind {
310+
InfoDialogKind::Info => (
311+
"Notice",
313312
Style::default()
314313
.fg(Color::Cyan)
315-
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
316-
)),
317-
Line::from("Press Enter to continue."),
318-
];
314+
.add_modifier(Modifier::BOLD),
315+
),
316+
InfoDialogKind::Error => (
317+
"Attention",
318+
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
319+
),
320+
};
319321

320-
let popup = Paragraph::new(lines).block(
321-
Block::default()
322-
.title("Complete")
323-
.borders(Borders::ALL)
324-
.border_style(Style::default().fg(Color::Green)),
325-
);
326-
frame.render_widget(popup, popup_area);
322+
let popup_block = Block::default()
323+
.title(title)
324+
.borders(Borders::ALL)
325+
.border_style(border_style);
326+
frame.render_widget(popup_block.clone(), popup_area);
327+
328+
let inner = popup_block.inner(popup_area).inner(&Margin {
329+
vertical: 1,
330+
horizontal: 2,
331+
});
332+
let layout = Layout::default()
333+
.direction(Direction::Vertical)
334+
.constraints([
335+
Constraint::Min(4),
336+
Constraint::Length(2),
337+
Constraint::Length(1),
338+
])
339+
.split(inner);
340+
341+
let message_widget = Paragraph::new(message.to_owned())
342+
.wrap(Wrap { trim: false })
343+
.alignment(Alignment::Left);
344+
frame.render_widget(message_widget, layout[0]);
345+
346+
let button = Paragraph::new(Line::from(Span::styled(
347+
"[ OK ]",
348+
Style::default()
349+
.fg(Color::Cyan)
350+
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
351+
)))
352+
.alignment(Alignment::Center);
353+
frame.render_widget(button, layout[1]);
354+
355+
let hint = Paragraph::new("Press Enter to continue.")
356+
.alignment(Alignment::Center)
357+
.style(Style::default().fg(Color::Gray));
358+
frame.render_widget(hint, layout[2]);
327359
}
328360

329361
fn render_global_actions(&self, frame: &mut Frame, area: Rect) {

src/commands/open_editor/mod.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
Repo,
77
commands::list::{find_worktrees, format_worktree},
88
editor::launch_worktree,
9-
telemetry::EditorLaunchStatus,
9+
telemetry::{EditorLaunchStatus, log_editor_launch_attempt},
1010
};
1111

1212
pub struct OpenEditorCommand {
@@ -21,7 +21,26 @@ impl OpenEditorCommand {
2121

2222
pub fn execute(&self, repo: &Repo) -> color_eyre::Result<()> {
2323
let resolved = self.resolve_target(repo)?;
24-
let outcome = launch_worktree(repo, &resolved.name, &resolved.path)?;
24+
let outcome = match launch_worktree(repo, &resolved.name, &resolved.path, false) {
25+
Ok(outcome) => {
26+
log_editor_launch_attempt(
27+
&resolved.name,
28+
&resolved.path,
29+
outcome.status,
30+
&outcome.message,
31+
);
32+
outcome
33+
}
34+
Err(error) => {
35+
log_editor_launch_attempt(
36+
&resolved.name,
37+
&resolved.path,
38+
EditorLaunchStatus::ConfigurationError,
39+
&error.to_string(),
40+
);
41+
return Err(error);
42+
}
43+
};
2544

2645
match outcome.status {
2746
EditorLaunchStatus::Success => {

0 commit comments

Comments
 (0)