Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
bin: Remove page size logic
Erik Kundt committed 1 year ago
commit ac71aa514ab6b184680a3dc1be032de6e31b2a17
parent 294e93e72392acd08c17abf81854271f05f8bfd4
6 files changed +110 -236
modified bin/commands/inbox/select.rs
@@ -64,9 +64,9 @@ pub enum AppPage {
pub struct BrowserState {
    items: Vec<NotificationItem>,
    selected: Option<usize>,
+
    scroll: usize,
    filter: NotificationItemFilter,
    search: store::StateValue<String>,
-
    page_size: usize,
    show_search: bool,
}

@@ -82,8 +82,7 @@ impl BrowserState {

#[derive(Clone, Debug)]
pub struct HelpState {
-
    progress: usize,
-
    page_size: usize,
+
    scroll: usize,
}

#[derive(Clone, Debug)]
@@ -192,31 +191,35 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items: notifications,
                selected: Some(0),
+
                scroll: 0,
                filter,
                search,
                show_search: false,
-
                page_size: 1,
-
            },
-
            help: HelpState {
-
                progress: 0,
-
                page_size: 1,
            },
+
            help: HelpState { scroll: 0 },
        })
    }
}

pub enum Message {
-
    Exit { selection: Option<Selection> },
-
    Select { selected: Option<usize> },
-
    BrowserPageSize(usize),
-
    HelpPageSize(usize),
+
    Exit {
+
        selection: Option<Selection>,
+
    },
+
    Select {
+
        selected: Option<usize>,
+
        scroll: usize,
+
    },
    OpenSearch,
-
    UpdateSearch { value: String },
+
    UpdateSearch {
+
        value: String,
+
    },
    ApplySearch,
    CloseSearch,
    OpenHelp,
    LeavePage,
-
    ScrollHelp { progress: usize },
+
    ScrollHelp {
+
        scroll: usize,
+
    },
}

impl store::State<Selection> for State {
@@ -225,16 +228,9 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected } => {
+
            Message::Select { selected, scroll } => {
                self.browser.selected = selected;
-
                None
-
            }
-
            Message::BrowserPageSize(size) => {
-
                self.browser.page_size = size;
-
                None
-
            }
-
            Message::HelpPageSize(size) => {
-
                self.help.page_size = size;
+
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -252,11 +248,13 @@ impl store::State<Selection> for State {
                    }
                }

+
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
+
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
@@ -275,8 +273,8 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { progress } => {
-
                self.help.progress = progress;
+
            Message::ScrollHelp { scroll } => {
+
                self.help.scroll = scroll;
                None
            }
        }
@@ -360,23 +358,10 @@ fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Mes
        })
        .on_update(|state: &State| {
            PageProps::default()
-
                .page_size(state.browser.page_size)
                .handle_keys(!state.browser.show_search)
                .to_boxed_any()
                .into()
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::BrowserPageSize(page_size));
-
            }
-
            None
-
        })
}

fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
@@ -394,13 +379,12 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
                    Some(Message::ScrollHelp {
-
                        progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
+
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                    })
                })
-
                .on_update(|state: &State| {
+
                .on_update(|_| {
                    TextAreaProps::default()
                        .text(&help_text())
-
                        .page_size(state.help.page_size)
                        .to_boxed_any()
                        .into()
                }),
@@ -414,7 +398,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                            [
                                Column::new(Text::raw(""), Constraint::Fill(1)),
                                Column::new(
-
                                    span::default(&format!("{}%", state.help.progress)).dim(),
+
                                    span::default(&format!("{}%", state.help.scroll)).dim(),
                                    Constraint::Min(4),
                                ),
                            ]
@@ -442,25 +426,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            Key::Char('?') => Some(Message::LeavePage),
            _ => None,
        })
-
        .on_update(|state: &State| {
-
            PageProps::default()
-
                .page_size(state.help.page_size)
-
                .handle_keys(true)
-
                .to_boxed_any()
-
                .into()
-
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::HelpPageSize(page_size));
-
            }
-
            None
-
        })
+
        .on_update(|_| PageProps::default().handle_keys(true).to_boxed_any().into())
}

