~mariusor/motley

c8234838c5a7340d5095417930990ab8fd1be47d — Marius Orcsik 3 months ago c02c350
Simplified some messages and removed some old writeX methods from item rendering
5 files changed, 115 insertions(+), 289 deletions(-)

M fedbox.go
M item_model.go
M statusbar.go
M tree.go
M ui.go
M fedbox.go => fedbox.go +1 -1
@@ 398,7 398,7 @@ func (m *model) loadDepsForNode(ctx context.Context, node *n) tea.Cmd {
			m.logFn("error while loading children %s", err)
		}
	}
	return m.status.startedLoading
	return m.tree.stoppedLoading
}

func (m *model) loadChildrenForNode(ctx context.Context, nn *n) error {

M item_model.go => item_model.go +51 -229
@@ 2,315 2,137 @@ package motley

import (
	"fmt"
	"io"
	"strings"

	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	vocab "github.com/go-ap/activitypub"
)

var _ tea.Model = itemModel{}
var _ tea.Model = pagerModel{}

type itemModel struct {
type pagerModel struct {
	*commonModel

	viewport viewport.Model

	item vocab.Item

	model tea.Model
	viewport viewport.Model
	model    tea.Model
}

func (i *itemModel) setSize(w, h int) {
	i.viewport.Height = h
	i.viewport.Width = w
func (p *pagerModel) setSize(w, h int) {
	p.viewport.Height = h
	p.viewport.Width = w
}

func (i itemModel) View() string {
	h := i.viewport.Height
	w := i.viewport.Width
func (p pagerModel) View() string {
	h := p.viewport.Height
	w := p.viewport.Width
	s := lipgloss.NewStyle().Height(h).MaxHeight(h).MaxWidth(w).Width(w)
	i.viewport.SetContent(s.Render(i.model.View()))
	return i.viewport.View()
}

func (i itemModel) writeActorItemIdentifier(s io.Writer, it vocab.Item) {
	i.writeActorIdentifier(s, it)
}

func (i itemModel) writeItemWithLabel(s io.Writer, l string, it vocab.Item) {
	if vocab.IsNil(it) {
		return
	}
	if c, ok := it.(vocab.ItemCollection); ok && len(c) == 0 {
		return
	}
	fmt.Fprintf(s, "%s: ", l)
	i.writeItem(s, it)
	s.Write([]byte{'\n'})
}

func (i itemModel) writeObject(s io.Writer) func(ob *vocab.Object) error {
	return func(ob *vocab.Object) error {
		fmt.Fprintf(s, "IRI: %s\n", ob.ID)
		if len(ob.Type) > 0 {
			m := textinput.New()
			m.Prompt = "Type: "
			m.SetValue(string(ob.Type))
			fmt.Fprintf(s, "%s\n", m.View())
		}
		if len(ob.MediaType) > 0 {
			fmt.Fprintf(s, "MediaType: %s\n", ob.MediaType)
		}

		//i.writeActorItemIdentifier(s, ob.AttributedTo)
		//fmt.Fprintf(s, "Recipients: ")
		//i.writeActorItemIdentifier(s, ob.To)
		//i.writeItemWithLabel(s, "CC", ob.CC)
		//i.writeItemWithLabel(s, "Bto", ob.Bto)
		//i.writeItemWithLabel(s, "BCC", ob.BCC)
		//i.writeItemWithLabel(s, "Audience", ob.Audience)

		i.writeNaturalLanguageValuesWithLabel(s, "Name", ob.Name)
		//i.writeNaturalLanguageValuesWithLabel(s, "Summary", ob.Summary)
		//i.writeNaturalLanguageValuesWithLabel(s, "Content", ob.Content)

		//if ob.Source.Content != nil {
		//	if len(ob.MediaType) > 0 {
		//		fmt.Fprintf(s, "Source[%s]: %s\n", ob.Source.MediaType, ob.Source.Content)
		//	} else {
		//		fmt.Fprintf(s, "Source: %s\n", ob.Source.Content)
		//	}
		//}

		//i.writeItemWithLabel(s, "URL", ob.URL)
		//
		//i.writeItemWithLabel(s, "Context", ob.Context)
		//i.writeItemWithLabel(s, "InReplyTo", ob.InReplyTo)
		//
		//i.writeItemWithLabel(s, "Tag", ob.Tag)
		return nil
	}
}

func (i itemModel) writeActivity(s io.Writer) func(act *vocab.Activity) error {
	return func(act *vocab.Activity) error {
		if err := vocab.OnIntransitiveActivity(act, i.writeIntransitiveActivity(s)); err != nil {
			return err
		}
		//i.writeItemWithLabel(s, "Object", act.Object)
		return nil
	}
}

func (i itemModel) writeIntransitiveActivity(s io.Writer) func(act *vocab.IntransitiveActivity) error {
	return func(act *vocab.IntransitiveActivity) error {
		if err := vocab.OnObject(act, i.writeObject(s)); err != nil {
			return err
		}
		//i.writeItemWithLabel(s, "Actor", act.Actor)
		//i.writeItemWithLabel(s, "Target", act.Target)
		//i.writeItemWithLabel(s, "Result", act.Result)
		//i.writeItemWithLabel(s, "Origin", act.Origin)
		//i.writeItemWithLabel(s, "Instrument", act.Instrument)
		return nil
	}
}

func (i itemModel) writeActor(s io.Writer) func(act *vocab.Actor) error {
	return func(act *vocab.Actor) error {
		return i.writeActorIdentifier(s, act)
	}
}

func (i itemModel) writeItemCollection(s io.Writer) func(col *vocab.ItemCollection) error {
	return func(col *vocab.ItemCollection) error {
		for _, it := range col.Collection() {
			if err := i.writeItem(s, it); err != nil {
				//p.logFn("error: %s", err)
			}
		}
		return nil
	}
}

func (i itemModel) writeCollection(s io.Writer) func(col vocab.CollectionInterface) error {
	return func(col vocab.CollectionInterface) error {
		for _, it := range col.Collection() {
			if err := i.writeItem(s, it); err != nil {
				//p.logFn("error: %s", err)
			}
		}
		return nil
	}
}

func (i itemModel) writeNaturalLanguageValuesWithLabel(s io.Writer, l string, values vocab.NaturalLanguageValues) error {
	ll := len(values)
	if ll == 0 {
		return nil
	}
	if ll == 1 {
		fmt.Fprintf(s, "%s: %s\n", l, values[0])
		return nil
	}
	vals := make([]string, len(values))
	for i, v := range values {
		if v.Ref == "" || v.Ref == vocab.NilLangRef {
			vals[i] = fmt.Sprintf("%s", v.Value)
		} else {
			vals[i] = fmt.Sprintf("[%s]%s", v.Ref, v.Value)
		}
	}
	if ll > 1 {
		fmt.Fprintf(s, "%s: [ %s ]\n", l, strings.Join(vals, ", "))
	}
	return nil
}

func (i itemModel) writeActorIdentifier(s io.Writer, it vocab.Item) error {
	if c, ok := it.(vocab.ItemCollection); ok && len(c) == 0 {
		return nil
	}
	return vocab.OnActor(it, func(act *vocab.Actor) error {
		if act.ID.Equals(vocab.PublicNS, true) {
			return nil
		}
		return vocab.OnObject(act, i.writeObject(s))
	})
}

func (i itemModel) writeItem(s io.Writer, it vocab.Item) error {
	if it == nil {
		return nil
	}

	if vocab.IsIRI(it) {
		fmt.Fprintf(s, "%s\n", it.GetLink())
		return nil
	}

	if vocab.IsItemCollection(it) {
		return vocab.OnItemCollection(it, i.writeItemCollection(s))
	}
	typ := it.GetType()
	if vocab.IntransitiveActivityTypes.Contains(typ) {
		return vocab.OnIntransitiveActivity(it, i.writeIntransitiveActivity(s))
	}
	if vocab.ActivityTypes.Contains(typ) {
		return vocab.OnActivity(it, i.writeActivity(s))
	}
	if vocab.ActorTypes.Contains(typ) {
		return vocab.OnActor(it, i.writeActor(s))
	}
	if vocab.ObjectTypes.Contains(typ) || typ == "" {
		return vocab.OnObject(it, i.writeObject(s))
	}
	return fmt.Errorf("unknown activitypub object of type %T", it)
	p.viewport.SetContent(s.Render(p.model.View()))
	return p.viewport.View()
}

func (i *itemModel) updateIntransitiveActivity(a *vocab.IntransitiveActivity) error {
func (p *pagerModel) updateIntransitiveActivity(a *vocab.IntransitiveActivity) error {
	// TODO(marius): IntransitiveActivity stuff
	return nil
}

func (i *itemModel) updateActivity(a *vocab.Activity) error {
	if err := vocab.OnIntransitiveActivity(a, i.updateIntransitiveActivity); err != nil {
func (p *pagerModel) updateActivity(a *vocab.Activity) error {
	if err := vocab.OnIntransitiveActivity(a, p.updateIntransitiveActivity); err != nil {
		return err
	}
	// TODO(marius): Activity stuff
	return nil
}

func (i *itemModel) updateActor(a *vocab.Actor) error {
func (p *pagerModel) updateActor(a *vocab.Actor) error {
	return nil
}

func (i *itemModel) updateItems(items *vocab.ItemCollection) error {
func (p *pagerModel) updateObject(o *vocab.Object) error {
	return nil
}
func (p *pagerModel) updateItems(items *vocab.ItemCollection) error {
	return nil
}

func (i *itemModel) updateModel(it vocab.Item) error {
func (p *pagerModel) updateModel(it vocab.Item) error {
	if it == nil {
		return nil
	}

	if vocab.IsItemCollection(it) {
		return vocab.OnItemCollection(it, i.updateItems)
		return vocab.OnItemCollection(it, p.updateItems)
	}
	typ := it.GetType()
	if vocab.IntransitiveActivityTypes.Contains(typ) {
		return vocab.OnIntransitiveActivity(it, i.updateIntransitiveActivity)
		return vocab.OnIntransitiveActivity(it, p.updateIntransitiveActivity)
	}
	if vocab.ActivityTypes.Contains(typ) {
		return vocab.OnActivity(it, i.updateActivity)
		return vocab.OnActivity(it, p.updateActivity)
	}
	if vocab.ActorTypes.Contains(typ) {
		return vocab.OnActor(it, i.updateActor)
		return vocab.OnActor(it, p.updateActor)
	}
	if vocab.ObjectTypes.Contains(typ) || typ == "" {
		return vocab.OnObject(it, p.updateObject)
	}
	//if vocab.ObjectTypes.Contains(typ) || typ == "" {
	//	return vocab.OnObject(it, i.updateObject)
	//}
	return fmt.Errorf("unknown activitypub object of type %T", it)
}

func newItemModel(common *commonModel) itemModel {
func newItemModel(common *commonModel) pagerModel {
	// Init viewport
	vp := viewport.New(0, 0)
	vp.YPosition = 0

	return itemModel{
	return pagerModel{
		commonModel: common,
		viewport:    vp,
		model:       newObjectModel(),
		model:       M,
	}
}
func (i itemModel) Init() tea.Cmd {
	i.logFn("Item View init")
func (p pagerModel) Init() tea.Cmd {
	p.logFn("Item View init")
	return noop
}

func (i itemModel) updateAsModel(msg tea.Msg) (itemModel, tea.Cmd) {
func (p pagerModel) updateAsModel(msg tea.Msg) (pagerModel, tea.Cmd) {
	cmds := make([]tea.Cmd, 0)
	switch mm := msg.(type) {
	case tea.WindowSizeMsg:
		i.logFn("item resize: %+v", msg)
		p.logFn("item resize: %+v", msg)
	case nodeUpdateMsg:
		var content tea.Model = M
		if mm.n != nil {
			i.item = mm.n.Item
			if !(vocab.IsIRI(i.item) || vocab.IsItemCollection(i.item)) {
				ob := newObjectModel()
				if err := vocab.OnObject(i.item, ob.updateObject); err != nil {
					cmds = append(cmds, errCmd(err))
				}
				content = ob
		p.item = mm.Item
		if !(vocab.IsIRI(p.item) || vocab.IsItemCollection(p.item)) {
			ob := newObjectModel()
			if err := vocab.OnObject(p.item, ob.updateObject); err != nil {
				cmds = append(cmds, errCmd(err))
			}
			content = ob
		}
		i.model = content
		p.model = content
	case tea.KeyMsg:
		switch mm.String() {
		case "home", "g":
			i.viewport.GotoTop()
			if i.viewport.HighPerformanceRendering {
				cmds = append(cmds, viewport.Sync(i.viewport))
			p.viewport.GotoTop()
			if p.viewport.HighPerformanceRendering {
				cmds = append(cmds, viewport.Sync(p.viewport))
			}
		case "end", "G":
			i.viewport.GotoBottom()
			if i.viewport.HighPerformanceRendering {
				cmds = append(cmds, viewport.Sync(i.viewport))
			p.viewport.GotoBottom()
			if p.viewport.HighPerformanceRendering {
				cmds = append(cmds, viewport.Sync(p.viewport))
			}
		}
	}

	return i, tea.Batch(cmds...)
	return p, tea.Batch(cmds...)
}

func (i itemModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	return i.updateAsModel(msg)
func (p pagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	return p.updateAsModel(msg)
}

func ItemType(o vocab.Item) string {

M statusbar.go => statusbar.go +4 -19
@@ 127,9 127,7 @@ func (s *statusModel) spin(msg tea.Msg) tea.Cmd {
	return tick
}

type statusNode struct {
	*n
}
type statusNode n

func (a statusNode) View() string {
	s := strings.Builder{}


@@ 137,7 135,7 @@ func (a statusNode) View() string {
	case pub.IRI, *pub.IRI:
		fmt.Fprintf(&s, "%s", it.GetID())
	case pub.ItemCollection:
		fmt.Fprintf(&s, "%s %s: %d items", ItemType(it), a.n.n, len(a.n.c))
		fmt.Fprintf(&s, "%s %s: %d items", ItemType(it), a.n, len(a.c))
	case pub.Item:
		fmt.Fprintf(&s, "%s: %s", ItemType(it), it.GetID())
	}


@@ 155,16 153,11 @@ func (s *statusModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
			cmd = s.spin(msg)
		}
	case nodeUpdateMsg:
		if mm.n != nil {
			cmd = s.showStatusMessage(statusNode{mm.n}.View())
		}
		cmd = s.showStatusMessage(statusNode(mm).View())
	case statusState:
		//if !msg.Is(statusError) && s.state.Is(statusError) {
		//	s.state ^= statusError
		//}
		s.state |= mm
		if !s.state.Is(statusBusy) {
			s.logFn("stopping spinner")
			s.logFn("resetting spinner")
			s.spinner = initializeSpinner()
		}
	case percentageMsg:


@@ 174,14 167,6 @@ func (s *statusModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	return s, cmd
}

func (s *statusModel) startedLoading() tea.Msg {
	return s.state | statusBusy
}

func (s *statusModel) stoppedLoading() tea.Msg {
	return s.state ^ statusBusy
}

func (s *statusModel) View() string {
	b := strings.Builder{}


M tree.go => tree.go +29 -7
@@ 7,7 7,9 @@ import (

type treeModel struct {
	*commonModel
	list *tree.Model

	state state
	list  *tree.Model
}

func newTreeModel(common *commonModel, t tree.Nodes) treeModel {


@@ 36,6 38,11 @@ func percentageChanged(f float64) func() tea.Msg {
}

func (t *treeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch mm := msg.(type) {
	case state:
		t.state = mm
	}

	if m, cmd := t.list.Update(msg); cmd != nil {
		t.list = m.(*tree.Model)
		return t, tea.Batch(cmd, percentageChanged(t.list.ScrollPercent()))


@@ 44,7 51,7 @@ func (t *treeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	return t, noop
}

const treeWidth = 32
const minTreeWidth = 32

func (t *treeModel) View() string {
	if t.list.Focused() {


@@ 89,10 96,25 @@ func (t *treeModel) Advance(current *n) *tree.Model {
}

func (t *treeModel) IsSyncing() bool {
	for _, n := range t.list.Children() {
		if n.State().Is(NodeSyncing) {
			return true
		}
	return t.state.Is(stateBusy)
}

type state uint8

func (s state) Is(st state) bool {
	return s&st == st
}

const stateBusy state = 1 << iota

func (t *treeModel) startedLoading() tea.Msg {
	t.state |= stateBusy
	return t.state
}

func (t *treeModel) stoppedLoading() tea.Msg {
	if t.state.Is(stateBusy) {
		t.state ^= stateBusy
	}
	return false
	return t.state
}

M ui.go => ui.go +30 -33
@@ 137,7 137,7 @@ type model struct {
	breadCrumbs         []*tree.Model

	tree   treeModel
	pager  itemModel
	pager  pagerModel
	status statusModel
}



@@ 162,7 162,7 @@ func (m *model) setSize(w, h int) {

	w = w - 2 - 2 // 1 for padding, 1 for border

	tw := max(treeWidth, int(0.28*float32(w)))
	tw := max(minTreeWidth, int(0.28*float32(w)))
	m.tree.setSize(tw-1-1, h)
	m.pager.setSize(w-tw-1-1, h)



@@ 178,24 178,24 @@ func (m *model) setSize(w, h int) {
	}
}

type nodeUpdateMsg struct {
	*n
}
type nodeUpdateMsg n

func nodeUpdateCmd(n *n) tea.Cmd {
func nodeUpdateCmd(n n) tea.Cmd {
	return func() tea.Msg {
		return nodeUpdateMsg{n}
		return nodeUpdateMsg(n)
	}
}

func (m *model) update(msg tea.Msg) tea.Cmd {
	cmds := make([]tea.Cmd, 0)

	m.logFn("update: %T: %v", msg, msg)
	switch mm := msg.(type) {
	case *n:
		if mm != nil {
			m.currentNodePosition = m.tree.list.Cursor()
			m.currentNode = mm
			m.tree.state |= stateBusy
			ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*300)
			defer cancel()
			cmd := m.loadDepsForNode(ctx, m.currentNode)


@@ 205,7 205,7 @@ func (m *model) update(msg tea.Msg) tea.Cmd {
					break
				}
			}
			cmds = append(cmds, nodeUpdateCmd(m.currentNode), cmd)
			cmds = append(cmds, nodeUpdateCmd(*m.currentNode), cmd)
		}
	case advanceMsg:
		cmds = append(cmds, m.Advance(mm))


@@ 224,7 224,7 @@ func (m *model) update(msg tea.Msg) tea.Cmd {
		case key.Matches(mm, helpKey):
			return tea.Batch(showHelpCmd(), resizeCmd(m.width, m.height))
		case key.Matches(mm, advanceKey):
			return advanceCmd(m.currentNode)
			return advanceCmd(*m.currentNode)
		case key.Matches(mm, backKey):
			return m.Back(mm)
		}


@@ 236,23 236,17 @@ func (m *model) update(msg tea.Msg) tea.Cmd {
		return tea.Quit
	}

	if cmd := m.updateTree(msg); cmd != nil {
		cmds = append(cmds, cmd)
		if m.tree.IsSyncing() {
			cmds = append(cmds, m.status.startedLoading)
		}
	}
	cmds = append(cmds, m.updateTree(msg))
	if !m.tree.IsSyncing() {
		cmds = append(cmds, nodeUpdateCmd(m.currentNode))
		cmds = append(cmds, m.updatePager(msg))
	}
	cmds = append(cmds, m.updatePager(msg))
	cmds = append(cmds, m.updateStatusBar(msg))
	return tea.Batch(cmds...)
}

func (m *model) updatePager(msg tea.Msg) tea.Cmd {
	p, cmd := m.pager.Update(msg)
	if pp, ok := p.(itemModel); ok {
	if pp, ok := p.(pagerModel); ok {
		m.pager = pp
	} else {
		return errCmd(fmt.Errorf("invalid pager: %T", p))


@@ 311,9 305,9 @@ func (m *model) Back(msg tea.Msg) tea.Cmd {

var noop tea.Cmd = nil

func advanceCmd(n *n) tea.Cmd {
func advanceCmd(n n) tea.Cmd {
	return func() tea.Msg {
		return advanceMsg{n}
		return advanceMsg(n)
	}
}



@@ 336,17 330,15 @@ func (m *model) Advance(msg advanceMsg) tea.Cmd {
		return noop
	}

	if msg.n == nil {
		m.logFn("invalid node to advance to")
		return errCmd(fmt.Errorf("trying to advance to an invalid node"))
	nn := n(msg)
	if msg.s.Is(NodeError) {
		return errCmd(fmt.Errorf("error: %s", nn.n))
	}
	if msg.n.s.Is(NodeError) {
		return errCmd(fmt.Errorf("error: %s", msg.n.n))
	}
	name := getRootNodeName(msg.n)
	newNode := node(msg.Item, withParent(msg.n), withName(name))

	name := getRootNodeName(&nn)
	newNode := node(msg.Item, withParent(&nn), withName(name))
	if err := m.loadChildrenForNode(context.Background(), newNode); err != nil {
		return errCmd(fmt.Errorf("unable to advance to %q: %w", msg.n.n, err))
		return errCmd(fmt.Errorf("unable to advance to %q: %w", nn.n, err))
	}
	if newNode.s.Is(tree.NodeCollapsible) && len(newNode.c) == 0 {
		return errCmd(fmt.Errorf("no items in collection %s", name))


@@ 363,6 355,9 @@ func errCmd(err error) tea.Cmd {
}

func nodeCmd(node *n) tea.Cmd {
	if node == nil {
		return noop
	}
	return func() tea.Msg {
		return node
	}


@@ 384,9 379,7 @@ func quitCmd() tea.Msg {
	return quitMsg{}
}

type advanceMsg struct {
	*n
}
type advanceMsg n

func renderWithBorder(s string, focused bool) string {
	borderColour := hintColor


@@ 411,6 404,10 @@ func (m *model) View() string {
	)
}

func (m *model) IsBusy() bool {
	return m.status.state.Is(statusBusy)
}

// ColorPair is a pair of colors, one intended for a dark background and the
// other intended for a light background. We'll automatically determine which
// of these colors to use.


@@ 467,4 464,4 @@ func (m motelyPager) View() string {
	return tit.String()
}

var M = motelyPager{Title: "Motely"}
var M = motelyPager{Title: "Motley"}