From db1099437729ccd0557e064a4aa15460a59213ca Mon Sep 17 00:00:00 2001 From: James Campos Date: Mon, 3 Aug 2020 16:09:13 -0700 Subject: view.rs --- src/main.rs | 416 ++-------------------------------------------------------- src/view.rs | 430 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 439 insertions(+), 407 deletions(-) create mode 100644 src/view.rs diff --git a/src/main.rs b/src/main.rs index 0acbc59..0b322c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,14 @@ use anyhow::Result; use crossterm::{ cursor, - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseEvent}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event}, queue, - style::{Attribute, Print}, + style::{self, Print}, terminal, }; use serde::{Deserialize, Serialize}; use std::{ - cmp::{min, Ordering}, + cmp::min, collections::HashMap, env, fs, io::{stdout, Write}, @@ -17,6 +17,9 @@ use std::{ }; use unicode_width::UnicodeWidthChar; +mod view; +use view::{Nav, Page, Search, View}; + mod epub; use epub::Chapter; @@ -75,7 +78,7 @@ fn wrap(text: &str, max_cols: usize) -> Vec<(usize, usize)> { lines } -fn get_line(lines: &Vec<(usize, usize)>, byte: usize) -> usize { +fn get_line(lines: &[(usize, usize)], byte: usize) -> usize { match lines.binary_search_by_key(&byte, |&(a, _)| a) { Ok(n) => n, Err(n) => n - 1, @@ -93,408 +96,7 @@ enum Direction { Prev, } -trait View { - fn render(&self, bk: &Bk) -> Vec; - fn on_key(&self, bk: &mut Bk, kc: KeyCode); - fn on_mouse(&self, _: &mut Bk, _: MouseEvent) {} -} - -// TODO render something useful? -struct Mark; -impl View for Mark { - fn on_key(&self, bk: &mut Bk, kc: KeyCode) { - if let KeyCode::Char(c) = kc { - bk.mark(c) - } - bk.view = Some(&Page) - } - fn render(&self, bk: &Bk) -> Vec { - Page::render(&Page, bk) - } -} - -struct Jump; -impl View for Jump { - fn on_key(&self, bk: &mut Bk, kc: KeyCode) { - if let KeyCode::Char(c) = kc { - if let Some(&pos) = bk.mark.get(&c) { - bk.jump(pos); - } - } - bk.view = Some(&Page); - } - fn render(&self, bk: &Bk) -> Vec { - Page::render(&Page, bk) - } -} - -struct Metadata; -impl View for Metadata { - fn on_key(&self, bk: &mut Bk, _: KeyCode) { - bk.view = Some(&Page); - } - fn render(&self, bk: &Bk) -> Vec { - let lines: Vec = bk.chapters.iter().map(|c| c.lines.len()).collect(); - let current = lines[..bk.chapter].iter().sum::() + bk.line; - let total = lines.iter().sum::(); - let progress = current as f32 / total as f32 * 100.0; - - let pages = lines[bk.chapter] / bk.rows; - let page = bk.line / bk.rows; - - let mut vec = vec![ - format!("chapter: {}/{}", page, pages), - format!("total: {:.0}%", progress), - String::new(), - ]; - vec.extend_from_slice(&bk.meta); - vec - } -} - -struct Help; -impl View for Help { - fn on_key(&self, bk: &mut Bk, _: KeyCode) { - bk.view = Some(&Page); - } - fn render(&self, _: &Bk) -> Vec { - let text = r#" - Esc q Quit - Fn Help - Tab Table of Contents - i Progress and Metadata - -PageDown Right Space f l Page Down - PageUp Left b h Page Up - d Half Page Down - u Half Page Up - Down j Line Down - Up k Line Up - Home g Chapter Start - End G Chapter End - [ Previous Chapter - ] Next Chapter - - / Search Forward - ? Search Backward - n Repeat search forward - N Repeat search backward - mx Set mark x - 'x Jump to mark x - "#; - - text.lines().map(String::from).collect() - } -} - -struct Nav; -impl View for Nav { - fn on_key(&self, bk: &mut Bk, kc: KeyCode) { - match kc { - KeyCode::Esc - | KeyCode::Tab - | KeyCode::Left - | KeyCode::Char('h') - | KeyCode::Char('q') => { - bk.jump_reset(); - bk.view = Some(&Page); - } - KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => { - bk.line = 0; - bk.view = Some(&Page); - } - KeyCode::Down | KeyCode::Char('j') => { - if bk.chapter < bk.chapters.len() - 1 { - bk.chapter += 1; - if bk.chapter == bk.nav_top + bk.rows { - bk.nav_top += 1; - } - } - } - KeyCode::Up | KeyCode::Char('k') => { - if bk.chapter > 0 { - if bk.chapter == bk.nav_top { - bk.nav_top -= 1; - } - bk.chapter -= 1; - } - } - KeyCode::Home | KeyCode::Char('g') => { - bk.chapter = 0; - bk.nav_top = 0; - } - KeyCode::End | KeyCode::Char('G') => { - bk.chapter = bk.chapters.len() - 1; - bk.nav_top = bk.chapters.len().saturating_sub(bk.rows); - } - _ => (), - } - } - fn render(&self, bk: &Bk) -> Vec { - let end = min(bk.nav_top + bk.rows, bk.chapters.len()); - - bk.chapters[bk.nav_top..end] - .iter() - .enumerate() - .map(|(i, chapter)| { - if bk.chapter == bk.nav_top + i { - format!( - "{}{}{}", - Attribute::Reverse, - chapter.title, - Attribute::Reset - ) - } else { - chapter.title.to_string() - } - }) - .collect() - } -} - -struct Page; -impl View for Page { - fn on_mouse(&self, bk: &mut Bk, e: MouseEvent) { - match e { - MouseEvent::Down(_, col, row, _) => { - let c = bk.chap(); - let line = bk.line + row as usize; - - if col < bk.pad() || line >= c.lines.len() { - return; - } - let (start, end) = c.lines[line]; - let line_col = (col - bk.pad()) as usize; - - let mut cols = 0; - let mut found = false; - let mut byte = start; - for (i, c) in c.text[start..end].char_indices() { - cols += c.width().unwrap(); - if cols > line_col { - byte += i; - found = true; - break; - } - } - - if !found { - return; - } - - let r = c.links.binary_search_by(|&(start, end, _)| { - if start > byte { - Ordering::Greater - } else if end <= byte { - Ordering::Less - } else { - Ordering::Equal - } - }); - - if let Ok(i) = r { - let url = &c.links[i].2; - let &(chapter, byte) = bk.links.get(url).unwrap(); - let line = get_line(&bk.chapters[chapter].lines, byte); - bk.jump((chapter, line)); - } - } - MouseEvent::ScrollDown(_, _, _) => bk.scroll_down(3), - MouseEvent::ScrollUp(_, _, _) => bk.scroll_up(3), - _ => (), - } - } - fn on_key(&self, bk: &mut Bk, kc: KeyCode) { - match kc { - KeyCode::Esc | KeyCode::Char('q') => bk.view = None, - KeyCode::Tab => { - bk.nav_top = bk.chapter.saturating_sub(bk.rows - 1); - bk.mark('\''); - bk.view = Some(&Nav); - } - KeyCode::F(_) => bk.view = Some(&Help), - KeyCode::Char('m') => bk.view = Some(&Mark), - KeyCode::Char('\'') => bk.view = Some(&Jump), - KeyCode::Char('i') => bk.view = Some(&Metadata), - KeyCode::Char('?') => bk.start_search(Direction::Prev), - KeyCode::Char('/') => bk.start_search(Direction::Next), - KeyCode::Char('N') => { - bk.search(SearchArgs { - dir: Direction::Prev, - skip: true, - }); - } - KeyCode::Char('n') => { - bk.search(SearchArgs { - dir: Direction::Next, - skip: true, - }); - } - KeyCode::End | KeyCode::Char('G') => { - bk.mark('\''); - bk.line = bk.chap().lines.len().saturating_sub(bk.rows); - } - KeyCode::Home | KeyCode::Char('g') => { - bk.mark('\''); - bk.line = 0; - } - KeyCode::Char('d') => bk.scroll_down(bk.rows / 2), - KeyCode::Char('u') => bk.scroll_up(bk.rows / 2), - KeyCode::Up | KeyCode::Char('k') => bk.scroll_up(3), - KeyCode::Left | KeyCode::PageUp | KeyCode::Char('b') | KeyCode::Char('h') => { - bk.scroll_up(bk.rows); - } - KeyCode::Down | KeyCode::Char('j') => bk.scroll_down(3), - KeyCode::Right - | KeyCode::PageDown - | KeyCode::Char('f') - | KeyCode::Char('l') - | KeyCode::Char(' ') => bk.scroll_down(bk.rows), - KeyCode::Char('[') => bk.prev_chapter(), - KeyCode::Char(']') => bk.next_chapter(), - _ => (), - } - } - fn render(&self, bk: &Bk) -> Vec { - let c = bk.chap(); - let line_end = min(bk.line + bk.rows, c.lines.len()); - - let attrs = { - let text_start = c.lines[bk.line].0; - let text_end = c.lines[line_end - 1].1; - - let qlen = bk.query.len(); - let mut search = Vec::new(); - if qlen > 0 { - for (pos, _) in c.text[text_start..text_end].match_indices(&bk.query) { - search.push((text_start + pos, Attribute::Reverse)); - search.push((text_start + pos + qlen, Attribute::NoReverse)); - } - } - let mut search_iter = search.into_iter().peekable(); - - let mut merged = Vec::new(); - let attr_start = match c - .attrs - .binary_search_by_key(&text_start, |&(pos, _, _)| pos) - { - Ok(n) => n, - Err(n) => n - 1, - }; - let mut attrs_iter = c.attrs[attr_start..].iter(); - let (_, _, attr) = attrs_iter.next().unwrap(); - if attr.has(Attribute::Bold) { - merged.push((text_start, Attribute::Bold)); - } - if attr.has(Attribute::Italic) { - merged.push((text_start, Attribute::Italic)); - } - if attr.has(Attribute::Underlined) { - merged.push((text_start, Attribute::Underlined)); - } - let mut attrs_iter = attrs_iter - .map(|&(pos, a, _)| (pos, a)) - .take_while(|(pos, _)| pos <= &text_end) - .peekable(); - - // use itertools? - loop { - match (search_iter.peek(), attrs_iter.peek()) { - (None, None) => break, - (Some(_), None) => { - merged.extend(search_iter); - break; - } - (None, Some(_)) => { - merged.extend(attrs_iter); - break; - } - (Some(&s), Some(&a)) => { - if s.0 < a.0 { - merged.push(s); - search_iter.next(); - } else { - merged.push(a); - attrs_iter.next(); - } - } - } - } - - merged - }; - - let mut buf = Vec::new(); - let mut iter = attrs.into_iter().peekable(); - for &(mut start, end) in &c.lines[bk.line..line_end] { - let mut s = String::new(); - while let Some(&(pos, attr)) = iter.peek() { - if pos > end { - break; - } - s.push_str(&c.text[start..pos]); - s.push_str(&attr.to_string()); - start = pos; - iter.next(); - } - s.push_str(&c.text[start..end]); - buf.push(s); - } - buf - } -} - -struct Search; -impl View for Search { - fn on_key(&self, bk: &mut Bk, kc: KeyCode) { - match kc { - KeyCode::Esc => { - bk.jump_reset(); - bk.view = Some(&Page); - } - KeyCode::Enter => { - bk.view = Some(&Page); - } - KeyCode::Backspace => { - bk.query.pop(); - bk.jump_reset(); - bk.search(SearchArgs { - dir: bk.dir.clone(), - skip: false, - }); - } - KeyCode::Char(c) => { - bk.query.push(c); - let args = SearchArgs { - dir: bk.dir.clone(), - skip: false, - }; - if !bk.search(args) { - bk.jump_reset(); - } - } - _ => (), - } - } - fn render(&self, bk: &Bk) -> Vec { - let mut buf = Page::render(&Page, bk); - if buf.len() == bk.rows { - buf.pop(); - } else { - for _ in buf.len()..bk.rows - 1 { - buf.push(String::new()); - } - } - let prefix = match bk.dir { - Direction::Next => '/', - Direction::Prev => '?', - }; - buf.push(format!("{}{}", prefix, bk.query)); - buf - } -} - -struct Bk<'a> { +pub struct Bk<'a> { chapters: Vec, // position in the book chapter: usize, @@ -573,7 +175,7 @@ impl Bk<'_> { queue!( stdout, terminal::Clear(terminal::ClearType::All), - Print(Attribute::Reset) + Print(style::Attribute::Reset) )?; for (i, line) in view.render(self).iter().enumerate() { queue!(stdout, cursor::MoveTo(self.pad(), i as u16), Print(line))?; diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 0000000..d5818df --- /dev/null +++ b/src/view.rs @@ -0,0 +1,430 @@ +use crossterm::{ + event::{KeyCode, MouseEvent}, + style::Attribute, +}; +use std::cmp::{min, Ordering}; +use unicode_width::UnicodeWidthChar; + +use crate::{get_line, Bk, Direction, SearchArgs}; + +pub trait View { + fn render(&self, bk: &Bk) -> Vec; + fn on_key(&self, bk: &mut Bk, kc: KeyCode); + fn on_mouse(&self, _: &mut Bk, _: MouseEvent) {} +} + +// TODO render something useful? +struct Mark; +impl View for Mark { + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { + if let KeyCode::Char(c) = kc { + bk.mark(c) + } + bk.view = Some(&Page) + } + fn render(&self, bk: &Bk) -> Vec { + Page::render(&Page, bk) + } +} + +struct Jump; +impl View for Jump { + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { + if let KeyCode::Char(c) = kc { + if let Some(&pos) = bk.mark.get(&c) { + bk.jump(pos); + } + } + bk.view = Some(&Page); + } + fn render(&self, bk: &Bk) -> Vec { + Page::render(&Page, bk) + } +} + +struct Metadata; +impl View for Metadata { + fn on_key(&self, bk: &mut Bk, _: KeyCode) { + bk.view = Some(&Page); + } + fn render(&self, bk: &Bk) -> Vec { + let lines: Vec = bk.chapters.iter().map(|c| c.lines.len()).collect(); + let current = lines[..bk.chapter].iter().sum::() + bk.line; + let total = lines.iter().sum::(); + let progress = current as f32 / total as f32 * 100.0; + + let pages = lines[bk.chapter] / bk.rows; + let page = bk.line / bk.rows; + + let mut vec = vec![ + format!("chapter: {}/{}", page, pages), + format!("total: {:.0}%", progress), + String::new(), + ]; + vec.extend_from_slice(&bk.meta); + vec + } +} + +struct Help; +impl View for Help { + fn on_key(&self, bk: &mut Bk, _: KeyCode) { + bk.view = Some(&Page); + } + fn render(&self, _: &Bk) -> Vec { + let text = r#" + Esc q Quit + Fn Help + Tab Table of Contents + i Progress and Metadata + +PageDown Right Space f l Page Down + PageUp Left b h Page Up + d Half Page Down + u Half Page Up + Down j Line Down + Up k Line Up + Home g Chapter Start + End G Chapter End + [ Previous Chapter + ] Next Chapter + + / Search Forward + ? Search Backward + n Repeat search forward + N Repeat search backward + mx Set mark x + 'x Jump to mark x + "#; + + text.lines().map(String::from).collect() + } +} + +pub struct Nav; + +impl Nav { + fn scroll_up(&self, bk: &mut Bk) { + if bk.chapter > 0 { + if bk.chapter == bk.nav_top { + bk.nav_top -= 1; + } + bk.chapter -= 1; + } + } + fn scroll_down(&self, bk: &mut Bk) { + if bk.chapter < bk.chapters.len() - 1 { + bk.chapter += 1; + if bk.chapter == bk.nav_top + bk.rows { + bk.nav_top += 1; + } + } + } + fn click(&self, bk: &mut Bk, row: usize) { + if bk.nav_top + row < bk.chapters.len() { + bk.chapter = bk.nav_top + row; + bk.line = 0; + bk.view = Some(&Page); + } + } +} + +impl View for Nav { + fn on_mouse(&self, bk: &mut Bk, e: MouseEvent) { + match e { + MouseEvent::Down(_, _, row, _) => self.click(bk, row as usize), + MouseEvent::ScrollDown(_, _, _) => self.scroll_down(bk), + MouseEvent::ScrollUp(_, _, _) => self.scroll_up(bk), + _ => (), + } + } + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { + match kc { + KeyCode::Esc + | KeyCode::Tab + | KeyCode::Left + | KeyCode::Char('h') + | KeyCode::Char('q') => { + bk.jump_reset(); + bk.view = Some(&Page); + } + KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => { + bk.line = 0; + bk.view = Some(&Page); + } + KeyCode::Down | KeyCode::Char('j') => self.scroll_down(bk), + KeyCode::Up | KeyCode::Char('k') => self.scroll_up(bk), + KeyCode::Home | KeyCode::Char('g') => { + bk.chapter = 0; + bk.nav_top = 0; + } + KeyCode::End | KeyCode::Char('G') => { + bk.chapter = bk.chapters.len() - 1; + bk.nav_top = bk.chapters.len().saturating_sub(bk.rows); + } + _ => (), + } + } + fn render(&self, bk: &Bk) -> Vec { + let end = min(bk.nav_top + bk.rows, bk.chapters.len()); + + bk.chapters[bk.nav_top..end] + .iter() + .enumerate() + .map(|(i, chapter)| { + if bk.chapter == bk.nav_top + i { + format!( + "{}{}{}", + Attribute::Reverse, + chapter.title, + Attribute::Reset + ) + } else { + chapter.title.to_string() + } + }) + .collect() + } +} + +pub struct Page; +impl View for Page { + fn on_mouse(&self, bk: &mut Bk, e: MouseEvent) { + match e { + MouseEvent::Down(_, col, row, _) => { + let c = bk.chap(); + let line = bk.line + row as usize; + + if col < bk.pad() || line >= c.lines.len() { + return; + } + let (start, end) = c.lines[line]; + let line_col = (col - bk.pad()) as usize; + + let mut cols = 0; + let mut found = false; + let mut byte = start; + for (i, c) in c.text[start..end].char_indices() { + cols += c.width().unwrap(); + if cols > line_col { + byte += i; + found = true; + break; + } + } + + if !found { + return; + } + + let r = c.links.binary_search_by(|&(start, end, _)| { + if start > byte { + Ordering::Greater + } else if end <= byte { + Ordering::Less + } else { + Ordering::Equal + } + }); + + if let Ok(i) = r { + let url = &c.links[i].2; + let &(chapter, byte) = bk.links.get(url).unwrap(); + let line = get_line(&bk.chapters[chapter].lines, byte); + bk.jump((chapter, line)); + } + } + MouseEvent::ScrollDown(_, _, _) => bk.scroll_down(3), + MouseEvent::ScrollUp(_, _, _) => bk.scroll_up(3), + _ => (), + } + } + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { + match kc { + KeyCode::Esc | KeyCode::Char('q') => bk.view = None, + KeyCode::Tab => { + bk.nav_top = bk.chapter.saturating_sub(bk.rows - 1); + bk.mark('\''); + bk.view = Some(&Nav); + } + KeyCode::F(_) => bk.view = Some(&Help), + KeyCode::Char('m') => bk.view = Some(&Mark), + KeyCode::Char('\'') => bk.view = Some(&Jump), + KeyCode::Char('i') => bk.view = Some(&Metadata), + KeyCode::Char('?') => bk.start_search(Direction::Prev), + KeyCode::Char('/') => bk.start_search(Direction::Next), + KeyCode::Char('N') => { + bk.search(SearchArgs { + dir: Direction::Prev, + skip: true, + }); + } + KeyCode::Char('n') => { + bk.search(SearchArgs { + dir: Direction::Next, + skip: true, + }); + } + KeyCode::End | KeyCode::Char('G') => { + bk.mark('\''); + bk.line = bk.chap().lines.len().saturating_sub(bk.rows); + } + KeyCode::Home | KeyCode::Char('g') => { + bk.mark('\''); + bk.line = 0; + } + KeyCode::Char('d') => bk.scroll_down(bk.rows / 2), + KeyCode::Char('u') => bk.scroll_up(bk.rows / 2), + KeyCode::Up | KeyCode::Char('k') => bk.scroll_up(3), + KeyCode::Left | KeyCode::PageUp | KeyCode::Char('b') | KeyCode::Char('h') => { + bk.scroll_up(bk.rows); + } + KeyCode::Down | KeyCode::Char('j') => bk.scroll_down(3), + KeyCode::Right + | KeyCode::PageDown + | KeyCode::Char('f') + | KeyCode::Char('l') + | KeyCode::Char(' ') => bk.scroll_down(bk.rows), + KeyCode::Char('[') => bk.prev_chapter(), + KeyCode::Char(']') => bk.next_chapter(), + _ => (), + } + } + fn render(&self, bk: &Bk) -> Vec { + let c = bk.chap(); + let line_end = min(bk.line + bk.rows, c.lines.len()); + + let attrs = { + let text_start = c.lines[bk.line].0; + let text_end = c.lines[line_end - 1].1; + + let qlen = bk.query.len(); + let mut search = Vec::new(); + if qlen > 0 { + for (pos, _) in c.text[text_start..text_end].match_indices(&bk.query) { + search.push((text_start + pos, Attribute::Reverse)); + search.push((text_start + pos + qlen, Attribute::NoReverse)); + } + } + let mut search_iter = search.into_iter().peekable(); + + let mut merged = Vec::new(); + let attr_start = match c + .attrs + .binary_search_by_key(&text_start, |&(pos, _, _)| pos) + { + Ok(n) => n, + Err(n) => n - 1, + }; + let mut attrs_iter = c.attrs[attr_start..].iter(); + let (_, _, attr) = attrs_iter.next().unwrap(); + if attr.has(Attribute::Bold) { + merged.push((text_start, Attribute::Bold)); + } + if attr.has(Attribute::Italic) { + merged.push((text_start, Attribute::Italic)); + } + if attr.has(Attribute::Underlined) { + merged.push((text_start, Attribute::Underlined)); + } + let mut attrs_iter = attrs_iter + .map(|&(pos, a, _)| (pos, a)) + .take_while(|(pos, _)| pos <= &text_end) + .peekable(); + + // use itertools? + loop { + match (search_iter.peek(), attrs_iter.peek()) { + (None, None) => break, + (Some(_), None) => { + merged.extend(search_iter); + break; + } + (None, Some(_)) => { + merged.extend(attrs_iter); + break; + } + (Some(&s), Some(&a)) => { + if s.0 < a.0 { + merged.push(s); + search_iter.next(); + } else { + merged.push(a); + attrs_iter.next(); + } + } + } + } + + merged + }; + + let mut buf = Vec::new(); + let mut iter = attrs.into_iter().peekable(); + for &(mut start, end) in &c.lines[bk.line..line_end] { + let mut s = String::new(); + while let Some(&(pos, attr)) = iter.peek() { + if pos > end { + break; + } + s.push_str(&c.text[start..pos]); + s.push_str(&attr.to_string()); + start = pos; + iter.next(); + } + s.push_str(&c.text[start..end]); + buf.push(s); + } + buf + } +} + +pub struct Search; +impl View for Search { + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { + match kc { + KeyCode::Esc => { + bk.jump_reset(); + bk.view = Some(&Page); + } + KeyCode::Enter => { + bk.view = Some(&Page); + } + KeyCode::Backspace => { + bk.query.pop(); + bk.jump_reset(); + bk.search(SearchArgs { + dir: bk.dir.clone(), + skip: false, + }); + } + KeyCode::Char(c) => { + bk.query.push(c); + let args = SearchArgs { + dir: bk.dir.clone(), + skip: false, + }; + if !bk.search(args) { + bk.jump_reset(); + } + } + _ => (), + } + } + fn render(&self, bk: &Bk) -> Vec { + let mut buf = Page::render(&Page, bk); + if buf.len() == bk.rows { + buf.pop(); + } else { + for _ in buf.len()..bk.rows - 1 { + buf.push(String::new()); + } + } + let prefix = match bk.dir { + Direction::Next => '/', + Direction::Prev => '?', + }; + buf.push(format!("{}{}", prefix, bk.query)); + buf + } +} -- cgit v1.2.3