Skip to content

Commit cfd8e67

Browse files
authored
Merge pull request #3 from merlinfuchs/multi-page
Support multiple pages with different layouts
2 parents c36bff9 + 750fd89 commit cfd8e67

32 files changed

Lines changed: 294 additions & 146 deletions

README.md

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,47 +23,82 @@ blimp
2323

2424
## Configuration
2525

26-
The app will look for a configuration file called `blimp.toml`. Here is an example configuration for the example above:
26+
The app will look for a configuration file called `blimp.toml`. Blimp comes with pretty good defaults but you can adjust everything how you want.
2727

28-
```toml
29-
# The layout is based on a grid, you can add rows and columns or remove some widgets
30-
# On smaller screens you probably don't want to cramp everything in, so remove the views that you don't need
31-
layout = [
32-
["weather", "weather"],
33-
["weather", "weather"],
34-
["feeds", "feeds"],
35-
["latency", "status"]
36-
]
28+
### Configure the widgets
3729

38-
[[views.status.targets]]
30+
```toml
31+
[[widgets.status.targets]]
3932
name = "Xenon Bot"
4033
type = "https"
4134
host = "xenon.bot"
4235

43-
[[views.status.targets]]
36+
[[widgets.status.targets]]
4437
name = "Embed Generator"
4538
type = "https"
4639
host = "message.style"
4740

48-
[[views.status.targets]]
41+
[[widgets.status.targets]]
4942
name = "Friendly Captcha API"
5043
type = "https"
5144
host = "eu-api.friendlycaptcha.eu"
5245

53-
[[views.status.targets]]
46+
[[widgets.status.targets]]
5447
name = "Google DNS"
5548
type = "ping"
5649
host = "8.8.8.8"
5750

58-
[views.weather]
51+
[widgets.weather]
5952
# You openweathermap.org API key
6053
owm_api_key = ""
6154
# The latitude and longitude of the weather location
6255
owm_lat = 51.33
6356
owm_lon = 12.37
6457

