fix: tview data races
This commit is contained in:
parent
3efd009983
commit
88c352d560
@ -28,7 +28,7 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
state := newStateFromRunParams(params)
|
state := newStateFromRunParams(params)
|
||||||
logger := params.Logger
|
logger := params.Logger
|
||||||
|
|
||||||
ui, err := terminal.StartActor(ctx, terminal.StartActorParams{
|
ui, err := terminal.StartUI(ctx, terminal.StartParams{
|
||||||
ClipboardAvailable: params.ClipboardAvailable,
|
ClipboardAvailable: params.ClipboardAvailable,
|
||||||
BuildInfo: params.BuildInfo,
|
BuildInfo: params.BuildInfo,
|
||||||
Logger: logger.With("component", "ui"),
|
Logger: logger.With("component", "ui"),
|
||||||
@ -50,7 +50,7 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
if exists, err := containerClient.ContainerRunning(ctx, container.AllContainers()); err != nil {
|
if exists, err := containerClient.ContainerRunning(ctx, container.AllContainers()); err != nil {
|
||||||
return fmt.Errorf("check existing containers: %w", err)
|
return fmt.Errorf("check existing containers: %w", err)
|
||||||
} else if exists {
|
} else if exists {
|
||||||
if <-ui.ShowStartupCheckModal() {
|
if ui.ShowStartupCheckModal() {
|
||||||
if err = containerClient.RemoveContainers(ctx, container.AllContainers()); err != nil {
|
if err = containerClient.RemoveContainers(ctx, container.AllContainers()); err != nil {
|
||||||
return fmt.Errorf("remove existing containers: %w", err)
|
return fmt.Errorf("remove existing containers: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// AppState holds application state.
|
// AppState holds application state.
|
||||||
type AppState struct {
|
type AppState struct {
|
||||||
@ -9,6 +12,15 @@ type AppState struct {
|
|||||||
BuildInfo BuildInfo
|
BuildInfo BuildInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone performs a deep copy of AppState.
|
||||||
|
func (s *AppState) Clone() AppState {
|
||||||
|
return AppState{
|
||||||
|
Source: s.Source,
|
||||||
|
Destinations: slices.Clone(s.Destinations),
|
||||||
|
BuildInfo: s.BuildInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BuildInfo holds information about the build.
|
// BuildInfo holds information about the build.
|
||||||
type BuildInfo struct {
|
type BuildInfo struct {
|
||||||
GoVersion string
|
GoVersion string
|
||||||
|
33
domain/types_test.go
Normal file
33
domain/types_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.netflux.io/rob/octoplex/domain"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppStateClone(t *testing.T) {
|
||||||
|
s := &domain.AppState{
|
||||||
|
Source: domain.Source{Live: true},
|
||||||
|
Destinations: []domain.Destination{
|
||||||
|
{
|
||||||
|
Container: domain.Container{ID: "123"},
|
||||||
|
Status: 0,
|
||||||
|
Name: "YouTube",
|
||||||
|
URL: "rtmp://a.rtmp.youtube.com/live2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BuildInfo: domain.BuildInfo{Version: "1.0.0"},
|
||||||
|
}
|
||||||
|
|
||||||
|
s2 := s.Clone()
|
||||||
|
|
||||||
|
assert.Equal(t, s.Source.Live, s2.Source.Live)
|
||||||
|
assert.Equal(t, s.Destinations, s2.Destinations)
|
||||||
|
assert.Equal(t, s.BuildInfo, s2.BuildInfo)
|
||||||
|
|
||||||
|
// ensure the destinations slice is cloned
|
||||||
|
s.Destinations[0].Name = "Twitch"
|
||||||
|
assert.Equal(t, "YouTube", s2.Destinations[0].Name)
|
||||||
|
}
|
@ -24,11 +24,10 @@ type sourceViews struct {
|
|||||||
rx *tview.TextView
|
rx *tview.TextView
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actor is responsible for managing the terminal user interface.
|
// UI is responsible for managing the terminal user interface.
|
||||||
type Actor struct {
|
type UI struct {
|
||||||
app *tview.Application
|
app *tview.Application
|
||||||
pages *tview.Pages
|
pages *tview.Pages
|
||||||
ch chan action
|
|
||||||
commandCh chan Command
|
commandCh chan Command
|
||||||
buildInfo domain.BuildInfo
|
buildInfo domain.BuildInfo
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
@ -36,23 +35,20 @@ type Actor struct {
|
|||||||
destView *tview.Table
|
destView *tview.Table
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultChanSize = 64
|
// StartParams contains the parameters for starting a new terminal user
|
||||||
|
|
||||||
type action func()
|
|
||||||
|
|
||||||
// StartActorParams contains the parameters for starting a new terminal user
|
|
||||||
// interface.
|
// interface.
|
||||||
type StartActorParams struct {
|
type StartParams struct {
|
||||||
ChanSize int
|
ChanSize int
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
ClipboardAvailable bool
|
ClipboardAvailable bool
|
||||||
BuildInfo domain.BuildInfo
|
BuildInfo domain.BuildInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartActor starts the terminal user interface actor.
|
const defaultChanSize = 64
|
||||||
func StartActor(ctx context.Context, params StartActorParams) (*Actor, error) {
|
|
||||||
|
// StartUI starts the terminal user interface.
|
||||||
|
func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
||||||
chanSize := cmp.Or(params.ChanSize, defaultChanSize)
|
chanSize := cmp.Or(params.ChanSize, defaultChanSize)
|
||||||
ch := make(chan action, chanSize)
|
|
||||||
commandCh := make(chan Command, chanSize)
|
commandCh := make(chan Command, chanSize)
|
||||||
|
|
||||||
app := tview.NewApplication()
|
app := tview.NewApplication()
|
||||||
@ -146,8 +142,7 @@ func StartActor(ctx context.Context, params StartActorParams) (*Actor, error) {
|
|||||||
app.SetFocus(destView)
|
app.SetFocus(destView)
|
||||||
app.EnableMouse(false)
|
app.EnableMouse(false)
|
||||||
|
|
||||||
actor := &Actor{
|
ui := &UI{
|
||||||
ch: ch,
|
|
||||||
commandCh: commandCh,
|
commandCh: commandCh,
|
||||||
buildInfo: params.BuildInfo,
|
buildInfo: params.BuildInfo,
|
||||||
logger: params.Logger,
|
logger: params.Logger,
|
||||||
@ -170,50 +165,48 @@ func StartActor(ctx context.Context, params StartActorParams) (*Actor, error) {
|
|||||||
case tcell.KeyRune:
|
case tcell.KeyRune:
|
||||||
switch event.Rune() {
|
switch event.Rune() {
|
||||||
case 'c', 'C':
|
case 'c', 'C':
|
||||||
actor.copySourceURLToClipboard(params.ClipboardAvailable)
|
ui.copySourceURLToClipboard(params.ClipboardAvailable)
|
||||||
case '?':
|
case '?':
|
||||||
actor.showAbout()
|
ui.showAbout()
|
||||||
}
|
}
|
||||||
case tcell.KeyCtrlC:
|
case tcell.KeyCtrlC:
|
||||||
actor.confirmQuit()
|
ui.confirmQuit()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
go actor.actorLoop(ctx)
|
|
||||||
|
|
||||||
return actor, nil
|
go ui.run(ctx)
|
||||||
|
|
||||||
|
return ui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// C returns a channel that receives commands from the user interface.
|
// C returns a channel that receives commands from the user interface.
|
||||||
func (a *Actor) C() <-chan Command {
|
func (ui *UI) C() <-chan Command {
|
||||||
return a.commandCh
|
return ui.commandCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Actor) actorLoop(ctx context.Context) {
|
func (ui *UI) run(ctx context.Context) {
|
||||||
|
defer close(ui.commandCh)
|
||||||
|
|
||||||
uiDone := make(chan struct{})
|
uiDone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
uiDone <- struct{}{}
|
uiDone <- struct{}{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := a.app.Run(); err != nil {
|
if err := ui.app.Run(); err != nil {
|
||||||
a.logger.Error("tui application error", "err", err)
|
ui.logger.Error("tui application error", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
a.logger.Info("Context done")
|
|
||||||
case <-uiDone:
|
|
||||||
close(a.commandCh)
|
|
||||||
case action, ok := <-a.ch:
|
|
||||||
if !ok {
|
|
||||||
return
|
return
|
||||||
}
|
case <-uiDone:
|
||||||
action()
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,9 +217,10 @@ func (a *Actor) actorLoop(ctx context.Context) {
|
|||||||
// The method will block until the user has made a choice, after which the
|
// The method will block until the user has made a choice, after which the
|
||||||
// channel will receive true if the user wants to quit the other instance, or
|
// channel will receive true if the user wants to quit the other instance, or
|
||||||
// false to quit this instance.
|
// false to quit this instance.
|
||||||
func (a *Actor) ShowStartupCheckModal() <-chan bool {
|
func (ui *UI) ShowStartupCheckModal() bool {
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
|
|
||||||
|
ui.app.QueueUpdateDraw(func() {
|
||||||
modal := tview.NewModal()
|
modal := tview.NewModal()
|
||||||
modal.SetText("Another instance of Octoplex may already be running. Pressing continue will close that instance. Continue?").
|
modal.SetText("Another instance of Octoplex may already be running. Pressing continue will close that instance. Continue?").
|
||||||
AddButtons([]string{"Continue", "Exit"}).
|
AddButtons([]string{"Continue", "Exit"}).
|
||||||
@ -234,41 +228,57 @@ func (a *Actor) ShowStartupCheckModal() <-chan bool {
|
|||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(buttonIndex int, _ string) {
|
SetDoneFunc(func(buttonIndex int, _ string) {
|
||||||
if buttonIndex == 0 {
|
if buttonIndex == 0 {
|
||||||
|
ui.pages.RemovePage("modal")
|
||||||
|
ui.app.SetFocus(ui.destView)
|
||||||
done <- true
|
done <- true
|
||||||
a.pages.RemovePage("modal")
|
|
||||||
a.app.SetFocus(a.destView)
|
|
||||||
} else {
|
} else {
|
||||||
done <- false
|
done <- false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
a.pages.AddPage("modal", modal, true, true)
|
ui.pages.AddPage("modal", modal, true, true)
|
||||||
a.app.Draw()
|
})
|
||||||
|
|
||||||
return done
|
return <-done
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetState sets the state of the terminal user interface.
|
func (ui *UI) handleMediaServerClosed(exitReason string) {
|
||||||
func (a *Actor) SetState(state domain.AppState) {
|
done := make(chan struct{})
|
||||||
a.ch <- func() {
|
|
||||||
if state.Source.ExitReason != "" {
|
ui.app.QueueUpdateDraw(func() {
|
||||||
modal := tview.NewModal()
|
modal := tview.NewModal()
|
||||||
modal.SetText("Mediaserver error: " + state.Source.ExitReason).
|
modal.SetText("Mediaserver error: " + exitReason).
|
||||||
AddButtons([]string{"Quit"}).
|
AddButtons([]string{"Quit"}).
|
||||||
SetBackgroundColor(tcell.ColorBlack).
|
SetBackgroundColor(tcell.ColorBlack).
|
||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(int, string) {
|
SetDoneFunc(func(int, string) {
|
||||||
|
ui.logger.Info("closing app")
|
||||||
// TODO: improve app cleanup
|
// TODO: improve app cleanup
|
||||||
a.app.Stop()
|
done <- struct{}{}
|
||||||
|
|
||||||
|
ui.app.Stop()
|
||||||
})
|
})
|
||||||
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
a.pages.AddPage("modal", modal, true, true)
|
ui.pages.AddPage("modal", modal, true, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
a.redrawFromState(state)
|
// SetState sets the state of the terminal user interface.
|
||||||
|
func (ui *UI) SetState(state domain.AppState) {
|
||||||
|
if state.Source.ExitReason != "" {
|
||||||
|
ui.handleMediaServerClosed(state.Source.ExitReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The state is mutable so can't be passed into QueueUpdateDraw, which
|
||||||
|
// passes it to another goroutine, without cloning it first.
|
||||||
|
stateClone := state.Clone()
|
||||||
|
ui.app.QueueUpdateDraw(func() {
|
||||||
|
ui.redrawFromState(stateClone)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const dash = "—"
|
const dash = "—"
|
||||||
@ -286,7 +296,7 @@ const (
|
|||||||
headerTracks = "Tracks"
|
headerTracks = "Tracks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Actor) redrawFromState(state domain.AppState) {
|
func (ui *UI) redrawFromState(state domain.AppState) {
|
||||||
headerCell := func(content string, expansion int) *tview.TableCell {
|
headerCell := func(content string, expansion int) *tview.TableCell {
|
||||||
return tview.
|
return tview.
|
||||||
NewTableCell(content).
|
NewTableCell(content).
|
||||||
@ -295,59 +305,59 @@ func (a *Actor) redrawFromState(state domain.AppState) {
|
|||||||
SetSelectable(false)
|
SetSelectable(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.sourceViews.url.SetText(state.Source.RTMPURL)
|
ui.sourceViews.url.SetText(state.Source.RTMPURL)
|
||||||
|
|
||||||
tracks := dash
|
tracks := dash
|
||||||
if state.Source.Live && len(state.Source.Tracks) > 0 {
|
if state.Source.Live && len(state.Source.Tracks) > 0 {
|
||||||
tracks = strings.Join(state.Source.Tracks, ", ")
|
tracks = strings.Join(state.Source.Tracks, ", ")
|
||||||
}
|
}
|
||||||
a.sourceViews.tracks.SetText(tracks)
|
ui.sourceViews.tracks.SetText(tracks)
|
||||||
|
|
||||||
if state.Source.Live {
|
if state.Source.Live {
|
||||||
a.sourceViews.status.SetText("[black:green]receiving")
|
ui.sourceViews.status.SetText("[black:green]receiving")
|
||||||
} else if state.Source.Container.State == "running" && state.Source.Container.HealthState == "healthy" {
|
} else if state.Source.Container.State == "running" && state.Source.Container.HealthState == "healthy" {
|
||||||
a.sourceViews.status.SetText("[black:yellow]ready")
|
ui.sourceViews.status.SetText("[black:yellow]ready")
|
||||||
} else {
|
} else {
|
||||||
a.sourceViews.status.SetText("[white:red]not ready")
|
ui.sourceViews.status.SetText("[white:red]not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
a.sourceViews.health.SetText("[white]" + cmp.Or(rightPad(state.Source.Container.HealthState, 9), dash))
|
ui.sourceViews.health.SetText("[white]" + cmp.Or(rightPad(state.Source.Container.HealthState, 9), dash))
|
||||||
|
|
||||||
cpuPercent := dash
|
cpuPercent := dash
|
||||||
if state.Source.Container.State == "running" {
|
if state.Source.Container.State == "running" {
|
||||||
cpuPercent = fmt.Sprintf("%.1f", state.Source.Container.CPUPercent)
|
cpuPercent = fmt.Sprintf("%.1f", state.Source.Container.CPUPercent)
|
||||||
}
|
}
|
||||||
a.sourceViews.cpu.SetText("[white]" + cpuPercent)
|
ui.sourceViews.cpu.SetText("[white]" + cpuPercent)
|
||||||
|
|
||||||
memUsage := dash
|
memUsage := dash
|
||||||
if state.Source.Container.State == "running" {
|
if state.Source.Container.State == "running" {
|
||||||
memUsage = fmt.Sprintf("%.1f", float64(state.Source.Container.MemoryUsageBytes)/1024/1024)
|
memUsage = fmt.Sprintf("%.1f", float64(state.Source.Container.MemoryUsageBytes)/1024/1024)
|
||||||
}
|
}
|
||||||
a.sourceViews.mem.SetText("[white]" + memUsage)
|
ui.sourceViews.mem.SetText("[white]" + memUsage)
|
||||||
|
|
||||||
rxRate := dash
|
rxRate := dash
|
||||||
if state.Source.Container.State == "running" {
|
if state.Source.Container.State == "running" {
|
||||||
rxRate = fmt.Sprintf("%d", state.Source.Container.RxRate)
|
rxRate = fmt.Sprintf("%d", state.Source.Container.RxRate)
|
||||||
}
|
}
|
||||||
a.sourceViews.rx.SetText("[white]" + rxRate)
|
ui.sourceViews.rx.SetText("[white]" + rxRate)
|
||||||
|
|
||||||
a.destView.Clear()
|
ui.destView.Clear()
|
||||||
a.destView.SetCell(0, 0, headerCell("[grey]"+headerName, 3))
|
ui.destView.SetCell(0, 0, headerCell("[grey]"+headerName, 3))
|
||||||
a.destView.SetCell(0, 1, headerCell("[grey]"+headerURL, 3))
|
ui.destView.SetCell(0, 1, headerCell("[grey]"+headerURL, 3))
|
||||||
a.destView.SetCell(0, 2, headerCell("[grey]"+headerStatus, 2))
|
ui.destView.SetCell(0, 2, headerCell("[grey]"+headerStatus, 2))
|
||||||
a.destView.SetCell(0, 3, headerCell("[grey]"+headerContainer, 2))
|
ui.destView.SetCell(0, 3, headerCell("[grey]"+headerContainer, 2))
|
||||||
a.destView.SetCell(0, 4, headerCell("[grey]"+headerHealth, 2))
|
ui.destView.SetCell(0, 4, headerCell("[grey]"+headerHealth, 2))
|
||||||
a.destView.SetCell(0, 5, headerCell("[grey]"+headerCPU, 1))
|
ui.destView.SetCell(0, 5, headerCell("[grey]"+headerCPU, 1))
|
||||||
a.destView.SetCell(0, 6, headerCell("[grey]"+headerMem, 1))
|
ui.destView.SetCell(0, 6, headerCell("[grey]"+headerMem, 1))
|
||||||
a.destView.SetCell(0, 7, headerCell("[grey]"+headerTx, 1))
|
ui.destView.SetCell(0, 7, headerCell("[grey]"+headerTx, 1))
|
||||||
|
|
||||||
for i, dest := range state.Destinations {
|
for i, dest := range state.Destinations {
|
||||||
a.destView.SetCell(i+1, 0, tview.NewTableCell(dest.Name))
|
ui.destView.SetCell(i+1, 0, tview.NewTableCell(dest.Name))
|
||||||
a.destView.SetCell(i+1, 1, tview.NewTableCell(dest.URL).SetReference(dest.URL).SetMaxWidth(20))
|
ui.destView.SetCell(i+1, 1, tview.NewTableCell(dest.URL).SetReference(dest.URL).SetMaxWidth(20))
|
||||||
const statusLen = 10
|
const statusLen = 10
|
||||||
switch dest.Status {
|
switch dest.Status {
|
||||||
case domain.DestinationStatusLive:
|
case domain.DestinationStatusLive:
|
||||||
a.destView.SetCell(
|
ui.destView.SetCell(
|
||||||
i+1,
|
i+1,
|
||||||
2,
|
2,
|
||||||
tview.NewTableCell(rightPad("sending", statusLen)).
|
tview.NewTableCell(rightPad("sending", statusLen)).
|
||||||
@ -361,48 +371,46 @@ func (a *Actor) redrawFromState(state domain.AppState) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
a.destView.SetCell(i+1, 2, tview.NewTableCell("[white]"+rightPad("off-air", statusLen)))
|
ui.destView.SetCell(i+1, 2, tview.NewTableCell("[white]"+rightPad("off-air", statusLen)))
|
||||||
}
|
}
|
||||||
|
|
||||||
a.destView.SetCell(i+1, 3, tview.NewTableCell("[white]"+rightPad(cmp.Or(dest.Container.State, dash), 10)))
|
ui.destView.SetCell(i+1, 3, tview.NewTableCell("[white]"+rightPad(cmp.Or(dest.Container.State, dash), 10)))
|
||||||
|
|
||||||
healthState := dash
|
healthState := dash
|
||||||
if dest.Status == domain.DestinationStatusLive {
|
if dest.Status == domain.DestinationStatusLive {
|
||||||
healthState = "healthy"
|
healthState = "healthy"
|
||||||
}
|
}
|
||||||
a.destView.SetCell(i+1, 4, tview.NewTableCell("[white]"+rightPad(healthState, 7)))
|
ui.destView.SetCell(i+1, 4, tview.NewTableCell("[white]"+rightPad(healthState, 7)))
|
||||||
|
|
||||||
cpuPercent := dash
|
cpuPercent := dash
|
||||||
if dest.Container.State == "running" {
|
if dest.Container.State == "running" {
|
||||||
cpuPercent = fmt.Sprintf("%.1f", dest.Container.CPUPercent)
|
cpuPercent = fmt.Sprintf("%.1f", dest.Container.CPUPercent)
|
||||||
}
|
}
|
||||||
a.destView.SetCell(i+1, 5, tview.NewTableCell("[white]"+rightPad(cpuPercent, 4)))
|
ui.destView.SetCell(i+1, 5, tview.NewTableCell("[white]"+rightPad(cpuPercent, 4)))
|
||||||
|
|
||||||
memoryUsage := dash
|
memoryUsage := dash
|
||||||
if dest.Container.State == "running" {
|
if dest.Container.State == "running" {
|
||||||
memoryUsage = fmt.Sprintf("%.1f", float64(dest.Container.MemoryUsageBytes)/1000/1000)
|
memoryUsage = fmt.Sprintf("%.1f", float64(dest.Container.MemoryUsageBytes)/1000/1000)
|
||||||
}
|
}
|
||||||
a.destView.SetCell(i+1, 6, tview.NewTableCell("[white]"+rightPad(memoryUsage, 4)))
|
ui.destView.SetCell(i+1, 6, tview.NewTableCell("[white]"+rightPad(memoryUsage, 4)))
|
||||||
|
|
||||||
txRate := dash
|
txRate := dash
|
||||||
if dest.Container.State == "running" {
|
if dest.Container.State == "running" {
|
||||||
txRate = "[white]" + rightPad(strconv.Itoa(dest.Container.TxRate), 4)
|
txRate = "[white]" + rightPad(strconv.Itoa(dest.Container.TxRate), 4)
|
||||||
}
|
}
|
||||||
a.destView.SetCell(i+1, 7, tview.NewTableCell(txRate))
|
ui.destView.SetCell(i+1, 7, tview.NewTableCell(txRate))
|
||||||
}
|
}
|
||||||
|
|
||||||
a.app.Draw()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the terminal user interface.
|
// Close closes the terminal user interface.
|
||||||
func (a *Actor) Close() {
|
func (ui *UI) Close() {
|
||||||
a.app.Stop()
|
ui.app.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Actor) copySourceURLToClipboard(clipboardAvailable bool) {
|
func (ui *UI) copySourceURLToClipboard(clipboardAvailable bool) {
|
||||||
var text string
|
var text string
|
||||||
if clipboardAvailable {
|
if clipboardAvailable {
|
||||||
clipboard.Write(clipboard.FmtText, []byte(a.sourceViews.url.GetText(true)))
|
clipboard.Write(clipboard.FmtText, []byte(ui.sourceViews.url.GetText(true)))
|
||||||
text = "Ingress URL copied to clipboard"
|
text = "Ingress URL copied to clipboard"
|
||||||
} else {
|
} else {
|
||||||
text = "Copy to clipboard not available"
|
text = "Copy to clipboard not available"
|
||||||
@ -416,14 +424,14 @@ func (a *Actor) copySourceURLToClipboard(clipboardAvailable bool) {
|
|||||||
SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
modal.SetDoneFunc(func(buttonIndex int, _ string) {
|
modal.SetDoneFunc(func(buttonIndex int, _ string) {
|
||||||
a.pages.RemovePage("modal")
|
ui.pages.RemovePage("modal")
|
||||||
a.app.SetFocus(a.destView)
|
ui.app.SetFocus(ui.destView)
|
||||||
})
|
})
|
||||||
|
|
||||||
a.pages.AddPage("modal", modal, true, true)
|
ui.pages.AddPage("modal", modal, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Actor) confirmQuit() {
|
func (ui *UI) confirmQuit() {
|
||||||
modal := tview.NewModal()
|
modal := tview.NewModal()
|
||||||
modal.SetText("Are you sure you want to quit?").
|
modal.SetText("Are you sure you want to quit?").
|
||||||
AddButtons([]string{"Quit", "Cancel"}).
|
AddButtons([]string{"Quit", "Cancel"}).
|
||||||
@ -431,36 +439,36 @@ func (a *Actor) confirmQuit() {
|
|||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(buttonIndex int, _ string) {
|
SetDoneFunc(func(buttonIndex int, _ string) {
|
||||||
if buttonIndex == 1 || buttonIndex == -1 {
|
if buttonIndex == 1 || buttonIndex == -1 {
|
||||||
a.pages.RemovePage("modal")
|
ui.pages.RemovePage("modal")
|
||||||
a.app.SetFocus(a.destView)
|
ui.app.SetFocus(ui.destView)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.commandCh <- CommandQuit{}
|
ui.commandCh <- CommandQuit{}
|
||||||
})
|
})
|
||||||
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
a.pages.AddPage("modal", modal, true, true)
|
ui.pages.AddPage("modal", modal, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Actor) showAbout() {
|
func (ui *UI) showAbout() {
|
||||||
modal := tview.NewModal()
|
modal := tview.NewModal()
|
||||||
modal.SetText(fmt.Sprintf(
|
modal.SetText(fmt.Sprintf(
|
||||||
"%s: live stream multiplexer\n\nv0.0.0 %s (%s)",
|
"%s: live stream multiplexer\n\nv0.0.0 %s (%s)",
|
||||||
domain.AppName,
|
domain.AppName,
|
||||||
a.buildInfo.Version,
|
ui.buildInfo.Version,
|
||||||
a.buildInfo.GoVersion,
|
ui.buildInfo.GoVersion,
|
||||||
)).
|
)).
|
||||||
AddButtons([]string{"Ok"}).
|
AddButtons([]string{"Ok"}).
|
||||||
SetBackgroundColor(tcell.ColorBlack).
|
SetBackgroundColor(tcell.ColorBlack).
|
||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(buttonIndex int, _ string) {
|
SetDoneFunc(func(buttonIndex int, _ string) {
|
||||||
a.pages.RemovePage("modal")
|
ui.pages.RemovePage("modal")
|
||||||
a.app.SetFocus(a.destView)
|
ui.app.SetFocus(ui.destView)
|
||||||
})
|
})
|
||||||
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
a.pages.AddPage("modal", modal, true, true)
|
ui.pages.AddPage("modal", modal, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rightPad(s string, n int) string {
|
func rightPad(s string, n int) string {
|
Loading…
x
Reference in New Issue
Block a user