Skip to content

Commit dfed3e6

Browse files
gitcoder89431claude
andcommitted
feat: replace sync status in header with session ↑/↓ transfer stats
Snapshots Syncthing's total inBytesTotal/outBytesTotal at startup, polls every 5s, and shows the session delta as: ↑ 1.2 MB ↓ 3.4 MB Replaces the "Up to Date" / "⠹ Syncing 73%" text which conveyed redundant info already visible in the file list badges. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5f40ec7 commit dfed3e6

5 files changed

Lines changed: 80 additions & 6 deletions

File tree

internal/app/app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type Model struct {
5353
lastActivityState syncthing.FolderState
5454
activitySeeded bool
5555
currentSyncItem string // file currently being transferred, shown in header
56+
transferIn int64 // session bytes received
57+
transferOut int64 // session bytes sent
5658
}
5759

5860
func New(meta BuildInfo, client *syncthing.Client, syncthingURL string) Model {
@@ -95,6 +97,10 @@ func (m Model) Init() tea.Cmd {
9597
}
9698
return deviceIDMsg{id: dev.ID, name: dev.Name}
9799
},
100+
func() tea.Msg {
101+
client.InitTransferStats()
102+
return transferStatsMsg{}
103+
},
98104
}
99105
for _, screen := range m.screens {
100106
cmds = append(cmds, screen.Init())

internal/app/update.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"github.com/gitcoder89431/meshd/internal/theme"
1515
)
1616

17+
type transferStatsMsg struct{ in, out int64 }
18+
1719
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1820
switch msg := msg.(type) {
1921
case tea.WindowSizeMsg:
@@ -40,6 +42,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4042
m.deviceID = msg.id
4143
m.deviceName = msg.name
4244
return m, nil
45+
case transferStatsMsg:
46+
m.transferIn = msg.in
47+
m.transferOut = msg.out
48+
client := m.client
49+
return m, tea.Tick(5*time.Second, func(time.Time) tea.Msg {
50+
in, out, _ := client.GetTransferStats()
51+
return transferStatsMsg{in: in, out: out}
52+
})
4353
case spinnerTickMsg:
4454
m.spinnerFrame = (m.spinnerFrame + 1) % len(spinnerFrames)
4555
if m.spinnerActive {

internal/app/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (m Model) View() tea.View {
2323
dims := layout.Calculate(m.width, m.height, m.showSidebar)
2424
active := m.screens[m.activeScreen]
2525

26-
head := clipHeight(header.View(header.Model{AppName: config.AppName, DeviceID: m.deviceID, DeviceName: m.deviceName, SyncStatus: m.syncStatus}, dims.Header.Width, dims.Header.Height, m.theme), dims.Header.Height)
26+
head := clipHeight(header.View(header.Model{AppName: config.AppName, DeviceID: m.deviceID, DeviceName: m.deviceName, TransferIn: m.transferIn, TransferOut: m.transferOut}, dims.Header.Width, dims.Header.Height, m.theme), dims.Header.Height)
2727

2828
footBindings := m.keys.ShortHelp()
2929
type footerBinder interface{ FooterBindings() []key.Binding }

internal/components/header/header.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
package header
22

33
import (
4+
"fmt"
45
"strings"
56

67
"charm.land/lipgloss/v2"
78
"github.com/gitcoder89431/meshd/internal/theme"
89
)
910

1011
type Model struct {
11-
AppName string
12-
DeviceID string // full device ID (fallback if no name)
13-
DeviceName string // human-readable device name (preferred)
14-
SyncStatus string // pre-rendered sync status shown on the right
12+
AppName string
13+
DeviceID string // full device ID (fallback if no name)
14+
DeviceName string // human-readable device name (preferred)
15+
TransferIn int64 // session bytes received
16+
TransferOut int64 // session bytes sent
1517
}
1618

1719
func View(m Model, width, height int, t theme.Theme) string {
@@ -30,9 +32,24 @@ func View(m Model, width, height int, t theme.Theme) string {
3032
if label != "" {
3133
left += t.Border.Render(" | ") + t.Muted.Render(label)
3234
}
33-
right := m.SyncStatus
35+
right := t.Muted.Render("↑ "+formatBytes(m.TransferOut)) +
36+
t.Border.Render(" ") +
37+
t.Muted.Render("↓ "+formatBytes(m.TransferIn))
3438
rightW := lipgloss.Width(right)
3539
gap := max(0, innerWidth-lipgloss.Width(left)-rightW)
3640
content := left + lipgloss.NewStyle().Width(gap).Render("") + right
3741
return t.Header.Width(width).Height(height).Render(content)
3842
}
43+
44+
func formatBytes(b int64) string {
45+
switch {
46+
case b >= 1<<30:
47+
return fmt.Sprintf("%.1f GB", float64(b)/(1<<30))
48+
case b >= 1<<20:
49+
return fmt.Sprintf("%.1f MB", float64(b)/(1<<20))
50+
case b >= 1<<10:
51+
return fmt.Sprintf("%.0f KB", float64(b)/(1<<10))
52+
default:
53+
return fmt.Sprintf("%d B", b)
54+
}
55+
}

internal/syncthing/client.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ type Client struct {
2626
longPollHTTP *http.Client // separate client for 60s long-polls (#8)
2727
folderPath string // cached after first EnsureMeshdFolder (#14)
2828
initEventID int // event ID at startup, used by GetRecentEvents (#10)
29+
baselineIn int64 // inBytesTotal at session start
30+
baselineOut int64 // outBytesTotal at session start
2931
}
3032

3133
func New(baseURL, apiKey string) *Client {
@@ -583,6 +585,45 @@ func (c *Client) InitEventID() {
583585
}
584586
}
585587

588+
// InitTransferStats snapshots current connection totals so GetTransferStats
589+
// can return session-relative deltas.
590+
func (c *Client) InitTransferStats() {
591+
in, out, err := c.connectionTotals()
592+
if err == nil {
593+
c.baselineIn = in
594+
c.baselineOut = out
595+
}
596+
}
597+
598+
// GetTransferStats returns bytes received and sent since InitTransferStats was called.
599+
func (c *Client) GetTransferStats() (in, out int64, err error) {
600+
curIn, curOut, err := c.connectionTotals()
601+
if err != nil {
602+
return 0, 0, err
603+
}
604+
return max64(0, curIn-c.baselineIn), max64(0, curOut-c.baselineOut), nil
605+
}
606+
607+
func (c *Client) connectionTotals() (in, out int64, err error) {
608+
var result struct {
609+
Total struct {
610+
InBytesTotal int64 `json:"inBytesTotal"`
611+
OutBytesTotal int64 `json:"outBytesTotal"`
612+
} `json:"total"`
613+
}
614+
if err := c.get("/rest/system/connections", &result); err != nil {
615+
return 0, 0, err
616+
}
617+
return result.Total.InBytesTotal, result.Total.OutBytesTotal, nil
618+
}
619+
620+
func max64(a, b int64) int64 {
621+
if a > b {
622+
return a
623+
}
624+
return b
625+
}
626+
586627
// GetRecentEvents returns Syncthing events since this session started. (#10)
587628
func (c *Client) GetRecentEvents(limit int) ([]SyncEvent, error) {
588629
// use initEventID so we only see events from this session, not all history

0 commit comments

Comments
 (0)