fn help_text() -> Text<'static> {
modified bin/commands/inbox/select/ui.rs
@@ -18,7 +18,6 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps};
-
use tui::ui::widget::utils;
use tui::ui::widget::{self, ViewProps};
use tui::ui::widget::{RenderProps, ToWidget, View};

@@ -41,12 +40,12 @@ pub struct BrowserProps<'a> {
    notifications: Vec<NotificationItem>,
    /// Current (selected) table index
    selected: Option<usize>,
+
    /// Current scroll progress
+
    progress: usize,
    /// Notification statistics.
    stats: HashMap<String, usize>,
    /// Table columns
    columns: Vec<Column<'a>>,
-
    /// Current page size (height of table content).
-
    page_size: usize,
    /// If search widget should be shown.
    show_search: bool,
    /// Current search string.
@@ -80,6 +79,7 @@ impl<'a> From<&State> for BrowserProps<'a> {
            header,
            notifications,
            selected: state.browser.selected,
+
            progress: state.browser.scroll,
            stats,
            columns: [
                Column::new("", Constraint::Length(5)),
@@ -94,7 +94,6 @@ impl<'a> From<&State> for BrowserProps<'a> {
                Column::new("", Constraint::Length(18)).hide_small(),
            ]
            .to_vec(),
-
            page_size: state.browser.page_size,
            search: state.browser.search.read(),
            show_search: state.browser.show_search,
        }
@@ -130,8 +129,11 @@ impl Browser {
                    Table::<State, Message, NotificationItem, 9>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
+
                            let (selected, scroll) =
+
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
-
                                selected: s.and_then(|s| s.unwrap_usize()),
+
                                selected: Some(selected),
+
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -142,7 +144,6 @@ impl Browser {
                                .items(state.browser.notifications())
                                .selected(state.browser.selected)
                                .footer(!state.browser.show_search)
-
                                .page_size(state.browser.page_size)
                                .to_boxed_any()
                                .into()
                        }),
@@ -288,17 +289,7 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
        span::default(" Unseen").dim(),
    ]);

-
    let progress = props
-
        .selected
-
        .map(|selected| {
-
            utils::scroll::percent_absolute(
-
                selected.saturating_sub(props.page_size),
-
                props.notifications.len(),
-
                props.page_size,
-
            )
-
        })
-
        .unwrap_or_default();
-
    let progress = span::default(&format!("{}%", progress)).dim();
+
    let progress = span::default(&format!("{}%", props.progress)).dim();

    match NotificationItemFilter::from_str(&props.search)
        .unwrap_or_default()
modified bin/commands/issue/select.rs
@@ -56,10 +56,11 @@ pub enum AppPage {
#[derive(Clone, Debug)]
pub struct BrowserState {
    items: Vec<IssueItem>,
+
    scroll: usize,
    selected: Option<usize>,
    filter: IssueItemFilter,
    search: store::StateValue<String>,
-
    page_size: usize,
+

    show_search: bool,
}

@@ -75,8 +76,7 @@ impl BrowserState {

#[derive(Clone, Debug)]
pub struct HelpState {
-
    progress: usize,
-
    page_size: usize,
+
    scroll: usize,
}

#[derive(Clone, Debug)]
@@ -110,31 +110,35 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items,
                selected: Some(0),
+
                scroll: 0,
                filter,
                search,
                show_search: false,
-
                page_size: 1,
-
            },
-
            help: HelpState {
-
                progress: 0,
-
                page_size: 1,
            },
+
            help: HelpState { scroll: 0 },
        })
    }
}

pub enum Message {
-
    Exit { selection: Option<Selection> },
-
    Select { selected: Option<usize> },
-
    BrowserPageSize(usize),
-
    HelpPageSize(usize),
+
    Exit {
+
        selection: Option<Selection>,
+
    },
+
    Select {
+
        selected: Option<usize>,
+
        scroll: usize,
+
    },
    OpenSearch,
-
    UpdateSearch { value: String },
+
    UpdateSearch {
+
        value: String,
+
    },
    ApplySearch,
    CloseSearch,
    OpenHelp,
    LeavePage,
-
    ScrollHelp { progress: usize },
+
    ScrollHelp {
+
        scroll: usize,
+
    },
}

