use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use crate::domain::session::{PublishBranchAction, Session, Status};
use crate::ui::style;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HelpAction {
pub(crate) footer_label: &'static str,
pub(crate) key: &'static str,
pub(crate) popup_label: &'static str,
}
impl HelpAction {
pub(crate) const fn new(
footer_label: &'static str,
key: &'static str,
popup_label: &'static str,
) -> Self {
Self {
footer_label,
key,
popup_label,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ViewSessionState {
Done,
Canceled,
InProgress,
Rebasing,
MergeQueue,
NewSession,
StackedDraft,
Review,
AgentReview,
Interactive,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ViewHelpState {
pub(crate) can_open_worktree: bool,
pub(crate) publish_pull_request_action: Option<PublishBranchAction>,
pub(crate) session_state: ViewSessionState,
}
pub(crate) fn session_view_state(session: &Session) -> ViewSessionState {
match session.status {
Status::Done => ViewSessionState::Done,
Status::Canceled => ViewSessionState::Canceled,
Status::InProgress => ViewSessionState::InProgress,
Status::Draft if session.is_draft_session() && session.is_stacked_child() => {
ViewSessionState::StackedDraft
}
Status::Draft if session.is_draft_session() => ViewSessionState::NewSession,
Status::Draft | Status::Question => ViewSessionState::Interactive,
Status::Rebasing => ViewSessionState::Rebasing,
Status::Merging | Status::Queued => ViewSessionState::MergeQueue,
Status::Review => ViewSessionState::Review,
Status::AgentReview => ViewSessionState::AgentReview,
}
}
pub(crate) fn session_list_actions(
can_cancel_selected_session: bool,
can_open_selected_session: bool,
) -> Vec<HelpAction> {
let mut actions = list_base_actions();
actions.push(HelpAction::new("new session", "a", "Choose session type"));
if can_cancel_selected_session {
actions.push(HelpAction::new("cancel", "c", "Cancel session"));
}
if can_open_selected_session {
actions.push(HelpAction::new("open session", "Enter", "Open session"));
}
actions.push(HelpAction::new("nav", "j/k", "Navigate sessions"));
actions.push(HelpAction::new("next tab", "Tab", "Switch tab"));
actions.push(HelpAction::new("help", "?", "Help"));
actions
}
pub(crate) fn project_list_actions() -> Vec<HelpAction> {
let mut actions = list_base_actions();
actions.push(HelpAction::new("select", "Enter", "Select active project"));
actions.push(HelpAction::new("nav", "j/k", "Navigate projects"));
actions.push(HelpAction::new("next tab", "Tab", "Switch tab"));
actions.push(HelpAction::new("help", "?", "Help"));
actions
}
pub(crate) fn project_list_footer_actions() -> Vec<HelpAction> {
vec![
HelpAction::new("quit", "q", "Quit"),
HelpAction::new("select", "Enter", "Select active project"),
HelpAction::new("nav", "j/k", "Navigate projects"),
HelpAction::new("help", "?", "Help"),
]
}
pub(crate) fn session_list_footer_actions(
can_cancel_selected_session: bool,
can_open_selected_session: bool,
) -> Vec<HelpAction> {
let mut actions = list_base_actions();
actions.push(HelpAction::new("new session", "a", "Choose session type"));
if can_cancel_selected_session {
actions.push(HelpAction::new("cancel", "c", "Cancel session"));
}
if can_open_selected_session {
actions.push(HelpAction::new("open session", "Enter", "Open session"));
}
actions.push(HelpAction::new("nav", "j/k", "Navigate sessions"));
actions.push(HelpAction::new("help", "?", "Help"));
actions
}
pub(crate) fn settings_actions() -> Vec<HelpAction> {
let mut actions = list_base_actions();
actions.push(HelpAction::new("nav", "j/k", "Navigate settings"));
actions.push(HelpAction::new("edit", "Enter", "Edit setting"));
actions.push(HelpAction::new("next tab", "Tab", "Switch tab"));
actions.push(HelpAction::new("help", "?", "Help"));
actions
}
pub(crate) fn settings_footer_actions() -> Vec<HelpAction> {
vec![
HelpAction::new("quit", "q", "Quit"),
HelpAction::new("nav", "j/k", "Navigate settings"),
HelpAction::new("edit", "Enter", "Edit setting"),
HelpAction::new("help", "?", "Help"),
]
}
pub(crate) fn review_actions() -> Vec<HelpAction> {
let mut actions = vec![
HelpAction::new("quit", "q", "Quit"),
HelpAction::new("nav", "j/k", "Navigate reviews"),
HelpAction::new("open", "Enter", "Open review details"),
HelpAction::new("refresh", "s", "Refresh requested reviews"),
];
actions.push(HelpAction::new("next tab", "Tab", "Switch tab"));
actions.push(HelpAction::new("help", "?", "Help"));
actions
}
pub(crate) fn view_actions(state: ViewHelpState) -> Vec<HelpAction> {
let can_open_worktree = state.can_open_worktree
&& matches!(
state.session_state,
ViewSessionState::Interactive
| ViewSessionState::NewSession
| ViewSessionState::StackedDraft
| ViewSessionState::Review
| ViewSessionState::AgentReview
);
let can_edit_session = matches!(
state.session_state,
ViewSessionState::Interactive
| ViewSessionState::NewSession
| ViewSessionState::StackedDraft
| ViewSessionState::Review
| ViewSessionState::AgentReview
);
let can_show_diff = matches!(
state.session_state,
ViewSessionState::Review | ViewSessionState::AgentReview
);
let can_show_review = matches!(
state.session_state,
ViewSessionState::Review | ViewSessionState::AgentReview
);
let can_continue_terminal_session = matches!(state.session_state, ViewSessionState::Done);
let can_stop_session = state.session_state == ViewSessionState::InProgress;
let mut actions = vec![HelpAction::new("back", "q", "Back to list")];
append_view_prompt_actions(&mut actions, state.session_state, can_edit_session);
if can_stop_session {
actions.push(HelpAction::new(
"stop",
"Ctrl+c",
"Stop current turn (pops one queued chat message at a time first)",
));
}
if state.session_state == ViewSessionState::NewSession {
actions.push(HelpAction::new("start", "s", "Start staged session"));
}
if can_open_worktree {
actions.push(HelpAction::new("open", "o", "Open worktree"));
}
if can_show_diff {
actions.push(HelpAction::new("diff", "d", "Show diff"));
}
if can_show_review {
actions.push(HelpAction::new("review", "f", "Focused review"));
}
if let Some(publish_pull_request_action) = state.publish_pull_request_action {
actions.push(publish_pull_request_help_action(
publish_pull_request_action,
));
}
if can_edit_session && state.session_state != ViewSessionState::StackedDraft {
actions.push(HelpAction::new(
"add to merge queue",
"m",
"Add to merge queue",
));
if state.session_state != ViewSessionState::AgentReview {
actions.push(HelpAction::new("rebase", "r", "Rebase"));
}
}
if can_continue_terminal_session {
actions.push(HelpAction::new("continue", "c", "Continue in new session"));
}
actions.push(HelpAction::new("scroll", "j/k", "Scroll output"));
actions.push(HelpAction::new("top", "g", "Scroll to top"));
actions.push(HelpAction::new("bottom", "G", "Scroll to bottom"));
actions.push(HelpAction::new("half down", "Ctrl+d", "Half page down"));
actions.push(HelpAction::new("half up", "Ctrl+u", "Half page up"));
actions.push(HelpAction::new("help", "?", "Help"));
actions
}
pub(crate) fn view_footer_actions(state: ViewHelpState) -> Vec<HelpAction> {
let can_open_worktree = state.can_open_worktree
&& matches!(
state.session_state,
ViewSessionState::Interactive
| ViewSessionState::NewSession
| ViewSessionState::StackedDraft
| ViewSessionState::Review
| ViewSessionState::AgentReview
);
let can_edit_session = matches!(
state.session_state,
ViewSessionState::Interactive
| ViewSessionState::NewSession
| ViewSessionState::StackedDraft
| ViewSessionState::Review
| ViewSessionState::AgentReview
);
let can_show_review = matches!(
state.session_state,
ViewSessionState::Review | ViewSessionState::AgentReview
);
let can_continue_terminal_session = matches!(state.session_state, ViewSessionState::Done);
let can_stop_session = state.session_state == ViewSessionState::InProgress;
let mut actions = vec![HelpAction::new("back", "q", "Back to list")];
append_view_footer_edit_actions(&mut actions, state.session_state, can_edit_session);
if can_stop_session {
actions.push(HelpAction::new(
"stop",
"Ctrl+c",
"Stop current turn (pops one queued chat message at a time first)",
));
}
if can_open_worktree {
actions.push(HelpAction::new("open", "o", "Open worktree"));
}
if can_show_review {
actions.push(HelpAction::new("review", "f", "Focused review"));
}
if let Some(publish_pull_request_action) = state.publish_pull_request_action {
actions.push(publish_pull_request_help_action(
publish_pull_request_action,
));
}
if can_continue_terminal_session {
actions.push(HelpAction::new("continue", "c", "Continue in new session"));
}
actions.push(HelpAction::new("scroll", "j/k", "Scroll output"));
actions.push(HelpAction::new("help", "?", "Help"));
actions
}
fn append_view_footer_edit_actions(
actions: &mut Vec<HelpAction>,
session_state: ViewSessionState,
can_edit_session: bool,
) {
if !can_edit_session {
return;
}
append_view_prompt_actions(actions, session_state, can_edit_session);
if session_state == ViewSessionState::NewSession {
actions.push(HelpAction::new("start", "s", "Start staged session"));
}
if session_state == ViewSessionState::StackedDraft {
return;
}
actions.push(HelpAction::new(
"add to merge queue",
"m",
"Add to merge queue",
));
if session_state != ViewSessionState::AgentReview {
actions.push(HelpAction::new("rebase", "r", "Rebase"));
}
}
fn append_view_prompt_actions(
actions: &mut Vec<HelpAction>,
session_state: ViewSessionState,
can_edit_session: bool,
) {
if !can_edit_session {
return;
}
actions.push(prompt_action_help_action(session_state));
actions.push(HelpAction::new("commands menu", "/", "Open commands menu"));
}
fn prompt_action_help_action(session_state: ViewSessionState) -> HelpAction {
if matches!(
session_state,
ViewSessionState::NewSession | ViewSessionState::StackedDraft
) {
return HelpAction::new("add draft", "Enter", "Add draft");
}
HelpAction::new("reply", "Enter", "Reply")
}
pub(crate) fn diff_actions(right_panel: super::app_mode::DiffRightPanel) -> Vec<HelpAction> {
vec![
HelpAction::new("back", "q/Esc", "Back to session"),
HelpAction::new("select file", "j/k", "Select file"),
HelpAction::new("scroll pane", "Up/Down", "Scroll right panel"),
HelpAction::new(
diff_toggle_comments_label(right_panel),
"c",
diff_toggle_comments_label(right_panel),
),
HelpAction::new("help", "?", "Help"),
]
}
pub(crate) fn diff_footer_actions(right_panel: super::app_mode::DiffRightPanel) -> Vec<HelpAction> {
vec![
HelpAction::new("back", "q/Esc", "Back to session"),
HelpAction::new("select file", "j/k", "Select file"),
HelpAction::new(
diff_toggle_comments_label(right_panel),
"c",
diff_toggle_comments_label(right_panel),
),
HelpAction::new("help", "?", "Help"),
]
}
fn diff_toggle_comments_label(right_panel: super::app_mode::DiffRightPanel) -> &'static str {
match right_panel {
super::app_mode::DiffRightPanel::Diff => "Show comments",
super::app_mode::DiffRightPanel::Comments => "Show diff",
}
}
pub(crate) fn footer_line(actions: &[HelpAction]) -> Line<'static> {
let mut spans = Vec::new();
for (index, action) in actions.iter().enumerate() {
if index > 0 {
spans.push(footer_separator_span());
}
spans.push(footer_key_span(action.key));
spans.push(footer_muted_span(": "));
spans.push(footer_muted_span(action.footer_label));
}
Line::from(spans)
}
pub(crate) fn footer_key_span(key: &'static str) -> Span<'static> {
Span::styled(
key.to_string(),
Style::default()
.fg(style::palette::accent())
.add_modifier(Modifier::BOLD),
)
}
pub(crate) fn footer_muted_span(text: impl Into<String>) -> Span<'static> {
Span::styled(
text.into(),
Style::default().fg(style::palette::text_muted()),
)
}
pub(crate) fn footer_separator_span() -> Span<'static> {
Span::styled(" | ", Style::default().fg(style::palette::text_subtle()))
}
fn list_base_actions() -> Vec<HelpAction> {
vec![
HelpAction::new("quit", "q", "Quit"),
HelpAction::new("sync", "s", "Sync"),
]
}
fn publish_pull_request_help_action(action: PublishBranchAction) -> HelpAction {
match action {
PublishBranchAction::PublishPullRequest | PublishBranchAction::Push => {
HelpAction::new("PR", "p", "Create or refresh forge review request")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::session::PublishBranchAction;
#[test]
fn test_project_list_actions_exclude_new_session_shortcut() {
let actions = project_list_actions();
assert!(!actions.iter().any(|action| action.key == "a"));
}
#[test]
fn test_settings_actions_exclude_new_session_shortcut() {
let actions = settings_actions();
assert!(!actions.iter().any(|action| action.key == "a"));
}
#[test]
fn test_session_list_actions_include_new_session_shortcut() {
let actions = session_list_actions(false, false);
assert!(actions.iter().any(|action| action.key == "a"));
}
#[test]
fn test_session_list_actions_hide_enter_without_openable_session() {
let actions = session_list_actions(false, false);
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(actions.iter().any(|action| action.key == "j/k"));
}
#[test]
fn test_session_list_footer_actions_hides_non_critical_session_commands() {
let actions = session_list_footer_actions(false, true);
assert!(actions.iter().any(|action| action.key == "Enter"));
assert!(actions.iter().any(|action| action.key == "a"));
assert!(!actions.iter().any(|action| action.key == "d"));
assert!(!actions.iter().any(|action| action.key == "c"));
assert!(!actions.iter().any(|action| action.key == "Tab"));
}
#[test]
fn test_session_list_footer_actions_includes_cancel_when_session_is_cancelable() {
let actions = session_list_footer_actions(true, true);
assert!(actions.iter().any(|action| action.key == "c"));
assert!(actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "Tab"));
}
#[test]
fn test_view_actions_in_progress_shows_stop_and_hides_open_and_edit_actions() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::InProgress,
};
let actions = view_actions(state);
assert!(actions.iter().any(|action| action.key == "Ctrl+c"));
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "o"));
assert!(!actions.iter().any(|action| action.key == "d"));
}
#[test]
fn test_view_actions_hide_open_when_worktree_is_unavailable() {
let state = ViewHelpState {
can_open_worktree: false,
publish_pull_request_action: None,
session_state: ViewSessionState::Review,
};
let actions = view_actions(state);
assert!(!actions.iter().any(|action| action.key == "o"));
}
#[test]
fn test_view_actions_rebasing_hides_open_and_stop() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Rebasing,
};
let actions = view_actions(state);
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "o"));
assert!(!actions.iter().any(|action| action.key == "Ctrl+c"));
assert!(!actions.iter().any(|action| action.key == "d"));
}
#[test]
fn test_view_actions_merge_queue_hides_worktree_shortcuts_and_stop() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::MergeQueue,
};
let actions = view_actions(state);
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "o"));
assert!(!actions.iter().any(|action| action.key == "Ctrl+c"));
assert!(!actions.iter().any(|action| action.key == "d"));
}
#[test]
fn test_view_actions_review_shows_diff() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: Some(PublishBranchAction::PublishPullRequest),
session_state: ViewSessionState::Review,
};
let actions = view_actions(state);
assert!(actions.iter().any(|action| action.key == "d"));
assert!(actions.iter().any(|action| action.key == "f"));
assert!(
actions
.iter()
.any(|action| action.key == "p" && action.footer_label == "PR")
);
assert!(actions.iter().any(|action| action.key == "p"));
assert!(actions.iter().any(|action| action.key == "o"));
assert!(actions.iter().any(|action| action.key == "Enter"));
assert!(actions.iter().any(|action| {
action.key == "/"
&& action.footer_label == "commands menu"
&& action.popup_label == "Open commands menu"
}));
assert!(!actions.iter().any(|action| action.key == "S-Tab"));
assert!(
actions
.iter()
.any(|action| action.key == "f" && action.popup_label == "Focused review")
);
}
#[test]
fn test_view_actions_agent_review_hides_rebase() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: Some(PublishBranchAction::PublishPullRequest),
session_state: ViewSessionState::AgentReview,
};
let actions = view_actions(state);
assert!(actions.iter().any(|action| action.key == "d"));
assert!(actions.iter().any(|action| action.key == "f"));
assert!(actions.iter().any(|action| action.key == "m"));
assert!(actions.iter().any(|action| action.key == "p"));
assert!(!actions.iter().any(|action| action.key == "r"));
}
#[test]
fn test_view_actions_interactive_hides_diff() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Interactive,
};
let actions = view_actions(state);
assert!(!actions.iter().any(|action| action.key == "d"));
assert!(!actions.iter().any(|action| action.key == "f"));
assert!(actions.iter().any(|action| action.key == "Enter"));
assert!(actions.iter().any(|action| action.key == "/"));
}
#[test]
fn test_view_actions_new_session_shows_add_draft_and_start() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::NewSession,
};
let actions = view_actions(state);
assert!(
actions
.iter()
.any(|action| action.key == "Enter" && action.footer_label == "add draft")
);
assert!(actions.iter().any(|action| action.key == "/"));
assert!(actions.iter().any(|action| action.key == "s"));
}
#[test]
fn test_view_actions_stacked_draft_hides_start_merge_and_rebase() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::StackedDraft,
};
let actions = view_actions(state);
assert!(
actions
.iter()
.any(|action| action.key == "Enter" && action.footer_label == "add draft")
);
assert!(actions.iter().any(|action| action.key == "/"));
assert!(!actions.iter().any(|action| action.key == "s"));
assert!(!actions.iter().any(|action| action.key == "m"));
assert!(!actions.iter().any(|action| action.key == "r"));
}
#[test]
fn test_view_actions_done_shows_continue_and_hides_edit_actions() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Done,
};
let actions = view_actions(state);
assert!(actions.iter().any(|action| action.key == "c"));
assert!(!actions.iter().any(|action| action.key == "p"));
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "d"));
assert!(!actions.iter().any(|action| action.key == "f"));
assert!(!actions.iter().any(|action| action.key == "m"));
assert!(!actions.iter().any(|action| action.key == "r"));
}
#[test]
fn test_view_footer_actions_review_shows_advanced_actions() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: Some(PublishBranchAction::PublishPullRequest),
session_state: ViewSessionState::Review,
};
let actions = view_footer_actions(state);
assert!(actions.iter().any(|action| action.key == "Enter"));
assert!(actions.iter().any(|action| action.key == "/"));
assert!(actions.iter().any(|action| action.key == "o"));
assert!(actions.iter().any(|action| action.key == "f"));
assert!(actions.iter().any(|action| action.key == "p"));
assert!(actions.iter().any(|action| action.key == "p"));
assert!(actions.iter().any(|action| action.key == "m"));
assert!(actions.iter().any(|action| action.key == "r"));
}
#[test]
fn test_view_footer_actions_agent_review_hides_rebase() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: Some(PublishBranchAction::PublishPullRequest),
session_state: ViewSessionState::AgentReview,
};
let actions = view_footer_actions(state);
assert!(actions.iter().any(|action| action.key == "Enter"));
assert!(actions.iter().any(|action| action.key == "/"));
assert!(actions.iter().any(|action| action.key == "o"));
assert!(actions.iter().any(|action| action.key == "f"));
assert!(actions.iter().any(|action| action.key == "p"));
assert!(actions.iter().any(|action| action.key == "p"));
assert!(actions.iter().any(|action| action.key == "m"));
assert!(!actions.iter().any(|action| action.key == "r"));
}
#[test]
fn test_view_footer_actions_rebasing_hides_open_and_stop() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Rebasing,
};
let actions = view_footer_actions(state);
assert!(!actions.iter().any(|action| action.key == "o"));
assert!(!actions.iter().any(|action| action.key == "p"));
assert!(!actions.iter().any(|action| action.key == "Ctrl+c"));
assert!(!actions.iter().any(|action| action.key == "Enter"));
}
#[test]
fn test_view_footer_actions_new_session_keeps_edit_actions_grouped() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::NewSession,
};
let actions = view_footer_actions(state);
let ordered_keys = actions.iter().map(|action| action.key).collect::<Vec<_>>();
assert_eq!(&ordered_keys[..6], ["q", "Enter", "/", "s", "m", "r"]);
}
#[test]
fn test_view_footer_actions_stacked_draft_hides_start_merge_and_rebase() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::StackedDraft,
};
let actions = view_footer_actions(state);
assert!(actions.iter().any(|action| action.key == "Enter"));
assert!(actions.iter().any(|action| action.key == "/"));
assert!(!actions.iter().any(|action| action.key == "s"));
assert!(!actions.iter().any(|action| action.key == "m"));
assert!(!actions.iter().any(|action| action.key == "r"));
}
#[test]
fn test_view_footer_actions_merge_queue_hides_worktree_shortcuts_and_stop() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::MergeQueue,
};
let actions = view_footer_actions(state);
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "o"));
assert!(!actions.iter().any(|action| action.key == "Ctrl+c"));
assert!(actions.iter().any(|action| action.key == "q"));
assert!(actions.iter().any(|action| action.key == "j/k"));
}
#[test]
fn test_view_actions_canceled_without_branch_publish_action() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Canceled,
};
let actions = view_actions(state);
assert!(!actions.iter().any(|action| action.key == "c"));
assert!(!actions.iter().any(|action| action.key == "p"));
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "o"));
assert!(!actions.iter().any(|action| action.key == "t"));
}
#[test]
fn test_view_footer_actions_done_shows_continue_before_scroll() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Done,
};
let actions = view_footer_actions(state);
let ordered_keys = actions.iter().map(|action| action.key).collect::<Vec<_>>();
assert_eq!(&ordered_keys[..4], ["q", "c", "j/k", "?"]);
}
#[test]
fn test_view_footer_actions_canceled_hides_continue() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Canceled,
};
let actions = view_footer_actions(state);
let ordered_keys = actions.iter().map(|action| action.key).collect::<Vec<_>>();
assert_eq!(&ordered_keys[..3], ["q", "j/k", "?"]);
}
#[test]
fn test_footer_line_styles_keys_labels_and_separator() {
let actions = vec![
HelpAction::new("quit", "q", "Quit"),
HelpAction::new("help", "?", "Help"),
];
let line = footer_line(&actions);
assert_eq!(line.to_string(), "q: quit | ?: help");
assert_eq!(
line.spans[0].style,
Style::default()
.fg(style::palette::accent())
.add_modifier(Modifier::BOLD)
);
assert_eq!(
line.spans[1].style,
Style::default().fg(style::palette::text_muted())
);
assert_eq!(
line.spans[2].style,
Style::default().fg(style::palette::text_muted())
);
assert_eq!(
line.spans[3].style,
Style::default().fg(style::palette::text_subtle())
);
assert_eq!(
line.spans[4].style,
Style::default()
.fg(style::palette::accent())
.add_modifier(Modifier::BOLD)
);
}
#[test]
fn test_view_footer_actions_in_progress_shows_stop_and_hides_open() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::InProgress,
};
let actions = view_footer_actions(state);
assert!(actions.iter().any(|action| action.key == "Ctrl+c"));
assert!(!actions.iter().any(|action| action.key == "o"));
assert!(!actions.iter().any(|action| action.key == "Enter"));
assert!(!actions.iter().any(|action| action.key == "d"));
}
#[test]
fn test_view_actions_review_hides_stop() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Review,
};
let actions = view_actions(state);
assert!(!actions.iter().any(|action| action.key == "Ctrl+c"));
}
#[test]
fn test_view_footer_actions_review_hides_stop() {
let state = ViewHelpState {
can_open_worktree: true,
publish_pull_request_action: None,
session_state: ViewSessionState::Review,
};
let actions = view_footer_actions(state);
assert!(!actions.iter().any(|action| action.key == "Ctrl+c"));
}
#[test]
fn test_footer_muted_span_uses_muted_footer_style() {
let span = footer_muted_span("note");
assert_eq!(span.content, "note");
assert_eq!(
span.style,
Style::default().fg(style::palette::text_muted())
);
}
#[test]
fn test_footer_separator_span_uses_shared_separator_style() {
let span = footer_separator_span();
assert_eq!(span.content, " | ");
assert_eq!(
span.style,
Style::default().fg(style::palette::text_subtle())
);
}
}