From a8b86a60d36f1c98d9b7f57979254b0205911231 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Fri, 23 Aug 2024 22:03:55 +0800 Subject: [PATCH] feat: status bar --- internal/tui/constants/constants.go | 3 + internal/tui/node_details/node_details.go | 7 +++ internal/tui/node_list/node_list.go | 20 ++++-- internal/tui/status_bar/status_bar.go | 76 +++++++++++++++++++++++ internal/tui/tui.go | 35 ++++++++--- internal/tui/types/types.go | 4 ++ 6 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 internal/tui/status_bar/status_bar.go create mode 100644 internal/tui/types/types.go diff --git a/internal/tui/constants/constants.go b/internal/tui/constants/constants.go index 572f3c8..4a7ca82 100644 --- a/internal/tui/constants/constants.go +++ b/internal/tui/constants/constants.go @@ -2,7 +2,9 @@ package constants import "github.com/charmbracelet/lipgloss" +var ColorBW = lipgloss.AdaptiveColor{Light: "#000", Dark: "#FFF"} var ColorNormal = lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#DDDDDD"} +var ColorNormalInv = lipgloss.AdaptiveColor{Light: "#DDDDDD", Dark: "#1A1A1A"} var ColorDanger = lipgloss.AdaptiveColor{Light: "197", Dark: "197"} var ColorSuccess = lipgloss.AdaptiveColor{Light: "034", Dark: "049"} var ColorWarning = lipgloss.AdaptiveColor{Light: "214", Dark: "214"} @@ -13,6 +15,7 @@ var ColorSecondary = lipgloss.AdaptiveColor{Light: "#05EAFF", Dark: "#00E5FA"} var PrimaryTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorPrimary.Dark)).Foreground(lipgloss.Color("#000000")) var SecondaryTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorSecondary.Light)).Foreground(lipgloss.Color("#000000")) +var WarningTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorWarning.Light)).Foreground(lipgloss.Color("#000000")) var NormalTextStyle = lipgloss.NewStyle().Foreground(ColorNormal) var DangerTextStyle = lipgloss.NewStyle().Foreground(ColorDanger) var SuccessTextStyle = lipgloss.NewStyle().Foreground(ColorSuccess) diff --git a/internal/tui/node_details/node_details.go b/internal/tui/node_details/node_details.go index e839882..b1f757b 100644 --- a/internal/tui/node_details/node_details.go +++ b/internal/tui/node_details/node_details.go @@ -1,8 +1,12 @@ package nodedetails import ( + "fmt" + "github.com/atotto/clipboard" + "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" + "github.com/bilguun0203/tailscale-tui/internal/tui/types" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -54,6 +58,9 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } if copyStr != "" { clipboard.WriteAll(copyStr) + status := fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr)) + cmd = func() tea.Msg { return types.StatusMsg(status) } + cmds = append(cmds, cmd) } } } diff --git a/internal/tui/node_list/node_list.go b/internal/tui/node_list/node_list.go index 8e5423f..52abd4c 100644 --- a/internal/tui/node_list/node_list.go +++ b/internal/tui/node_list/node_list.go @@ -10,6 +10,7 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" + "github.com/bilguun0203/tailscale-tui/internal/tui/types" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/spinner" @@ -46,7 +47,6 @@ func (m *Model) SetSize(w int, h int) { } type NodeSelectedMsg tsKey.NodePublic -type RefreshMsg bool func (m *Model) updateKeybindings() { if m.list.SelectedItem() != nil { @@ -84,13 +84,21 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } if copyStr == "" { m.list.NewStatusMessage("Sorry, nothing to copy.") + cmd = func() tea.Msg { return types.StatusMsg("Sorry, nothing to copy.") } + cmds = append(cmds, cmd) } else { - clipboard.WriteAll(copyStr) - m.list.NewStatusMessage(fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr))) + err := clipboard.WriteAll(copyStr) + status := fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr)) + if err != nil { + status = fmt.Sprintf("Sorry, error occured: %s", err) + } + m.list.NewStatusMessage(status) + cmd = func() tea.Msg { return types.StatusMsg(status) } + cmds = append(cmds, cmd) } } if key.Matches(msg, m.keyMap.Refresh) { - cmd = func() tea.Msg { return RefreshMsg(true) } + cmd = func() tea.Msg { return types.RefreshMsg(true) } cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.Enter) { @@ -99,12 +107,12 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } if key.Matches(msg, m.keyMap.TSUp) { ts.SetTSStatus(true) - cmd = func() tea.Msg { return RefreshMsg(true) } + cmd = func() tea.Msg { return types.RefreshMsg(true) } cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.TSDown) { ts.SetTSStatus(false) - cmd = func() tea.Msg { return RefreshMsg(true) } + cmd = func() tea.Msg { return types.RefreshMsg(true) } cmds = append(cmds, cmd) } return m, cmds diff --git a/internal/tui/status_bar/status_bar.go b/internal/tui/status_bar/status_bar.go new file mode 100644 index 0000000..ced3aac --- /dev/null +++ b/internal/tui/status_bar/status_bar.go @@ -0,0 +1,76 @@ +package statusbar + +import ( + "github.com/bilguun0203/tailscale-tui/internal/tui/constants" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type Model struct { + barStyle lipgloss.Style + prefix string + prefixStyle lipgloss.Style + msg string + msgStyle lipgloss.Style + suffix string + suffixStyle lipgloss.Style + w int + h int +} + +func (m *Model) UpdatePrefix(v string) { + m.prefix = v +} + +func (m *Model) UpdateMessage(v string) { + m.msg = v +} + +func (m *Model) UpdateSuffix(v string) { + m.suffix = v +} + +func (m *Model) UpdatePrefixStyle(style lipgloss.Style) { + m.prefixStyle = style +} + +func (m *Model) UpdateMessageStyle(style lipgloss.Style) { + m.msgStyle = style +} + +func (m *Model) UpdateSuffixStyle(style lipgloss.Style) { + m.suffixStyle = style +} + +func (m Model) Init() tea.Cmd { + return nil +} +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.w, m.h = msg.Width, msg.Height + m.barStyle = m.barStyle.Width(m.w) + } + return m, nil +} + +func (m Model) View() string { + prefixView := m.prefixStyle.Render(m.prefix) + suffixView := m.suffixStyle.Render(m.suffix) + msgW := m.w - lipgloss.Width(prefixView) - lipgloss.Width(suffixView) + msgView := m.msgStyle.Padding(0, 1).Width(msgW).Render(m.msg) + return m.barStyle.Render(prefixView + msgView + suffixView) +} + +func New() Model { + barStyle := lipgloss.NewStyle().Background(constants.ColorNormalInv).Margin(1, 0).Height(1) + return Model{ + barStyle: barStyle, + prefix: "TAILSCALE-TUI", + prefixStyle: constants.PrimaryTitleStyle, + msg: "", + msgStyle: lipgloss.NewStyle().Background(barStyle.GetBackground()).Foreground(constants.ColorBW), + suffix: "", + suffixStyle: constants.SecondaryTitleStyle, + } +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 0c06a8f..06d23de 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -7,8 +7,11 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/tui/constants" nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" nodelist "github.com/bilguun0203/tailscale-tui/internal/tui/node_list" + statusbar "github.com/bilguun0203/tailscale-tui/internal/tui/status_bar" + "github.com/bilguun0203/tailscale-tui/internal/tui/types" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "tailscale.com/ipn/ipnstate" tsKey "tailscale.com/types/key" ) @@ -35,6 +38,7 @@ type Model struct { Err error nodelist nodelist.Model nodedetails nodedetails.Model + statusbar statusbar.Model spinner spinner.Model w, h int } @@ -67,23 +71,29 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Err = nil m.tsStatus = msg m.viewState = viewStateList + m.statusbar.UpdateMessage("Showing all network devices") case ts.StatusErrorMsg: m.isLoading = false m.Err = msg + m.statusbar.UpdateMessage(fmt.Sprintf("Error: %s", msg)) return m, tea.Quit case nodedetails.BackMsg: m.viewState = viewStateList - case nodelist.RefreshMsg: + m.statusbar.UpdateMessage("Showing all network devices") + case types.RefreshMsg: m.isLoading = true cmds = append(cmds, m.getTsStatus()) cmds = append(cmds, m.spinner.Tick) + case types.StatusMsg: + m.statusbar.UpdateMessage(string(msg)) case nodelist.NodeSelectedMsg: m.selectedNodeID = tsKey.NodePublic(msg) m.nodedetails = nodedetails.New(m.tsStatus, m.selectedNodeID, m.w, m.h) m.viewState = viewStateDetails + m.statusbar.UpdateMessage("Showing device details") case tea.WindowSizeMsg: - m.w, m.h = msg.Width, msg.Height - m.nodelist.SetSize(msg.Width, msg.Height) + m.w, m.h = msg.Width, msg.Height-3 + m.nodelist.SetSize(m.w, m.h) case spinner.TickMsg: if m.isLoading { m.spinner, tmpCmd = m.spinner.Update(msg) @@ -91,6 +101,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + if m.isLoading { + m.statusbar.UpdatePrefixStyle(constants.WarningTitleStyle) + } else { + m.statusbar.UpdatePrefixStyle(constants.PrimaryTitleStyle) + } + switch m.viewState { case viewStateDetails: m.nodedetails, tmpCmd = m.nodedetails.Update(msg) @@ -104,18 +120,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, tmpCmd) } } + m.statusbar, tmpCmd = m.statusbar.Update(msg) + cmds = append(cmds, tmpCmd) return m, tea.Batch(cmds...) } func (m Model) View() string { switch m.viewState { case viewStateDetails: - return m.nodedetails.View() + return lipgloss.JoinVertical(lipgloss.Left, m.nodedetails.View(), m.statusbar.View()) case viewStateList: if m.isLoading { - return fmt.Sprintf("\n\n %s Loading...\n\n", m.spinner.View()) + m.statusbar.UpdateMessage(fmt.Sprintf("%s Loading...", m.spinner.View())) } - return m.nodelist.View() + return lipgloss.JoinVertical(lipgloss.Left, m.nodelist.View(), m.statusbar.View()) default: return "*_*" } @@ -126,10 +144,11 @@ func New() Model { viewState: viewStateList, isLoading: true, spinner: spinner.New(), + statusbar: statusbar.New(), } - m.spinner.Spinner = spinner.Dot + m.spinner.Spinner = spinner.Line m.spinner.Style = constants.SpinnerStyle - m.nodelist = nodelist.New(nil, m.w, m.h) + m.nodelist = nodelist.New(nil, m.w, m.h-lipgloss.Height(m.statusbar.View())) return m } diff --git a/internal/tui/types/types.go b/internal/tui/types/types.go new file mode 100644 index 0000000..744bc0d --- /dev/null +++ b/internal/tui/types/types.go @@ -0,0 +1,4 @@ +package types + +type RefreshMsg bool +type StatusMsg string