Test message 123!
This commit is contained in:
1 parent
4cf726709f
commit
d4fdc9890a
52 files changed
+5537
-277
No files matched your search
@@ -0,0 +1,201 @@
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use super::modals::render_modal;
|
||||
use super::right_panel::render_right;
|
||||
use super::state::{HubState, Modal, SortMode, Tab};
|
||||
use super::tabs::render_list;
|
||||
|
||||
pub static HELP_LINES: &[&str] = &[
|
||||
"── Navigation ───────────────────────────────",
|
||||
" ↑↓ / jk Move cursor up/down",
|
||||
" Tab Switch tab (Repos → Projects → Todos → PRs → Issues)",
|
||||
" Enter Open repo/project detail view",
|
||||
" q / Esc Quit (or clear active filter)",
|
||||
"",
|
||||
"── Filtering & Sorting ───────────────────────",
|
||||
" / Fuzzy filter repos (name, branch, owner, lang)",
|
||||
" S Cycle sort: Default → Name → Recent → Dirty↑ → Behind↑",
|
||||
" d Toggle: show only dirty repos",
|
||||
" b Toggle: show only behind repos",
|
||||
" f Toggle: show only ahead repos",
|
||||
" a Toggle: show only archived repos (no commits >90d)",
|
||||
"",
|
||||
"── Open / Navigate ───────────────────────────",
|
||||
" o Open repo in $EDITOR",
|
||||
" O Open repo in file manager (xdg-open)",
|
||||
" W Open repo in browser (GitHub/GitLab)",
|
||||
" y Copy repo path to clipboard",
|
||||
" u Copy remote URL to clipboard",
|
||||
" g Open .gitignore in $EDITOR",
|
||||
" C Clone a repository into workspace",
|
||||
"",
|
||||
"── Repo Actions ──────────────────────────────",
|
||||
" s Sync (pull / commit+push) selected repos",
|
||||
" r Create release tag",
|
||||
" B Toggle bookmark (★) — visible in table",
|
||||
" P Toggle pin (◆) — pinned repos shown at top",
|
||||
" n Rename/label repo display name",
|
||||
" Space Select repo for batch action",
|
||||
"",
|
||||
"── Projects & Todos ──────────────────────────",
|
||||
" p Project menu (create / assign / rename / delete)",
|
||||
" + New todo",
|
||||
" e Edit todo (Todos tab)",
|
||||
" Space (Todos tab) Cycle todo status",
|
||||
" D / Del Delete todo or project",
|
||||
" T Scan repos for TODO/FIXME/HACK → import to Todos",
|
||||
"",
|
||||
"── Help ──────────────────────────────────────",
|
||||
" ? Show this help",
|
||||
" Esc / q Close modal or quit",
|
||||
];
|
||||
|
||||
pub fn render_hub(f: &mut Frame, state: &mut HubState) {
|
||||
let area = f.area();
|
||||
let bottom_h: u16 = if state.debug_mode { 11 } else { 3 };
|
||||
let outer = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Fill(1), Constraint::Length(bottom_h)])
|
||||
.split(area);
|
||||
|
||||
let tab_span = |label: &str, on: bool| {
|
||||
if on {
|
||||
Span::styled(format!(" {} ", label), Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
} else {
|
||||
Span::styled(format!(" {} ", label), Style::default().fg(Color::DarkGray))
|
||||
}
|
||||
};
|
||||
let n = state.repo_pos.len();
|
||||
let pos_str = format!(" {}/{}", if n == 0 { 0 } else { state.cursor + 1 }, n);
|
||||
|
||||
let behind_c = state.repos.iter().filter(|r| r.behind > 0).count();
|
||||
let dirty_c = state.repos.iter().filter(|r| r.is_dirty).count();
|
||||
let health_str = if behind_c > 0 || dirty_c > 0 {
|
||||
format!(" ↓{} ⚠{} ", behind_c, dirty_c)
|
||||
} else {
|
||||
" ✓ ".into()
|
||||
};
|
||||
let health_col = if behind_c > 0 { Color::Red } else if dirty_c > 0 { Color::Yellow } else { Color::Green };
|
||||
|
||||
let pr_badge = if !state.prs.is_empty() { format!("PRs({})", state.prs.len()) } else { "PRs".into() };
|
||||
let issue_badge = if !state.issues.is_empty() { format!("Issues({})", state.issues.len()) } else { "Issues".into() };
|
||||
|
||||
let mut top_spans = vec![
|
||||
Span::styled(" fxg ", Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
|
||||
tab_span("Repos", state.tab == Tab::Repos),
|
||||
Span::raw(" "),
|
||||
tab_span("Projects", state.tab == Tab::Projects),
|
||||
Span::raw(" "),
|
||||
tab_span("Todos", state.tab == Tab::Todos),
|
||||
Span::raw(" "),
|
||||
tab_span(&pr_badge, state.tab == Tab::PRs),
|
||||
Span::raw(" "),
|
||||
tab_span(&issue_badge, state.tab == Tab::Issues),
|
||||
Span::raw(" "),
|
||||
];
|
||||
|
||||
// Active filter indicators — only shown when on, so they don't crowd the bar.
|
||||
let active_filter_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
if state.filter_dirty { top_spans.push(Span::styled("[d:dirty] ", active_filter_style)); }
|
||||
if state.filter_behind { top_spans.push(Span::styled("[b:behind] ", active_filter_style)); }
|
||||
if state.filter_ahead { top_spans.push(Span::styled("[f:ahead] ", active_filter_style)); }
|
||||
if state.filter_archived { top_spans.push(Span::styled("[a:archived] ", active_filter_style)); }
|
||||
|
||||
if state.sort_mode != SortMode::Default {
|
||||
top_spans.push(Span::styled(
|
||||
format!("[S:{}] ", state.sort_mode.label()),
|
||||
Style::default().fg(Color::Cyan),
|
||||
));
|
||||
}
|
||||
if !state.filter_text.is_empty() {
|
||||
top_spans.push(Span::styled(
|
||||
format!("[/:{}_] ", state.filter_text),
|
||||
active_filter_style,
|
||||
));
|
||||
} else if state.filtering {
|
||||
top_spans.push(Span::styled("[/: _] ", active_filter_style));
|
||||
}
|
||||
|
||||
top_spans.push(Span::styled(health_str, Style::default().fg(health_col)));
|
||||
top_spans.push(Span::styled(pos_str, Style::default().fg(Color::DarkGray)));
|
||||
|
||||
if let Some(ref msg) = state.status_msg {
|
||||
top_spans.push(Span::styled(format!(" {}", msg), Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)));
|
||||
}
|
||||
|
||||
f.render_widget(Paragraph::new(Line::from(top_spans)), outer[0]);
|
||||
|
||||
let cols = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(73), Constraint::Percentage(27)])
|
||||
.split(outer[1]);
|
||||
|
||||
let left_blk = Block::default().borders(Borders::ALL);
|
||||
let left_inner = left_blk.inner(cols[0]);
|
||||
f.render_widget(left_blk, cols[0]);
|
||||
state.visible_rows = left_inner.height.saturating_sub(1) as usize;
|
||||
render_list(f, state, left_inner);
|
||||
render_right(f, state, cols[1]);
|
||||
|
||||
if state.debug_mode {
|
||||
let blk = Block::default().borders(Borders::TOP).title(" debug ")
|
||||
.border_style(Style::default().fg(Color::DarkGray));
|
||||
let inner = blk.inner(outer[2]);
|
||||
f.render_widget(blk, outer[2]);
|
||||
let h = inner.height as usize;
|
||||
let n = state.debug_log.len();
|
||||
let start = n.saturating_sub(h);
|
||||
let lines: Vec<Line> = state.debug_log[start..]
|
||||
.iter()
|
||||
.map(|s| Line::from(Span::styled(s.clone(), Style::default().fg(Color::DarkGray))))
|
||||
.collect();
|
||||
f.render_widget(Paragraph::new(lines), inner);
|
||||
} else {
|
||||
let dim = Style::default().fg(Color::DarkGray);
|
||||
let key = |k: &str| Span::styled(format!("[{}] ", k), dim);
|
||||
let sep = || Span::styled(" ", dim);
|
||||
|
||||
let hint_lines: Vec<Line> = if state.modal.is_some() {
|
||||
vec![
|
||||
Line::from(vec![key("↑↓/jk"), Span::styled("navigate ", dim), key("Enter"), Span::styled("confirm ", dim), key("Esc"), Span::styled("cancel", dim)]),
|
||||
Line::from(""),
|
||||
Line::from(""),
|
||||
]
|
||||
} else if state.filtering {
|
||||
vec![
|
||||
Line::from(vec![Span::styled(" type to filter", dim), sep(), key("Enter"), Span::styled("lock ", dim), key("Esc"), Span::styled("clear", dim)]),
|
||||
Line::from(""),
|
||||
Line::from(""),
|
||||
]
|
||||
} else if state.confirming.is_some() {
|
||||
vec![
|
||||
Line::from(vec![key("y"), Span::styled("confirm ", dim), key("Esc"), Span::styled("cancel", dim)]),
|
||||
Line::from(""),
|
||||
Line::from(""),
|
||||
]
|
||||
} else {
|
||||
match state.tab {
|
||||
Tab::PRs | Tab::Issues => vec![
|
||||
Line::from(vec![key("↑↓/jk"), Span::styled("navigate ", dim), key("Tab/←→"), Span::styled("switch tab ", dim), key("↵"), Span::styled("open in browser ", dim), key("R"), Span::styled("refresh API ", dim), key("?"), Span::styled("help ", dim), key("q"), Span::styled("quit", dim)]),
|
||||
Line::from(""),
|
||||
Line::from(""),
|
||||
],
|
||||
_ => vec![
|
||||
Line::from(vec![key("↑↓/jk"), Span::styled("navigate ", dim), key("Tab/←→"), Span::styled("tabs ", dim), key("/"), Span::styled("filter ", dim), key("d/b/f/a"), Span::styled("toggle ", dim), key("S"), Span::styled("sort ", dim), key("?"), Span::styled("help ", dim), key("q"), Span::styled("quit", dim)]),
|
||||
Line::from(vec![key("Space"), Span::styled("select ", dim), key("*"), Span::styled("select by ", dim), key("^A"), Span::styled("all/none ", dim), key("R"), Span::styled("API refresh ", dim), key("T"), Span::styled("scan TODOs ", dim), key("X"), Span::styled("code TODOs", dim)]),
|
||||
Line::from(vec![key("C"), Span::styled("clone ", dim), key("I"), Span::styled("init repo ", dim), key("L"), Span::styled("link folder", dim)]),
|
||||
],
|
||||
}
|
||||
};
|
||||
f.render_widget(Paragraph::new(hint_lines), outer[2]);
|
||||
}
|
||||
|
||||
if state.modal.is_some() {
|
||||
render_modal(f, &state.modal, area);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user