impl store::State<Selection> for State {
@@ -143,16 +147,9 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected } => {
+
            Message::Select { selected, scroll } => {
                self.browser.selected = selected;
-
                None
-
            }
-
            Message::BrowserPageSize(size) => {
-
                self.browser.page_size = size;
-
                None
-
            }
-
            Message::HelpPageSize(size) => {
-
                self.help.page_size = size;
+
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -170,11 +167,13 @@ impl store::State<Selection> for State {
                    }
                }

+
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
+
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
@@ -193,8 +192,8 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { progress } => {
-
                self.help.progress = progress;
+
            Message::ScrollHelp { scroll } => {
+
                self.help.scroll = scroll;
                None
            }
        }
@@ -278,23 +277,10 @@ fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Mes
        })
        .on_update(|state: &State| {
            PageProps::default()
-
                .page_size(state.browser.page_size)
                .handle_keys(!state.browser.show_search)
                .to_boxed_any()
                .into()
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::BrowserPageSize(page_size));
-
            }
-
            None
-
        })
}

fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
@@ -312,13 +298,12 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
                    Some(Message::ScrollHelp {
-
                        progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
+
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                    })
                })
-
                .on_update(|state: &State| {
+
                .on_update(|_| {
                    TextAreaProps::default()
                        .text(&help_text())
-
                        .page_size(state.help.page_size)
                        .to_boxed_any()
                        .into()
                }),
@@ -332,7 +317,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                            [
                                Column::new(Text::raw(""), Constraint::Fill(1)),
                                Column::new(
-
                                    span::default(&format!("{}%", state.help.progress)).dim(),
+
                                    span::default(&format!("{}%", state.help.scroll)).dim(),
                                    Constraint::Min(4),
                                ),
                            ]
@@ -360,25 +345,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            Key::Char('?') => Some(Message::LeavePage),
            _ => None,
        })
-
        .on_update(|state: &State| {
-
            PageProps::default()
-
                .page_size(state.help.page_size)
-
                .handle_keys(true)
-
                .to_boxed_any()
-
                .into()
-
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::HelpPageSize(page_size));
-
            }
-
            None
-
        })
+
        .on_update(|_| PageProps::default().handle_keys(true).to_boxed_any().into())
}

fn help_text() -> Text<'static> {
modified bin/commands/issue/select/ui.rs
@@ -21,7 +21,6 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps};
-
use tui::ui::widget::utils;
use tui::ui::widget::ViewProps;
use tui::ui::widget::{RenderProps, ToWidget, View};

@@ -43,14 +42,14 @@ pub struct BrowserProps<'a> {
    issues: Vec<IssueItem>,
    /// Current (selected) table index
    selected: Option<usize>,
+
    /// Current scroll progress
+
    progress: usize,
    /// Issue statistics.
    stats: HashMap<String, usize>,
    /// Header columns
    header: Vec<Column<'a>>,
    /// Table columns
    columns: Vec<Column<'a>>,
-
    /// Current page size (height of table content).
-
    page_size: usize,
    /// If search widget should be shown.
    show_search: bool,
    /// Current search string.
@@ -115,7 +114,7 @@ impl<'a> From<&State> for BrowserProps<'a> {
                Column::new("Opened", Constraint::Length(16)).hide_small(),
            ]
            .to_vec(),
-
            page_size: state.browser.page_size,
+
            progress: state.browser.scroll,
            search: state.browser.search.read(),
            show_search: state.browser.show_search,
        }
@@ -145,8 +144,11 @@ impl Browser {
                    Table::<State, Message, IssueItem, 8>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
+
                            let (selected, scroll) =
+
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
-
                                selected: s.and_then(|s| s.unwrap_usize()),
+
                                selected: Some(selected),
+
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -157,7 +159,6 @@ impl Browser {
                                .items(state.browser.issues())
                                .selected(state.browser.selected)
                                .footer(!state.browser.show_search)
-
                                .page_size(state.browser.page_size)
                                .to_boxed_any()
                                .into()
                        }),
