chiark / gitweb /
Ability to extend the feed backwards in time.
authorSimon Tatham <anakin@pobox.com>
Sat, 30 Dec 2023 09:30:09 +0000 (09:30 +0000)
committerSimon Tatham <anakin@pobox.com>
Sat, 30 Dec 2023 11:22:34 +0000 (11:22 +0000)
Haven't yet tested the part where extension runs out, though. That
will be easier when we get some more kinds of feed, so I've left a
FIXME comment in there.

src/client.rs
src/file.rs
src/tui.rs

index b9393d4e939d40eac739ba84aacdfd39c0fd79e8..85fded56fe6dd6c9a7e5e5cf81dfdeb31cac78c5 100644 (file)
@@ -19,6 +19,7 @@ pub enum FeedId {
     User(String, Boosts, Replies),
 }
 
+#[derive(Debug)]
 pub struct Feed {
     pub ids: VecDeque<String>, // ids, whether of statuses, accounts or what
     pub origin: isize,
@@ -33,7 +34,7 @@ pub struct Client {
     permit_write: bool,
 }
 
-#[derive(Debug)]
+#[derive(Debug, PartialEq, Eq, Clone)]
 pub enum ClientError {
     InternalError(String), // message
     UrlParseError(String, String), // url, message
@@ -228,13 +229,22 @@ impl Client {
         Ok(st)
     }
 
+    // Ok(bool) tells you whether any new items were in fact retrieved
     pub fn fetch_feed(&mut self, id: &FeedId, ext: FeedExtend) ->
-        Result<(), ClientError>
+        Result<bool, ClientError>
     {
         if ext == FeedExtend::Initial {
             if self.feeds.contains_key(id) {
-                // No need to fetch the initial contents - we already have some
-                return Ok(());
+                // No need to fetch the initial contents - we already
+                // have some.
+                //
+                // In this situation the boolean return value is not
+                // expected to very useful: its main purpose is when
+                // finding out if extending an _existing_ feed did
+                // anything. But we might as well return false in the
+                // case where nothing changed, and true in the case
+                // below where we did end up retrieving something.
+                return Ok(false);
             }
         } else {
             assert!(self.feeds.contains_key(id),
@@ -284,6 +294,7 @@ impl Client {
                 Err(ClientError::UrlError(url.clone(), e.to_string()))
             },
         }?;
+        let any_new = !sts.is_empty();
         for st in &sts {
             self.cache_status(st);
         }
@@ -305,12 +316,12 @@ impl Client {
                 let feed = self.feeds.get_mut(id).unwrap();
                 for id in ids.iter().rev() {
                     feed.ids.push_front(id.to_string());
-                    feed.origin += 1;
+                    feed.origin -= 1;
                 }
             },
         }
 
-        Ok(())
+        Ok(any_new)
     }
 
     pub fn borrow_feed(&self, id: &FeedId) -> &Feed {
index b08e32e186b301a1c11bec6f19ca040ff92a36ca..181d4c876ae4c22fd240e1fdf30cc0de1e5c55a9 100644 (file)
@@ -18,14 +18,14 @@ enum FilePosition {
 struct FeedFileContents {
     id: FeedId,
     header: FileHeader,
+    extender: Option<ExtendableIndicator>,
     origin: isize,
     items: Vec<Box<dyn TextFragment>>,
 }
 
 impl FeedFileContents {
     fn update_items(&mut self, client: &mut Client) {
-        // FIXME: deal with origin, and with the feed having been
-        // extended since we last looked
+        // FIXME: deal with the feed having been extended
 
         let feed = client.borrow_feed(&self.id);
         let ids: Vec<_> = feed.ids.iter().map(|x| x.clone()).collect();
@@ -40,7 +40,13 @@ impl FeedFileContents {
         }
     }
 
-    fn first_index(&self) -> isize { self.origin - 1 }
+    fn first_index(&self) -> isize {
+        let extcount = if self.extender.is_some() { 1 } else { 0 };
+        self.origin - 1 - extcount
+    }
+    fn extender_index(&self) -> Option<isize> {
+        if self.extender.is_some() { Some(self.origin - 1) } else { None }
+    }
     fn index_limit(&self) -> isize {
         self.origin.checked_add_unsigned(self.items.len())
             .expect("Out-of-range index")
@@ -53,8 +59,13 @@ impl FeedFileContents {
     }
 
     fn get(&self, index: isize) -> &dyn TextFragment {
-        if index == self.origin - 1 {
+        if index == self.first_index() {
             &self.header
+        } else if Some(index) == self.extender_index() {
+            match self.extender {
+                Some(ref ext) => ext,
+                _ => panic!("But it must be there, extender_index exists"),
+            }
         } else {
             let index = self.phys_index(index);
             &*self.items[index]
@@ -78,6 +89,7 @@ impl FeedFile {
         let mut contents = FeedFileContents {
             id: id,
             header: FileHeader::new(desc),
+            extender: Some(ExtendableIndicator::new()),
             origin: 0,
             items: Vec::new(),
         };
@@ -114,7 +126,7 @@ impl FeedFile {
         self.rendered.get(&index).expect("We just made sure this was present")
     }
 
-    fn ensure_enough_rendered(&mut self) -> usize {
+    fn ensure_enough_rendered(&mut self) -> Option<usize> {
         let (w, h) = self.last_size.expect(
             "ensure_enough_rendered before resize");
 
@@ -122,12 +134,32 @@ impl FeedFile {
 
         let mut item = item;
         let mut lines_rendered = line;
-        while item > self.contents.first_index() && lines_rendered + 1 < h {
+        while item > self.contents.first_index() && lines_rendered < h {
             item -= 1;
             lines_rendered += self.ensure_item_rendered(item, w).len();
         }
 
-        h - min(h, lines_rendered + 1)
+        if lines_rendered + 1 <= h {
+            Some(h - 1 - lines_rendered)
+        } else {
+            None
+        }
+    }
+
+    fn after_setting_pos(&mut self) {
+        let (w, _h) = self.last_size.expect(
+            "ensure_enough_rendered before setting pos");
+        let at_top = self.at_top();
+        match &mut self.contents.extender {
+            Some(ref mut ext) => {
+                ext.set_primed(at_top);
+            },
+            _ => (),
+        };
+        if let Some(ei) = self.contents.extender_index() {
+            self.rendered.remove(&ei);
+            self.ensure_item_rendered(ei, w);
+        }
     }
 
     fn refine_pos(&mut self, w: usize) -> (isize, usize) {
@@ -153,6 +185,20 @@ impl FeedFile {
         self.pos = newpos;
     }
 
+    fn fix_overshoot_at_top(&mut self) {
+        // If an attempt to move up the document left us with no line
+        // to display at the top of the screen, move down again to fix
+        // it
+        if let Some(overshoot) = self.ensure_enough_rendered() {
+            // Clip position at top of file
+            if overshoot > 0 {
+                self.move_down_inner(overshoot);
+            }
+        }
+    }
+
+    fn at_top(&mut self) -> bool { self.ensure_enough_rendered().is_some() }
+
     fn move_up(&mut self, distance: usize) {
         let (w, _h) = self.last_size.expect("move_up before resize");
         let (item, line) = match self.pos {
@@ -176,14 +222,11 @@ impl FeedFile {
             }
         }
         self.pos = FilePosition::Fine(item, line);
-        let overshoot = self.ensure_enough_rendered();
-        // Clip position at top of file
-        if overshoot > 0 {
-            self.move_down(overshoot);
-        }
+        self.fix_overshoot_at_top();
+        self.after_setting_pos();
     }
 
-    fn move_down(&mut self, distance: usize) {
+    fn move_down_inner(&mut self, distance: usize) {
         let (w, _h) = self.last_size.expect("move_down before resize");
         let (item, line) = match self.pos {
             FilePosition::Fine(item, line) => (item, line),
@@ -213,13 +256,15 @@ impl FeedFile {
         self.ensure_enough_rendered();
     }
 
+    fn move_down(&mut self, distance: usize) {
+        self.move_down_inner(distance);
+        self.after_setting_pos();
+    }
+
     fn goto_top(&mut self) {
         self.pos = FilePosition::Fine(self.contents.first_index(), 0);
-        let overshoot = self.ensure_enough_rendered();
-        // Clip position at top of file
-        if overshoot > 0 {
-            self.move_down(overshoot);
-        }
+        self.fix_overshoot_at_top();
+        self.after_setting_pos();
     }
 
     fn goto_bottom(&mut self) {
@@ -228,6 +273,7 @@ impl FeedFile {
         let line = self.ensure_item_rendered(item, w).len();
         self.pos = FilePosition::Fine(item, line);
         self.ensure_enough_rendered();
+        self.after_setting_pos();
     }
 }
 
@@ -239,6 +285,7 @@ impl ActivityState for FeedFile {
             self.rendered.clear();
         }
         self.ensure_enough_rendered();
+        self.after_setting_pos();
     }
 
     fn draw(&self, w: usize, h: usize)
@@ -298,7 +345,7 @@ impl ActivityState for FeedFile {
         (lines, CursorPosition::End)
     }
 
-    fn handle_keypress(&mut self, key: OurKey, _client: &mut Client) ->
+    fn handle_keypress(&mut self, key: OurKey, client: &mut Client) ->
         LogicalAction
     {
         let (_w, h) = match self.last_size {
@@ -334,8 +381,31 @@ impl ActivityState for FeedFile {
                 LogicalAction::Nothing
             },
             Pr('0') | Home => {
-                self.goto_top();
-                LogicalAction::Nothing
+                if self.at_top() && self.contents.extender.is_some() {
+                    match client.fetch_feed(&self.contents.id,
+                                            FeedExtend::Past) {
+                        Ok(any_new) => {
+                            self.rendered.remove(&self.contents.first_index());
+                            if let Some(i) = self.contents.extender_index() {
+                                self.rendered.remove(&i);
+                            }
+
+                            if !any_new {
+                                // Can't extend this any further into the past
+                                // FIXME: this is not tested yet
+                                self.contents.extender = None;
+                            }
+
+                            self.contents.update_items(client);
+                            self.ensure_enough_rendered();
+                            LogicalAction::Nothing
+                        },
+                        Err(e) => LogicalAction::Error(e),
+                    }
+                } else {
+                    self.goto_top();
+                    LogicalAction::Nothing
+                }
             }
             Pr('z') | Pr('Z') | End => {
                 self.goto_bottom();
index 8442b9658177fedc3bc3351b567699f6fd8681aa..beaa9569faed2489420f5563c7f665f620d949f0 100644 (file)
@@ -14,7 +14,7 @@ use std::io::{Stdout, Write, stdout};
 use unicode_width::UnicodeWidthStr;
 
 use super::activity_stack::*;
-use super::client::Client;
+use super::client::{Client, ClientError};
 use super::coloured_string::{ColouredString, ColouredStringSlice};
 use super::menu::*;
 use super::file::*;
@@ -337,6 +337,7 @@ pub enum LogicalAction {
     Goto(Activity),
     Exit,
     Nothing,
+    Error(ClientError), // throw UI into the Error Log
     NYI, // FIXME: get rid of this once everything is implemented
 }
 
@@ -446,6 +447,7 @@ impl TuiLogicalState {
                 self.changed_activity(client);
                 PhysicalAction::Nothing
             },
+            LogicalAction::Error(_) => PhysicalAction::Beep, // FIXME: Error Log
         }
     }