Add focus switching and scrollable preview pane
Tab toggles focus between inbox list and preview. Focused pane gets a cyan border. When preview is focused, up/down scrolls the email body instead of navigating the list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b781b0fa08
commit
174a1b1030
1 changed files with 74 additions and 34 deletions
108
src/lib.rs
108
src/lib.rs
|
|
@ -17,6 +17,12 @@ mod inbox;
|
|||
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Focus {
|
||||
Inbox,
|
||||
Preview,
|
||||
}
|
||||
|
||||
pub(crate) struct Email {
|
||||
pub seq: u32,
|
||||
pub subject: String,
|
||||
|
|
@ -89,6 +95,8 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
let mut preview_body: String = String::new();
|
||||
let mut preview_seq: Option<u32> = None;
|
||||
let mut body_loading = false;
|
||||
let mut focus = Focus::Inbox;
|
||||
let mut preview_scroll: u16 = 0;
|
||||
|
||||
// --- Main loop ---
|
||||
loop {
|
||||
|
|
@ -131,6 +139,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
WorkerResult::Body { seq, result } => {
|
||||
body_loading = false;
|
||||
if preview_seq == Some(seq) {
|
||||
preview_scroll = 0;
|
||||
match result {
|
||||
Ok(body) => preview_body = body,
|
||||
Err(e) => preview_body = format!("Error loading body: {}", e),
|
||||
|
|
@ -159,19 +168,30 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
.split(area);
|
||||
|
||||
// --- Top: Inbox list ---
|
||||
let inbox_border = if focus == Focus::Inbox {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
let preview_border = if focus == Focus::Preview {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
|
||||
if let Some(e) = &error {
|
||||
let p = Paragraph::new(format!("IMAP error: {}", e))
|
||||
.block(Block::default().title("Inbox").borders(Borders::ALL))
|
||||
.block(Block::default().title("Inbox").borders(Borders::ALL).border_style(inbox_border))
|
||||
.style(Style::default().fg(Color::Red));
|
||||
frame.render_widget(p, layout[0]);
|
||||
} else if emails.is_empty() && loading {
|
||||
let p = Paragraph::new("Loading...")
|
||||
.block(Block::default().title("Inbox").borders(Borders::ALL))
|
||||
.block(Block::default().title("Inbox").borders(Borders::ALL).border_style(inbox_border))
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
frame.render_widget(p, layout[0]);
|
||||
} else if emails.is_empty() {
|
||||
let p = Paragraph::new("No messages in inbox.")
|
||||
.block(Block::default().title("Inbox").borders(Borders::ALL))
|
||||
.block(Block::default().title("Inbox").borders(Borders::ALL).border_style(inbox_border))
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
frame.render_widget(p, layout[0]);
|
||||
} else {
|
||||
|
|
@ -192,7 +212,8 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
.block(
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL),
|
||||
.borders(Borders::ALL)
|
||||
.border_style(inbox_border),
|
||||
)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(
|
||||
|
|
@ -212,12 +233,13 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
preview_body.clone()
|
||||
};
|
||||
let preview = Paragraph::new(preview_text)
|
||||
.block(Block::default().title("Preview").borders(Borders::ALL))
|
||||
.block(Block::default().title("Preview").borders(Borders::ALL).border_style(preview_border))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false });
|
||||
.wrap(ratatui::widgets::Wrap { trim: false })
|
||||
.scroll((preview_scroll, 0));
|
||||
frame.render_widget(preview, layout[1]);
|
||||
|
||||
let status = Paragraph::new(" 'q' quit | 'r' refresh | ↑/↓ navigate")
|
||||
let status = Paragraph::new(" 'q' quit | 'r' refresh | ↑/↓ navigate | Tab switch pane")
|
||||
.style(Style::default().fg(Color::DarkGray));
|
||||
frame.render_widget(status, layout[2]);
|
||||
})?;
|
||||
|
|
@ -227,6 +249,12 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||
KeyCode::Tab => {
|
||||
focus = match focus {
|
||||
Focus::Inbox => Focus::Preview,
|
||||
Focus::Preview => Focus::Inbox,
|
||||
};
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
if !loading {
|
||||
let _ = cmd_tx.send(WorkerCmd::Refresh);
|
||||
|
|
@ -234,39 +262,51 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
|||
last_fetch = Instant::now();
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
let len = emails.len();
|
||||
if len > 0 {
|
||||
let i = list_state.selected().map_or(0, |i| i + 1);
|
||||
if i >= len && has_older && !loading {
|
||||
if let Some(seq) = oldest_seq {
|
||||
let _ = cmd_tx.send(WorkerCmd::FetchMore { oldest_seq: seq });
|
||||
loading = true;
|
||||
KeyCode::Down | KeyCode::Char('j') => match focus {
|
||||
Focus::Inbox => {
|
||||
let len = emails.len();
|
||||
if len > 0 {
|
||||
let i = list_state.selected().map_or(0, |i| i + 1);
|
||||
if i >= len && has_older && !loading {
|
||||
if let Some(seq) = oldest_seq {
|
||||
let _ = cmd_tx.send(WorkerCmd::FetchMore { oldest_seq: seq });
|
||||
loading = true;
|
||||
}
|
||||
}
|
||||
let new_idx = i.min(len.saturating_sub(1));
|
||||
list_state.select(Some(new_idx));
|
||||
let seq = emails[new_idx].seq;
|
||||
if preview_seq != Some(seq) {
|
||||
preview_seq = Some(seq);
|
||||
preview_body.clear();
|
||||
preview_scroll = 0;
|
||||
body_loading = true;
|
||||
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||
}
|
||||
}
|
||||
let new_idx = i.min(len.saturating_sub(1));
|
||||
list_state.select(Some(new_idx));
|
||||
let seq = emails[new_idx].seq;
|
||||
if preview_seq != Some(seq) {
|
||||
preview_seq = Some(seq);
|
||||
preview_body.clear();
|
||||
body_loading = true;
|
||||
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||
}
|
||||
}
|
||||
Focus::Preview => {
|
||||
preview_scroll = preview_scroll.saturating_add(1);
|
||||
}
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
let i = list_state.selected().map_or(0, |i| i.saturating_sub(1));
|
||||
list_state.select(Some(i));
|
||||
if !emails.is_empty() {
|
||||
let seq = emails[i].seq;
|
||||
if preview_seq != Some(seq) {
|
||||
preview_seq = Some(seq);
|
||||
preview_body.clear();
|
||||
body_loading = true;
|
||||
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||
KeyCode::Up | KeyCode::Char('k') => match focus {
|
||||
Focus::Inbox => {
|
||||
let i = list_state.selected().map_or(0, |i| i.saturating_sub(1));
|
||||
list_state.select(Some(i));
|
||||
if !emails.is_empty() {
|
||||
let seq = emails[i].seq;
|
||||
if preview_seq != Some(seq) {
|
||||
preview_seq = Some(seq);
|
||||
preview_body.clear();
|
||||
preview_scroll = 0;
|
||||
body_loading = true;
|
||||
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||
}
|
||||
}
|
||||
}
|
||||
Focus::Preview => {
|
||||
preview_scroll = preview_scroll.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue