-
Notifications
You must be signed in to change notification settings - Fork 5
/
tsui.go
176 lines (151 loc) · 4.28 KB
/
tsui.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/neuralinkcorp/tsui/libts"
"github.com/neuralinkcorp/tsui/ui"
"github.com/neuralinkcorp/tsui/version"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
// Injected at build time by the flake.nix.
// This has to be a var or -X can't override it.
var Version = "local"
const (
// Rate at which to poll Tailscale for status updates.
tickInterval = 3 * time.Second
// Rate at which to gather latency from peers.
pingTickInterval = 6 * time.Second
// Per-peer ping timeout.
pingTimeout = 1 * time.Second
// How long to keep messages in the bottom bar.
errorLifetime = 6 * time.Second
successLifetime = 3 * time.Second
tipLifetime = 3 * time.Second
)
// The type of the bottom bar status message:
//
// statusTypeError, statusTypeSuccess
type statusType int
const (
statusTypeError statusType = iota
statusTypeSuccess
statusTypeTip
)
var ctx = context.Background()
// Central model containing application state.
type model struct {
// Current Tailscale state info.
state libts.State
// Ping results per peer.
pings map[tailcfg.StableNodeID]*ipnstate.PingResult
// Whether the user has write permissions to the Tailscale config.
canWrite bool
// Main menu.
menu ui.Appmenu
deviceInfo *ui.AppmenuItem
exitNodes *ui.AppmenuItem
networkDevices *ui.AppmenuItem
settings *ui.AppmenuItem
// Current width of the terminal.
terminalWidth int
// Current height of the terminal.
terminalHeight int
// Type of the status message.
statusType statusType
// Error text displayed at the bottom of the screen.
statusText string
// Current "generation" number for the status. Incremented every time the status
// is updated and used to keep track of status expiration messages.
statusGen int
// Result of the update checker.
latestVersion string
// Frame counter for the loading animation. This is always running in the background,
// even if the animation is not visible.
animationT int
}
// Initialize the application state.
func initialModel() (model, error) {
m := model{
// Main menu items.
deviceInfo: &ui.AppmenuItem{Label: "This Device"},
exitNodes: &ui.AppmenuItem{Label: "Exit Nodes",
Submenu: ui.Submenu{Exclusivity: ui.SubmenuExclusivityOne},
},
networkDevices: &ui.AppmenuItem{Label: "Network Devices"},
settings: &ui.AppmenuItem{Label: "Settings"},
}
state, err := libts.GetState(ctx)
if err != nil {
return m, err
}
m.canWrite = libts.CanWrite(ctx)
m.state = state
m.updateMenus()
return m, nil
}
// Bubbletea init function.
func (m model) Init() tea.Cmd {
return tea.Batch(
// Perform our initial state fetch to populate menus
updateState,
// Run an initial batch of pings.
makeDoPings(m.state.ExitNodes),
// Kick off our ticks.
tea.Tick(tickInterval, func(_ time.Time) tea.Msg {
return tickMsg{}
}),
tea.Tick(pingTickInterval, func(_ time.Time) tea.Msg {
return pingTickMsg{}
}),
tea.Tick(ui.PoggersAnimationInterval, func(_ time.Time) tea.Msg {
return animationTickMsg{}
}),
// And fetch the latest version.
fetchLatestVersion,
)
}
func mainError(err error) {
text := lipgloss.NewStyle().
Foreground(ui.Red).
Render(err.Error())
fmt.Fprintln(os.Stderr, text)
os.Exit(1)
}
func main() {
m, err := initialModel()
if err != nil {
mainError(err)
}
// Enable "alternate screen" mode, a terminal convention designed for rendering
// full-screen, interactive UIs.
p := tea.NewProgram(m, tea.WithAltScreen())
// Run the UI. This will return when the UI exits or errors.
finalModel, err := p.Run()
if err != nil {
mainError(err)
}
if finalModel == nil {
// This sometimes happens when runtime panics occur.
mainError(errors.New("looks like tsui crashed :("))
}
m = finalModel.(model)
if m.latestVersion != "" && Version != "local" && m.latestVersion != Version {
text := lipgloss.NewStyle().
Foreground(ui.Yellow).
Bold(true).
Render("Update available!")
text += lipgloss.NewStyle().
Foreground(ui.Yellow).
Render(fmt.Sprintf(" To upgrade tsui from %s to %s, run:", Version, m.latestVersion))
text += lipgloss.NewStyle().
Foreground(ui.Blue).
Render("\n " + version.UpdateCommand)
fmt.Println(text)
}
}