Browse Source

ui: make network requests run in the background

Benton Edmondson 1 year ago
parent
commit
f66ff04b82
8 changed files with 164 additions and 106 deletions
  1. 14 7
      main.go
  2. 3 3
      pub/activity.go
  3. 10 11
      pub/actor.go
  4. 14 8
      pub/collection.go
  5. 4 4
      pub/failure.go
  6. 2 2
      pub/interfaces.go
  7. 18 15
      pub/post.go
  8. 99 56
      ui/ui.go

+ 14 - 7
main.go

@@ -1,11 +1,11 @@
 package main
 
 import (
-	"fmt"
 	"os"
 	"golang.org/x/term"
 	"strings"
 	"mimicry/ui"
+	"time"
 )
 
 // TODO: clean up most panics
@@ -17,13 +17,21 @@ func main() {
 	oldTerminal, err := term.MakeRaw(int(os.Stdin.Fd()))
 	if err != nil { panic(err) }
 	defer term.Restore(int(os.Stdin.Fd()), oldTerminal)
-	width, heightInt, err := term.GetSize(int(os.Stdin.Fd()))
+	width, height, err := term.GetSize(int(os.Stdin.Fd()))
 	if err != nil { panic(err) }
-	height := uint(heightInt)
 	printRaw("")
 
-	state := ui.Start(os.Args[1])
-	printRaw(state.View(width, height))
+	state := ui.Start(os.Args[1], printRaw)
+	state.SetWidthHeight(width, height)
+
+	go func() {
+		for {
+			time.Sleep(1 * time.Second)
+			width, height, err := term.GetSize(int(os.Stdin.Fd()))
+			if err != nil { panic(err) }
+			state.SetWidthHeight(width, height)
+		}
+	}()
 
 	buffer := make([]byte, 1)
 	for {
@@ -36,12 +44,11 @@ func main() {
 		}
 
 		state.Update(input)
-		printRaw(state.View(width, height))
 	}
 }
 
 func printRaw(output string) {
 	output = strings.ReplaceAll(output, "\n", "\r\n")
-	_, err := fmt.Print("\x1b[0;0H\x1b[2J" + output)
+	_, err := os.Stdout.WriteString("\x1b[0;0H\x1b[2J" + output)
 	if err != nil { panic(err) }
 }

+ 3 - 3
pub/activity.go

@@ -96,10 +96,10 @@ func (a *Activity) Preview(width int) string {
 	return output
 }
 
