chore: refactor spacing c907dc42
Steve Simkins · 2026-05-12 19:46 1 file(s) · +50 −38
src/main.rs +50 −38
7 7
    layout::{Constraint, Direction, Layout},
8 8
    style::{Color, Modifier, Style},
9 9
    text::{Line, Span, Text},
10 -
    widgets::{Block, List, ListItem, ListState, Padding},
10 +
    widgets::{Block, Padding, Paragraph},
11 11
};
12 12
13 13
struct Item {
207 207
}
208 208
209 209
fn app(terminal: &mut DefaultTerminal, items: &[Item]) -> std::io::Result<()> {
210 -
    let mut state = ListState::default();
211 -
    state.select(Some(0));
210 +
    let mut selected = 0;
211 +
    let mut scroll_offset = 0;
212 212
213 213
    loop {
214 -
        terminal.draw(|f| render(f, items, &mut state))?;
214 +
        terminal.draw(|f| render(f, items, selected, &mut scroll_offset))?;
215 215
216 216
        if let crossterm::event::Event::Key(KeyEvent { code, .. }) = crossterm::event::read()? {
217 217
            let len = items.len();
218 218
            match code {
219 219
                KeyCode::Char('q') => break,
220 220
                KeyCode::Char('j') | KeyCode::Down => {
221 -
                    let next = state
222 -
                        .selected()
223 -
                        .map(|i| (i + 1).min(len.saturating_sub(1)))
224 -
                        .unwrap_or(0);
225 -
                    state.select(Some(next));
221 +
                    selected = (selected + 1).min(len.saturating_sub(1));
226 222
                }
227 223
                KeyCode::Char('k') | KeyCode::Up => {
228 -
                    let prev = state.selected().map(|i| i.saturating_sub(1)).unwrap_or(0);
229 -
                    state.select(Some(prev));
224 +
                    selected = selected.saturating_sub(1);
230 225
                }
231 226
                KeyCode::Enter => {
232 -
                    if let Some(i) = state.selected() {
233 -
                        if let Some(url) = items[i].url.as_deref() {
234 -
                            let _ = open::that(url);
235 -
                        }
227 +
                    if let Some(url) = items[selected].url.as_deref() {
228 +
                        let _ = open::that(url);
236 229
                    }
237 230
                }
238 231
                _ => {}
254 247
    format!("{} {}{}, {}", dt.format("%B"), day, suffix, dt.format("%Y"))
255 248
}
256 249
257 -
fn render(frame: &mut Frame, items: &[Item], state: &mut ListState) {
250 +
fn render(frame: &mut Frame, items: &[Item], selected: usize, scroll_offset: &mut u16) {
258 251
    let outer = frame.area();
259 252
    let [_, center, _] = Layout::default()
260 253
        .direction(Direction::Horizontal)
274 267
        .fg(Color::DarkGray)
275 268
        .add_modifier(Modifier::ITALIC);
276 269
277 -
    let selected = state.selected();
278 -
    let list_items: Vec<ListItem> = items
279 -
        .iter()
280 -
        .enumerate()
281 -
        .map(|(i, item)| {
282 -
            let bar = if selected == Some(i) { "▌ " } else { "  " };
283 -
            let mut lines = vec![Line::from(vec![
284 -
                Span::raw(bar),
285 -
                Span::styled(item.date.clone(), dim),
286 -
            ])];
287 -
            for wrapped in textwrap::wrap(&item.title, title_width.max(1)) {
288 -
                lines.push(Line::from(vec![
289 -
                    Span::raw(bar),
290 -
                    Span::raw(wrapped.into_owned()),
291 -
                ]));
292 -
            }
270 +
    let mut selected_start = 0;
271 +
    let mut selected_end = 0;
272 +
    let mut lines = Vec::new();
273 +
    for (i, item) in items.iter().enumerate() {
274 +
        if i == selected {
275 +
            selected_start = lines.len();
276 +
        }
277 +
278 +
        let bar = if i == selected { "▌ " } else { "  " };
279 +
        lines.push(Line::from(vec![
280 +
            Span::raw(bar),
281 +
            Span::styled(item.date.clone(), dim),
282 +
        ]));
283 +
        for wrapped in textwrap::wrap(&item.title, title_width.max(1)) {
293 284
            lines.push(Line::from(vec![
294 285
                Span::raw(bar),
295 -
                Span::styled(item.author.clone(), author_style),
286 +
                Span::raw(wrapped.into_owned()),
296 287
            ]));
297 -
            lines.push(Line::from(""));
288 +
        }
289 +
        lines.push(Line::from(vec![
290 +
            Span::raw(bar),
291 +
            Span::styled(item.author.clone(), author_style),
292 +
        ]));
293 +
        lines.push(Line::from(""));
294 +
295 +
        if i == selected {
296 +
            selected_end = lines.len();
297 +
        }
298 +
    }
298 299
299 -
            ListItem::new(Text::from(lines))
300 -
        })
301 -
        .collect();
300 +
    let viewport_height = inner.height as usize;
301 +
    if viewport_height > 0 {
302 +
        let mut offset = *scroll_offset as usize;
303 +
        if selected_start < offset {
304 +
            offset = selected_start;
305 +
        } else if selected_end > offset + viewport_height {
306 +
            offset = selected_end.saturating_sub(viewport_height);
307 +
        }
308 +
        offset = offset.min(lines.len().saturating_sub(viewport_height));
309 +
        *scroll_offset = offset.min(u16::MAX as usize) as u16;
310 +
    }
302 311
303 312
    frame.render_widget(block, center);
304 -
    frame.render_stateful_widget(List::new(list_items).highlight_symbol(""), inner, state);
313 +
    frame.render_widget(
314 +
        Paragraph::new(Text::from(lines)).scroll((*scroll_offset, 0)),
315 +
        inner,
316 +
    );
305 317
}