65-
[[views.feeds.targets]]
58+
[[widgets.feeds.targets]]
6659
url = "https://hnrss.org/newest"
6760
```
6861

62+
## Configure the layout
63+
64+
Blimp supports multiple pages that it switches between on an interval. Each page can have a distinct grid layout with different widgets. If you don't want blimp to switch between pages, only define one page.
65+
66+
```toml
67+
# How fast do you want blimp to switch between pages
68+
page_interval = 30000 # 30 seconds is the default
69+
70+
# Your first page
71+
[[pages]]
72+
layout = [
73+
["weather", "weather"],
74+
["weather", "weather"],
75+
["feeds", "feeds"],
76+
["latency", "status"]
77+
]
78+
79+
# Your second page
80+
[[pages]]
81+
layout = [
82+
["feeds", "feeds"],
83+
["feeds", "feeds"],
84+
["feeds", "feeds"],
85+
["latency", "status"]
86+
]
87+
88+
# Your third page
89+
[[pages]]
90+
...
91+
```
92+
6993
Look at the [default config](internal/config/default.config.toml) for other values you can override.
94+
95+
## Logging
96+
97+
Because logging to STDOUT is pointless because STDOUT is already used for the UI output, you can find log messages in a file called `blimp.log`.
98+
99+
If you want blimp to write to a different logfile you can set it in the config like this:
100+
101+
```toml
102+
[logging]
103+
filename = "blimp.log"
104+
```

internal/app.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/gdamore/tcell/v2"
8+
"github.com/merlinfuchs/blimp/internal/config"
9+
"github.com/merlinfuchs/blimp/internal/widgets/feeds"
10+
"github.com/merlinfuchs/blimp/internal/widgets/latency"
11+
"github.com/merlinfuchs/blimp/internal/widgets/status"
12+
"github.com/merlinfuchs/blimp/internal/widgets/weather"
13+
"github.com/rivo/tview"
14+
)
15+
16+
func AppEntry() error {
17+
app := tview.NewApplication()
18+
19+
widgets := map[string]Widget{
20+
"latency": latency.New(),
21+
"status": status.New(),
22+
"weather": weather.New(),
23+
"feeds": feeds.New(),
24+
}
25+
26+
pages, err := parsePagesFromConfig()
27+
if err != nil {
28+
return fmt.Errorf("failed to parse pages: %w", err)
29+
}
30+
31+
if len(pages) == 0 {
32+
return fmt.Errorf("no pages defined")
33+
}
34+
35+
pagesView, err := constructPages(pages, widgets)
36+
if err != nil {
37+
return fmt.Errorf("failed to construct pages: %w", err)
38+
}
39+
currentPage := 0
40+
41+
frame := tview.NewFrame(pagesView).
42+
SetBorders(2, 2, 1, 0, 4, 4)
43+
44+
go func() {
45+
for {
46+
frame.Clear()
47+
48+
now := time.Now()
49+
frame.
50+
AddText("Blimp v0.1.0", true, tview.AlignLeft, tcell.ColorDimGray).
51+
AddText(now.Format("15:04:05"), true, tview.AlignCenter, tcell.ColorLightGray).
52+
AddText(now.Format("Monday, January 2, 2006"), true, tview.AlignCenter, tcell.ColorDimGray).
53+
AddText(pages[currentPage].Title, true, tview.AlignRight, tcell.ColorDimGray)
54+
55+
app.QueueUpdateDraw(func() {
56+
for _, widget := range widgets {
57+
widget.Update()
58+
}
59+
})
60+
61+
<-time.After(time.Duration(config.K.Int("update_interval")) * time.Millisecond)
62+
}
63+
}()
64+
65+
go func() {
66+
for {
67+
<-time.After(time.Duration(config.K.Int("page_interval")) * time.Millisecond)
68+
currentPage = (currentPage + 1) % len(pages)
69+
pagesView.SwitchToPage(fmt.Sprintf("%d", currentPage))
70+
}
71+
}()
72+
73+
defer func() {
74+
for _, widget := range widgets {
75+
widget.Stop()
76+
}
77+
}()
78+
79+
if err := app.SetRoot(frame, true).EnableMouse(false).Run(); err != nil {
80+
return fmt.Errorf("failed to run app: %w", err)
81+
}
82+
83+
return nil
84+
}
85+
86+
func constructPages(pages []Page, widgets map[string]Widget) (*tview.Pages, error) {
87+
view := tview.NewPages()
88+
89+
usedWidgets := make(map[string]bool, len(widgets))
90+
91+
for i, page := range pages {
92+
if len(page.Layout) == 0 {
93+
return nil, fmt.Errorf("page #%d (%s) has no layout", i, page.Title)
94+
}
95+
96+
rowValues := make([]int, len(page.Layout))
97+
for i := 0; i < len(rowValues); i++ {
98+
rowValues[i] = -1
99+
}
100+
101+
colValues := make([]int, len(page.Layout[0]))
102+
for i := 0; i < len(colValues); i++ {
103+
colValues[i] = -1
104+
}
105+
106+
grid := tview.NewGrid().
107+
SetGap(1, 2).
108+
SetRows(rowValues...).
109+
SetColumns(colValues...)
110+
111+
for widgetName, widget := range widgets {
112+
found := false
113+
114+
minRow := -1
115+
maxRow := -1
116+
minCol := -1
117+
maxCol := -1
118+
119+
for r, cols := range page.Layout {
120+
for c, name := range cols {
121+
if name == widgetName {
122+
found = true
123+
maxRow = r
124+
maxCol = c
125+
if minRow == -1 {
126+
minRow = r
127+
}
128+
if minCol == -1 {
129+
minCol = c
130+
}
131+
}
132+
}
133+
}
134+
135+
if found {
136+
rowSpan := maxRow - minRow + 1
137+
colSpan := maxCol - minCol + 1
138+
139+
usedWidgets[widgetName] = true
140+
grid.AddItem(widget.Primitive(), minRow, minCol, rowSpan, colSpan, 0, 0, false)
141+
}
142+
}
143+
144+
view.AddPage(fmt.Sprintf("%d", i), grid, true, i == 0)
145+
}
146+
147+
for widgetName, widget := range widgets {
148+
if usedWidgets[widgetName] {
149+
widget.Start()
150+
}
151+
}
152+
153+
return view, nil
154+
}
Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
1-
update_interval = 250
1+
update_interval = 250 # 250 ms
2+
page_interval = 30000 # 30 seconds
23

4+
[[pages]]
35
layout = [
46
["weather", "weather"],
57
["weather", "weather"],
68
["feeds", "feeds"],
79
["latency", "status"]
810
]
911

10-
[views.latency]
12+
[[pages]]
13+
layout = [
14+
["feeds", "feeds"],
15+
["feeds", "feeds"],
16+
["latency", "status"]
17+
]
18+
19+
[widgets.latency]
1120
update_interval = 500 # 0.5 seconds
1221
history_length = 25
1322
target_host = "www.google.com"
1423

15-
[views.status]
24+
[widgets.status]
1625
update_interval = 10000 # 10 seconds
1726

18-
[views.weather]
27+
[widgets.weather]
1928
update_interval = 600000 # 10 minutes
2029
owm_language = "en"
2130
owm_unit = "metric"
2231

23-
[views.feeds]
32+
[widgets.feeds]
2433
update_interval = 60000 # 1 minutes
2534
max_items = 0 # 0 means auto
2635
show_feed_title = true
27-
show_published_time = true
36+
show_published_time = true
37+
38+
[logging]
39+
filename = "blimp.log"
40+
max_size = 1 # in MB
41+
max_age = 7 # in days
42+
max_backups = 5 # // note: old ones will still be deleted after max_age

internal/page.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package internal
2+
3+
import "github.com/merlinfuchs/blimp/internal/config"
4+
5+
type Page struct {
6+
Title string `koanf:"title"`
7+
Layout [][]string `koanf:"layout"`
8+
}
9+
10+
func parsePagesFromConfig() ([]Page, error) {
11+
var pages []Page
12+
13+
if err := config.K.Unmarshal("pages", &pages); err != nil {
14+
return nil, err
15+
}
16+
17+
return pages, nil
18+
}

internal/view.go renamed to internal/widget.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package internal
22

33
import "github.com/rivo/tview"
44

5-
type View interface {
5+
type Widget interface {
66
Start()
77
Stop()
88
Update() error
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type FeedsView struct {
2323

2424
func New() *FeedsView {
2525
targets := make([]FeedTarget, 0)
26-
if err := config.K.Unmarshal("views.feeds.targets", &targets); err != nil {
26+
if err := config.K.Unmarshal("widgets.feeds.targets", &targets); err != nil {
2727
log.Panic().Err(err).Msgf("Failed to unmarshal status targets")
2828
}
2929

@@ -49,7 +49,7 @@ func (l *FeedsView) Start() {
4949
select {
5050
case <-l.stopped:
5151
return
52-
case <-time.After(time.Duration(config.K.Int("views.feeds.update_interval")) * time.Millisecond):
52+
case <-time.After(time.Duration(config.K.Int("widgets.feeds.update_interval")) * time.Millisecond):
5353
l.updateItems()
5454
}
5555
}
@@ -99,7 +99,7 @@ func (l *FeedsView) Update() error {
9999
l.view.Clear()
100100

101101
items := l.items
102-
maxItems := config.K.Int("views.feeds.max_items")
102+
maxItems := config.K.Int("widgets.feeds.max_items")
103103
if maxItems == 0 {
104104
_, _, _, height := l.view.GetRect()
105105
maxItems = height - 4
@@ -111,14 +111,14 @@ func (l *FeedsView) Update() error {
111111

112112
for _, item := range items {
113113
text := "[gray]-"
114-
if config.K.Bool("views.feeds.show_published_time") {
114+
if config.K.Bool("widgets.feeds.show_published_time") {
115115
published := item.Item.PublishedParsed
116116
if published != nil {
117117
text += fmt.Sprintf(" [yellowgreen]%s", published.Format("2006-01-02 15:04"))
118118
}
119119
}
120120

121-
if config.K.Bool("views.feeds.show_feed_title") {
121+
if config.K.Bool("widgets.feeds.show_feed_title") {
122122
text += fmt.Sprintf(" [gray]%s", item.Feed.Title)
123123
}
124124

0 commit comments

Comments
 (0)