@@ -166,7 +167,7 @@ impl Browser {
                    let props = BrowserProps::from(state);

                    FooterProps::default()
-
                        .columns(browse_footer(&props, props.selected))
+
                        .columns(browse_footer(&props))
                        .to_boxed_any()
                        .into()
                }))
@@ -286,7 +287,7 @@ impl View for Browser {
    }
}

-
fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
    let search = Line::from(vec![
        span::default(" Search ").cyan().dim().reversed(),
        span::default(" "),
@@ -314,16 +315,7 @@ fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<C
        span::default(&props.issues.len().to_string()).dim(),
    ]);

-
    let progress = selected
-
        .map(|selected| {
-
            utils::scroll::percent_absolute(
-
                selected.saturating_sub(props.page_size),
-
                props.issues.len(),
-
                props.page_size,
-
            )
-
        })
-
        .unwrap_or_default();
-
    let progress = span::default(&format!("{}%", progress)).dim();
+
    let progress = span::default(&format!("{}%", props.progress)).dim();

    match IssueItemFilter::from_str(&props.search)
        .unwrap_or_default()
modified bin/commands/patch/select.rs
@@ -55,9 +55,9 @@ pub enum AppPage {
pub struct BrowserState {
    items: Vec<PatchItem>,
    selected: Option<usize>,
+
    scroll: usize,
    filter: PatchItemFilter,
    search: store::StateValue<String>,
-
    page_size: usize,
    show_search: bool,
}

@@ -73,8 +73,7 @@ impl BrowserState {

#[derive(Clone, Debug)]
pub struct HelpState {
-
    progress: usize,
-
    page_size: usize,
+
    scroll: usize,
}

#[derive(Clone, Debug)]
@@ -108,31 +107,35 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items,
                selected: Some(0),
+
                scroll: 0,
                filter,
                search,
                show_search: false,
-
                page_size: 1,
-
            },
-
            help: HelpState {
-
                progress: 0,
-
                page_size: 1,
            },
+
            help: HelpState { scroll: 0 },
        })
    }
}

pub enum Message {
-
    Exit { selection: Option<Selection> },
-
    Select { selected: Option<usize> },
-
    BrowserPageSize(usize),
-
    HelpPageSize(usize),
+
    Exit {
+
        selection: Option<Selection>,
+
    },
+
    Select {
+
        selected: Option<usize>,
+
        scroll: usize,
+
    },
    OpenSearch,
-
    UpdateSearch { value: String },
+
    UpdateSearch {
+
        value: String,
+
    },
    ApplySearch,
    CloseSearch,
    OpenHelp,
    LeavePage,
-
    ScrollHelp { progress: usize },
+
    ScrollHelp {
+
        scroll: usize,
+
    },
}

impl store::State<Selection> for State {
@@ -141,16 +144,9 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected } => {
+
            Message::Select { selected, scroll } => {
                self.browser.selected = selected;
-
                None
-
            }
-
            Message::BrowserPageSize(size) => {
-
                self.browser.page_size = size;
-
                None
-
            }
-
            Message::HelpPageSize(size) => {
-
                self.help.page_size = size;
+
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -168,11 +164,13 @@ impl store::State<Selection> for State {
                    }
                }

+
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
+
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
@@ -191,8 +189,8 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { progress } => {
-
                self.help.progress = progress;
+
            Message::ScrollHelp { scroll } => {
+
                self.help.scroll = scroll;
                None
            }
        }
@@ -277,23 +275,10 @@ fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Mes
        })
        .on_update(|state: &State| {
            PageProps::default()
-
                .page_size(state.browser.page_size)
                .handle_keys(!state.browser.show_search)
                .to_boxed_any()
                .into()
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::BrowserPageSize(page_size));
-
            }
-
            None
-
        })
}

fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
@@ -311,13 +296,12 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
                    Some(Message::ScrollHelp {
-
                        progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
+
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                    })
                })
