Add mesh view to Feed
ci / test (push) Successful in 36s
ci / release (push) Successful in 1m27s

This commit is contained in:
2026-07-03 06:40:15 -04:00
parent 4d6470d695
commit 38b2abd71b
7 changed files with 101 additions and 20 deletions
+6
View File
@@ -261,5 +261,11 @@ func listIndexForTab(m Model, y int, ly viewLayout) (int, bool) {
adj.MainInnerH = ly.ListH
return listIndexAt(y, adj, m.cursor, m.listLen())
}
if m.tab == TabFeed && m.feedMeshVisible && ly.ListH > 0 {
adj := ly
adj.MainInnerTop = ly.ListTop
adj.MainInnerH = ly.ListH
return listIndexAt(y, adj, m.cursor, m.listLen())
}
return listIndexAt(y, ly, m.cursor, m.listLen())
}
+10 -1
View File
@@ -252,7 +252,7 @@ func writeWrappedField(b *strings.Builder, label, value string, width, labelWidt
}
}
func renderFooter(t Theme, filtering, searching bool, filter string, follow bool, detailOpen bool, mouseEnabled bool, historyNote string) string {
func renderFooter(t Theme, filtering, searching bool, filter string, follow bool, detailOpen bool, mouseEnabled bool, tab ViewTab, feedMeshVisible bool, historyNote string) string {
var hints []string
if searching {
hints = []string{"type id…", "Enter lookup", "Esc cancel"}
@@ -266,6 +266,9 @@ func renderFooter(t Theme, filtering, searching bool, filter string, follow bool
} else {
hints = []string{"Tab view", "j/k nav", "Enter detail", "s search", "/ filter", "? help", "q quit"}
}
if tab == TabFeed {
hints = append(hints, "v mesh")
}
hints = append(hints, "m mouse")
if filtering {
hints = []string{"type filter…", "Enter apply", "Esc cancel"}
@@ -280,6 +283,9 @@ func renderFooter(t Theme, filtering, searching bool, filter string, follow bool
if mouseEnabled {
line += " " + lipgloss.NewStyle().Foreground(t.Teal).Render("[mouse]")
}
if tab == TabFeed && feedMeshVisible {
line += " " + lipgloss.NewStyle().Foreground(t.Teal).Render("[mesh]")
}
if historyNote != "" {
line += " " + lipgloss.NewStyle().Foreground(t.Muted).Render("[" + historyNote + "]")
}
@@ -297,6 +303,9 @@ func renderHelp(t Theme) string {
f Toggle follow mode (auto-scroll to newest)
PgUp Load older attacks (Feed) / moderation (Incidents) / more posts (Blog)
Feed tab
v Toggle peer mesh animation above the feed
Blog tab
Enter Read selected briefing
Esc Back to post list (while reading)
+10
View File
@@ -176,6 +176,16 @@ func renderFeedList(t Theme, items []viz.FeedItem, cursor int, store *viz.Store,
return strings.Join(lines, "\n")
}
func renderFeedSplitView(t Theme, meshStr string, items []viz.FeedItem, cursor int, store *viz.Store, height, width int, operator bool) string {
_, feedH := feedPanelHeights(height)
feedStr := renderFeedList(t, items, cursor, store, feedH, width, operator)
var parts []string
parts = append(parts, feedStr)
parts = append(parts, t.MutedStyle().Render(strings.Repeat("─", min(width, 40))))
parts = append(parts, meshStr)
return padToLines(strings.Join(parts, "\n"), height)
}
func renderIncidentRow(t Theme, inc viz.Incident, selected bool, width int) string {
headline := viz.IncidentHeadline(inc)
kind := string(viz.IncidentActionKindOf(inc))
+7 -1
View File
@@ -48,7 +48,7 @@ func (m Model) chromeBottom() string {
if m.statusErr != "" {
parts = append(parts, renderStatusErr(m.theme, m.statusErr))
}
parts = append(parts, renderFooter(m.theme, m.filtering, m.searching, m.filterQuery, m.follow, m.detailOpen, m.mouseEnabled, m.historyFooterNote()))
parts = append(parts, renderFooter(m.theme, m.filtering, m.searching, m.filterQuery, m.follow, m.detailOpen, m.mouseEnabled, m.tab, m.feedMeshVisible, m.historyFooterNote()))
if m.filtering {
parts = append(parts, m.filterInput.View())
}
@@ -86,6 +86,12 @@ func (m Model) viewLayout() viewLayout {
ly.MeshH = meshH
ly.ListTop = ly.MeshTop + meshH + 1
ly.ListH = listH
} else if m.tab == TabFeed && m.feedMeshVisible {
meshH, feedH := feedPanelHeights(mainH)
ly.ListTop = ly.MainInnerTop + 1
ly.ListH = feedH
ly.MeshTop = ly.ListTop + feedH + 1
ly.MeshH = meshH
}
return ly
}
+22 -2
View File
@@ -62,6 +62,7 @@ const blogPageSize = 20
type keyMap struct {
Up, Down, Quit, Help, Tab, Follow, Filter, Search, Enter, Reconnect, Close, Mouse key.Binding
PgUp, PgDn key.Binding
MeshView key.Binding
}
func defaultKeyMap() keyMap {
@@ -80,6 +81,7 @@ func defaultKeyMap() keyMap {
Reconnect: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "reconnect")),
PgUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("pgup", "history")),
PgDn: key.NewBinding(key.WithKeys("pgdn"), key.WithHelp("pgdn", "down")),
MeshView: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "mesh")),
}
}
@@ -143,6 +145,7 @@ type Model struct {
peerMesh *mesh.State
meshTickActive bool
feedMeshVisible bool
}
func NewModel(cfg config.Config) Model {
@@ -309,6 +312,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.follow {
m.cursor = 0
}
case key.Matches(msg, m.keys.MeshView):
if m.tab == TabFeed {
m.feedMeshVisible = !m.feedMeshVisible
if m.feedMeshVisible {
m.syncMeshFromNetwork()
if m.peerMesh != nil && m.peerMesh.HasPulses() {
cmds = append(cmds, m.startMeshTick())
}
} else {
m.meshTickActive = false
}
}
case key.Matches(msg, m.keys.Filter):
m.cancelSearch()
m.filtering = true
@@ -363,7 +378,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.detailOpen {
m.refreshDetailViewport()
}
if m.tab == TabPeers && m.peerMesh != nil && m.peerMesh.HasPulses() {
if m.meshShouldAnimate() && m.peerMesh != nil && m.peerMesh.HasPulses() {
cmds = append(cmds, m.startMeshTick())
}
@@ -503,7 +518,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Tick(500*time.Millisecond, func(time.Time) tea.Msg { return flashTickMsg{} })
case meshTickMsg:
if m.tab == TabPeers && m.peerMesh != nil {
if m.meshShouldAnimate() && m.peerMesh != nil {
m.peerMesh.Tick(mesh.FrameMs)
if m.peerMesh.HasPulses() {
return m, m.meshTickCmd()
@@ -774,6 +789,11 @@ func (m Model) renderMain(height, width int) string {
listW := listContentWidth(width)
switch m.tab {
case TabFeed:
if m.feedMeshVisible {
meshH, _ := feedPanelHeights(height)
meshStr := m.renderPeerMesh(meshH, listW)
return renderFeedSplitView(m.theme, meshStr, m.visibleFeed(), m.cursor, m.store, height, listW, m.cfg.Operator)
}
return renderFeedList(m.theme, m.visibleFeed(), m.cursor, m.store, height, listW, m.cfg.Operator)
case TabIncidents:
return renderIncidentList(m.theme, m.visibleIncidents(), m.cursor, height, listW, m.cfg.Operator)
+10 -5
View File
@@ -138,17 +138,22 @@ func (m *Model) handleMouseClick(ev tea.MouseEvent, ly viewLayout) []tea.Cmd {
return cmds
}
if m.tab == TabPeers && ly.MeshH > 0 && ev.Y >= ly.MeshTop && ev.Y < ly.MeshTop+ly.MeshH {
if ly.MeshH > 0 && ev.Y >= ly.MeshTop && ev.Y < ly.MeshTop+ly.MeshH &&
(m.tab == TabPeers || (m.tab == TabFeed && m.feedMeshVisible)) {
cellX := ev.X - 2
if cellX < 0 {
cellX = 0
}
cellY := ev.Y - ly.MeshTop
if id, ok := m.peerMesh.HitTest(cellX, cellY); ok {
if idx := m.peerMesh.PeerIndex(id, m.peerRoster()); idx >= 0 {
m.cursor = idx
m.follow = false
m.syncMeshSelection()
if m.tab == TabPeers {
if idx := m.peerMesh.PeerIndex(id, m.peerRoster()); idx >= 0 {
m.cursor = idx
m.follow = false
m.syncMeshSelection()
}
} else {
m.peerMesh.SetSelected(id)
}
}
return cmds
+36 -11
View File
@@ -22,16 +22,24 @@ func meshPalette(t Theme) mesh.Palette {
}
func peerPanelHeights(totalH int) (meshH, listH int) {
meshH = totalH * 65 / 100
if meshH < 8 {
meshH = 8
return splitPanelHeights(totalH, 65, 8, 3)
}
func feedPanelHeights(totalH int) (meshH, feedH int) {
return splitPanelHeights(totalH, 40, 6, 4)
}
func splitPanelHeights(totalH, meshPercent, minMesh, minList int) (meshH, listH int) {
meshH = totalH * meshPercent / 100
if meshH < minMesh {
meshH = minMesh
}
if meshH > totalH-4 {
meshH = totalH - 4
if meshH > totalH-minList-1 {
meshH = totalH - minList - 1
}
listH = totalH - meshH - 1
if listH < 3 {
listH = 3
if listH < minList {
listH = minList
}
return meshH, listH
}
@@ -169,18 +177,35 @@ func (m *Model) startMeshTick() tea.Cmd {
}
func (m *Model) onTabChanged() tea.Cmd {
if m.tab == TabPeers {
switch m.tab {
case TabPeers:
m.syncMeshFromNetwork()
m.syncMeshSelection()
if m.peerMesh != nil && m.peerMesh.HasPulses() {
return m.startMeshTick()
case TabFeed:
if m.feedMeshVisible {
m.syncMeshFromNetwork()
}
default:
m.meshTickActive = false
return nil
}
m.meshTickActive = false
if m.peerMesh != nil && m.peerMesh.HasPulses() && m.meshShouldAnimate() {
return m.startMeshTick()
}
return nil
}
func (m Model) meshShouldAnimate() bool {
switch m.tab {
case TabPeers:
return true
case TabFeed:
return m.feedMeshVisible
default:
return false
}
}
func (m *Model) meshPalette() mesh.Palette {
return meshPalette(m.theme)
}