-func (a *Activity) Children(quantity uint) ([]Tangible, Container, uint) {
-	return a.target.Children(quantity)
+func (a *Activity) Children() Container {
+	return a.target.Children()
 }
 
-func (a *Activity) Parents(quantity uint) []Tangible {
+func (a *Activity) Parents(quantity uint) ([]Tangible, Tangible) {
 	return a.target.Parents(quantity)
 }

+ 10 - 11
pub/actor.go

@@ -73,20 +73,19 @@ func (a *Actor) Kind() string {
 	return a.kind
 }
 
-func (a *Actor) Parents(quantity uint) []Tangible {
-	return []Tangible{}
+func (a *Actor) Parents(quantity uint) ([]Tangible, Tangible) {
+	return []Tangible{}, nil
 }
 
-func (a *Actor) Children(quantity uint) ([]Tangible, Container, uint) {
-	if errors.Is(a.postsErr, object.ErrKeyNotPresent) {
-		return []Tangible{}, nil, 0
-	}
-	if a.postsErr != nil {
-		return []Tangible{
-			NewFailure(a.postsErr),
-		}, nil, 0
+func (a *Actor) Children() Container {
+	/* the if is necessary because my understanding is
+	   the first nil is a (*Collection)(nil) whereas
+	   the second is (Container)(nil) */
+	if a.posts == nil {
+		return nil
+	} else {
+		return a.posts
 	}
-	return a.posts.Harvest(quantity, 0)
 }
 
 // TODO: here is where I'd put forgery errors in

+ 14 - 8
pub/collection.go

@@ -78,6 +78,10 @@ func (c *Collection) Size() (uint64, error) {
 }
 
 func (c *Collection) Harvest(amount uint, startingPoint uint) ([]Tangible, Container, uint) {
+	if c == nil {
+		panic("can't harvest nil collection")
+	}
+
 	if c.elementsErr != nil && !errors.Is(c.elementsErr, object.ErrKeyNotPresent) {
 		return []Tangible{NewFailure(c.elementsErr)}, nil, 0
 	}
@@ -118,19 +122,21 @@ func (c *Collection) Harvest(amount uint, startingPoint uint) ([]Tangible, Conta
 
 	wg.Add(1)
 	go func() {
-		if errors.Is(c.nextErr, object.ErrKeyNotPresent) || length > amount + startingPoint {
+		if length > amount + startingPoint {
 			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, c, amount + startingPoint
+		} else if errors.Is(c.nextErr, object.ErrKeyNotPresent) {
+			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, nil, 0
+		} else if c.nextErr != nil {
+			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(c.nextErr)}, nil, 0
+		} else if next, err := NewCollection(c.next, c.id); err != nil {
+			fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(err)}, nil, 0
 		} else {
-			if c.nextErr != nil {
-				fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(c.nextErr)}, c, amount + startingPoint
-			} else if next, err := NewCollection(c.next, c.id); err != nil {
-				fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(err)}, c, amount + startingPoint
-			} else {
-				fromLaterPages, nextCollection, nextStartingPoint = next.Harvest(amount - amountFromThisPage, 0)
-			}
+			fromLaterPages, nextCollection, nextStartingPoint = next.Harvest(amount - amountFromThisPage, 0)
 		}
+
 		wg.Done()
 	}()
+
 	wg.Wait()
 
 	return append(fromThisPage, fromLaterPages...), nextCollection, nextStartingPoint

+ 4 - 4
pub/failure.go

@@ -29,10 +29,10 @@ func (f *Failure) String(width int) string {
 	return f.Preview(width)
 }
 
-func (f *Failure) Parents(uint) []Tangible {
-	return []Tangible{}
+func (f *Failure) Parents(uint) ([]Tangible, Tangible) {
+	return []Tangible{}, nil
 }
 
-func (f *Failure) Children(uint) ([]Tangible, Container, uint) {
-	return []Tangible{}, nil, 0
+func (f *Failure) Children() Container {
+	return nil
 }

+ 2 - 2
pub/interfaces.go

@@ -9,8 +9,8 @@ type Tangible interface {
 
 	String(width int) string
 	Preview(width int) string
-	Parents(quantity uint) []Tangible
-	Children(quantity uint) ([]Tangible, Container, uint)
+	Parents(quantity uint) ([]Tangible, Tangible)
+	Children() Container
 }
 
 type Container interface {

+ 18 - 15
pub/post.go

@@ -100,33 +100,36 @@ func (p *Post) Kind() (string) {
 	return p.kind
 }
 
-func (p *Post) Children(quantity uint) ([]Tangible, Container, uint) {
-	if errors.Is(p.commentsErr, object.ErrKeyNotPresent) {
-		return []Tangible{}, nil, 0
-	}
-	if p.commentsErr != nil {
-		return []Tangible{
-			NewFailure(p.commentsErr),
-		}, nil, 0
+func (p *Post) Children() Container {
+	/* the if is necessary because my understanding is
+	the first nil is a (*Collection)(nil) whereas
+	the second is (Container)(nil) */
+	if p.comments == nil {
+		return nil
+	} else {
+		return p.comments
 	}
-	return p.comments.Harvest(quantity, 0)
 }
 
-func (p *Post) Parents(quantity uint) []Tangible {
+func (p *Post) Parents(quantity uint) ([]Tangible, Tangible) {
 	if quantity == 0 {
-		return []Tangible{}
+		panic("can't fetch 0 parents")
 	}
 	if errors.Is(p.parentErr, object.ErrKeyNotPresent) {
-		return []Tangible{}
+		return []Tangible{}, nil
 	}
 	if p.parentErr != nil {
-		return []Tangible{NewFailure(p.parentErr)}
+		return []Tangible{NewFailure(p.parentErr)}, nil
 	}
 	fetchedParent, fetchedParentErr := NewPost(p.parent, p.id)
 	if fetchedParentErr != nil {
-		return []Tangible{NewFailure(fetchedParentErr)}
+		return []Tangible{NewFailure(fetchedParentErr)}, nil
+	}
+	if quantity == 1 {
+		return []Tangible{fetchedParent}, fetchedParent
 	}
-	return append([]Tangible{fetchedParent}, fetchedParent.Parents(quantity - 1)...)
+	fetchedParentParents, fetchedParentFrontier := fetchedParent.Parents(quantity - 1)
+	return append([]Tangible{fetchedParent}, fetchedParentParents...), fetchedParentFrontier
 }
 
 func (p *Post) header(width int) string {

+ 99 - 56
ui/ui.go

@@ -6,37 +6,42 @@ import (
 	"mimicry/feed"
 	"fmt"
 	"sync"
+	"mimicry/style"
 )
 
 type State struct {
-	/* the 0 index is special; it is rendered in full, not as a preview.
-	   the others are all rendered as previews. negative indexes represent
-	   parents of the 0th element (found via `inReplyTo`) and positive
-	   elements represent children (found via the pertinent collection,
-	   e.g. `replies` for a post or `outbox` for an actor) */
+	m sync.Mutex
+
 	feed *feed.Feed
 	index int
 	context int
 
+	frontier pub.Tangible
+	loadingUp bool
+
 	page pub.Container
 	basepoint uint
+	loadingDown bool
+
+	width int
+	height int
+
+	output func(string)
 }
 
-func (s *State) View(width int, height uint) string {
-	//return s.feed.Get(0).String(width)
+func (s *State) View() string {
 	var top, center, bottom string
-	//TODO: this should be bounded based on size of feed
 	for i := s.index - s.context; i <= s.index + s.context; i++ {
 		if !s.feed.Contains(i) {
 			continue
 		}
 		var serialized string
 		if i == 0 {
-			serialized = s.feed.Get(i).String(width - 4)
+			serialized = s.feed.Get(i).String(s.width - 4)
 		} else if i > 0 {
-			serialized = "╰ " + ansi.Indent(s.feed.Get(i).Preview(width - 4), "  ", false)
+			serialized = "╰ " + ansi.Indent(s.feed.Get(i).Preview(s.width - 4), "  ", false)
 		} else {
-			serialized = s.feed.Get(i).Preview(width - 4)
+			serialized = s.feed.Get(i).Preview(s.width - 4)
 		}
 		if i == s.index {
 			center = ansi.Indent(serialized, "┃ ", true)
@@ -48,90 +53,128 @@ func (s *State) View(width int, height uint) string {
 			bottom += ansi.Indent("│\n" + serialized, "  ", true)
 		}
 	}
-	return ansi.CenterVertically(top, center, bottom, height)
+	if s.loadingUp {
+		if top != "" { top += "\n" }
+		top = "  " + style.Color("Loading…") + "\n" + top
+	}
+	if s.loadingDown {
+		if bottom != "" { bottom += "\n" }
+		bottom += "\n  " + style.Color("Loading…")
+	}
+	return ansi.CenterVertically(top, center, bottom, uint(s.height))
 }
 
 func (s *State) Update(input byte) {
-	/* Interesting problem, but you will succeed! */
 	switch input {
 	case 'k': // up
-		mayNeedLoading := s.index - s.context - 1
-		if !s.feed.Contains(mayNeedLoading) {
-			if s.feed.Contains(mayNeedLoading + 1) {
-				s.feed.Prepend(s.feed.Get(mayNeedLoading + 1).Parents(1))
-			}
-		}
-
+		s.m.Lock()
 		if s.feed.Contains(s.index - 1) {
 			s.index -= 1
-
-			/* Preload more into the HTTP cache */
-			s.PreloadUp(s.context)
 		}
+		s.loadSurroundings()
+		s.output(s.View())
+		s.m.Unlock()
 	case 'j': // down
-		mayNeedLoading := s.index + 1 + s.context
-		if !s.feed.Contains(mayNeedLoading) {
-			if s.page != nil {
-				var children []pub.Tangible
-				children, s.page, s.basepoint = s.page.Harvest(1, s.basepoint)
-				s.feed.Append(children)
-			}
-		}
-
+		s.m.Lock()
 		if s.feed.Contains(s.index + 1) {
 			s.index += 1
-
-			/* Preload more into the HTTP cache */
-			s.PreloadDown(s.context)
 		}
+		s.loadSurroundings()
+		s.output(s.View())
+		s.m.Unlock()
+	case 'g': // return to OP
+		s.m.Lock()
+		s.index = 0
+		s.output(s.View())
+		s.m.Unlock()
+	case ' ': // select
+		s.m.Lock()
+		s.switchTo(s.feed.Get(s.index))
+		s.output(s.View())
+		s.m.Unlock()
 	}
 	// TODO: the catchall down here will be to look at s.feed.Get(s.index).References()
 	// for urls to switch to
 }
 
-func (s *State) SwitchTo(item pub.Any)  {
+func (s *State) switchTo(item pub.Any)  {
 	switch narrowed := item.(type) {
 	case pub.Tangible:
 		s.feed = feed.Create(narrowed)
-		var parents, children []pub.Tangible
-		var wg sync.WaitGroup
-		wg.Add(2)
-		go func() {parents = narrowed.Parents(uint(s.context)); wg.Done()}()
-		go func() {children, s.page, s.basepoint = narrowed.Children(uint(s.context)); wg.Done()}()
-		wg.Wait()
-		s.feed.Prepend(parents)
-		s.feed.Append(children)
-		s.PreloadUp(s.context)
-		s.PreloadDown(s.context)
+		s.frontier = narrowed
+		s.page = narrowed.Children()
+		s.index = 0
+		s.loadingUp = false
+		s.loadingDown = false
+		s.basepoint = 0
+		s.loadSurroundings()
 	case pub.Container:
 		var children []pub.Tangible
 		children, s.page, s.basepoint = narrowed.Harvest(uint(s.context), 0)
 		s.feed = feed.CreateAndAppend(children)
-		s.PreloadDown(s.context)
 	default:
 		panic(fmt.Sprintf("unrecognized non-Tangible non-Container: %T", item))
 	}
 }
 
-func (s *State) PreloadDown(amount int) {
-	if s.page != nil {
-		go s.page.Harvest(uint(amount), s.basepoint)
+func (s *State) SetWidthHeight(width int, height int) {
+	s.m.Lock()
+	defer s.m.Unlock()
+	if s.width == width && s.height == height {
+		return
 	}
-} 
+	s.width = width
+	s.height = height
+	s.output(s.View())
+}
 
-func (s *State) PreloadUp(amount int) {
-	if s.feed.Contains(s.index - s.context) {
-		go s.feed.Get(s.index - s.context).Parents(uint(amount))
+func (s *State) loadSurroundings() {
+	feed := s.feed
+	frontier := s.frontier
+	page := s.page
+	basepoint := s.basepoint
+	context := s.context
+	if !s.loadingUp && !feed.Contains(s.index - context) && frontier != nil {
+		s.loadingUp = true
+		go func() {
+			parents, newFrontier := frontier.Parents(uint(context))
+			s.m.Lock()
+			feed.Prepend(parents)
+			if feed == s.feed {
+				s.frontier = newFrontier
+				s.loadingUp = false
+				s.output(s.View())
+			}
+			s.m.Unlock()
+		}()
+	}
+	if !s.loadingDown && !feed.Contains(s.index + context) && page != nil {
+		s.loadingDown = true
+		go func() {
+			children, newPage, newBasepoint := page.Harvest(uint(context), basepoint)
+			s.m.Lock()
+			feed.Append(children)
+			if feed == s.feed {
+				s.page = newPage
+				s.basepoint = newBasepoint
+				s.loadingDown = false
+				s.output(s.View())
+			}
+			s.m.Unlock()
+		}()
 	}
 }
 
-func Start(input string) *State {
+func Start(input string, output func(string)) *State {
 	item := pub.FetchUserInput(input)
 	s := &State{
 		feed: &feed.Feed{},
 		index: 0,
 		context: 5,
+		output: output,
 	}
-	s.SwitchTo(item)
+	s.m.Lock()
+	s.switchTo(item)
+	s.m.Unlock()
 	return s
 }