-
                .on_update(|state: &State| {
+
                .on_update(|_| {
                    TextAreaProps::default()
                        .text(&help_text())
-
                        .page_size(state.help.page_size)
                        .to_boxed_any()
                        .into()
                }),
@@ -331,7 +315,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                            [
                                Column::new(Text::raw(""), Constraint::Fill(1)),
                                Column::new(
-
                                    span::default(&format!("{}%", state.help.progress)).dim(),
+
                                    span::default(&format!("{}%", state.help.scroll)).dim(),
                                    Constraint::Min(4),
                                ),
                            ]
@@ -359,25 +343,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            Key::Char('?') => Some(Message::LeavePage),
            _ => None,
        })
-
        .on_update(|state: &State| {
-
            PageProps::default()
-
                .page_size(state.help.page_size)
-
                .handle_keys(true)
-
                .to_boxed_any()
-
                .into()
-
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::HelpPageSize(page_size));
-
            }
-
            None
-
        })
+
        .on_update(|_| PageProps::default().handle_keys(true).to_boxed_any().into())
}

fn help_text() -> Text<'static> {
modified bin/commands/patch/select/ui.rs
@@ -23,7 +23,6 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps};
-
use tui::ui::widget::utils;
use tui::ui::widget::ViewProps;
use tui::ui::widget::{RenderProps, ToWidget, View};

@@ -45,14 +44,14 @@ pub struct BrowserProps<'a> {
    patches: Vec<PatchItem>,
    /// Current (selected) table index
    selected: Option<usize>,
+
    /// Current scroll progress
+
    progress: usize,
    /// Patch statistics.
    stats: HashMap<String, usize>,
    /// Header columns
    header: Vec<Column<'a>>,
    /// Table columns
    columns: Vec<Column<'a>>,
-
    /// Current page size (height of table content).
-
    page_size: usize,
    /// If search widget should be shown.
    show_search: bool,
    /// Current search string.
@@ -91,6 +90,7 @@ impl<'a> From<&State> for BrowserProps<'a> {
            mode: state.mode.clone(),
            patches,
            selected: state.browser.selected,
+
            progress: state.browser.scroll,
            stats,
            header: [
                Column::new(" ● ", Constraint::Length(3)),
@@ -116,7 +116,6 @@ impl<'a> From<&State> for BrowserProps<'a> {
                Column::new("Updated", Constraint::Length(16)).hide_small(),
            ]
            .to_vec(),
-
            page_size: state.browser.page_size,
            show_search: state.browser.show_search,
            search: state.browser.search.read(),
        }
@@ -146,8 +145,11 @@ impl Browser {
                    Table::<State, Message, PatchItem, 9>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
+
                            let (selected, scroll) =
+
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
-
                                selected: s.and_then(|s| s.unwrap_usize()),
+
                                selected: Some(selected),
+
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -158,7 +160,6 @@ impl Browser {
                                .items(state.browser.patches())
                                .selected(state.browser.selected)
                                .footer(!state.browser.show_search)
-
                                .page_size(state.browser.page_size)
                                .to_boxed_any()
                                .into()
                        }),
@@ -168,7 +169,7 @@ impl Browser {
                    let props = BrowserProps::from(state);

                    FooterProps::default()
-
                        .columns(browser_footer(&props, props.selected))
+
                        .columns(browser_footer(&props))
                        .to_boxed_any()
                        .into()
                }))
@@ -300,7 +301,7 @@ impl View for Browser {
    }
}

-
fn browser_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
fn browser_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
    let filter = PatchItemFilter::from_str(&props.search).unwrap_or_default();

    let search = Line::from(vec![
@@ -338,16 +339,7 @@ fn browser_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<
        span::default(&props.patches.len().to_string()).dim(),
    ]);

-
    let progress = selected
-
        .map(|selected| {
-
            utils::scroll::percent_absolute(
-
                selected.saturating_sub(props.page_size),
-
                props.patches.len(),
-
                props.page_size,
-
            )
-
        })
-
        .unwrap_or_default();
-
    let progress = span::default(&format!("{}%", progress)).dim();
+
    let progress = span::default(&format!("{}%", props.progress)).dim();

    match filter.status() {
        Some(state) => {