mirror of
https://github.com/Adembc/lazyssh.git
synced 2026-07-14 12:13:34 +02:00
improve ui architecture
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package ui
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
AppName = "lazyssh"
|
||||
AppVersion = "v0.0.1"
|
||||
RepoURL = "github.com/adembc/lazyssh"
|
||||
Banner = `
|
||||
AppName = "lazyssh"
|
||||
AppVersion = "v0.0.1"
|
||||
RepoURL = "github.com/adembc/lazyssh"
|
||||
SplashScreenDuration = 1 * time.Second
|
||||
Banner = `
|
||||
|
||||
$$\ $$\
|
||||
$$ | $$ |
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Adembc/lazyssh/internal/core/domain"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey {
|
||||
// =============================================================================
|
||||
// Event Handlers (handle user input/events)
|
||||
// =============================================================================
|
||||
|
||||
func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey {
|
||||
// Don't handle global keys when search has focus
|
||||
if t.app.GetFocus() == t.searchBar {
|
||||
return event
|
||||
@@ -17,31 +22,31 @@ func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey {
|
||||
t.app.Stop()
|
||||
return nil
|
||||
case '/':
|
||||
t.showSearchBar()
|
||||
t.handleSearchToggle()
|
||||
return nil
|
||||
case 'a':
|
||||
t.showAddServerForm()
|
||||
t.handleServerAdd()
|
||||
return nil
|
||||
case 'e':
|
||||
t.showEditServerForm()
|
||||
t.handleServerEdit()
|
||||
return nil
|
||||
case 'd':
|
||||
t.deleteSelectedServer()
|
||||
t.handleServerDelete()
|
||||
return nil
|
||||
case '?':
|
||||
t.showHelpModal()
|
||||
t.handleHelpShow()
|
||||
return nil
|
||||
}
|
||||
|
||||
if event.Key() == tcell.KeyEnter {
|
||||
t.connectToSelectedServer()
|
||||
t.handleServerConnect()
|
||||
return nil
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (t *tui) handleSearch(query string) {
|
||||
func (t *tui) handleSearchInput(query string) {
|
||||
filtered, _ := t.serverService.ListServers(query)
|
||||
t.serverList.UpdateServers(filtered)
|
||||
if len(filtered) == 0 {
|
||||
@@ -49,42 +54,32 @@ func (t *tui) handleSearch(query string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) handleServerConnect(server domain.Server) {
|
||||
showConnectModal(t.app, t.root, server)
|
||||
func (t *tui) handleSearchToggle() {
|
||||
t.showSearchBar()
|
||||
}
|
||||
|
||||
func (t *tui) handleServerConnect() {
|
||||
if server, ok := t.serverList.GetSelectedServer(); ok {
|
||||
t.showConnectModal(server)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) handleServerSelectionChange(server domain.Server) {
|
||||
t.details.UpdateServer(server)
|
||||
}
|
||||
|
||||
func (t *tui) showSearchBar() {
|
||||
t.left.Clear()
|
||||
t.left.AddItem(t.searchBar, 3, 0, true)
|
||||
t.left.AddItem(t.serverList, 0, 1, false)
|
||||
t.app.SetFocus(t.searchBar)
|
||||
t.searchVisible = true
|
||||
}
|
||||
|
||||
func (t *tui) hideSearchBar() {
|
||||
t.left.Clear()
|
||||
t.left.AddItem(t.hintBar, 1, 0, false)
|
||||
t.left.AddItem(t.serverList, 0, 1, true)
|
||||
t.app.SetFocus(t.serverList)
|
||||
t.searchVisible = false
|
||||
}
|
||||
|
||||
func (t *tui) showAddServerForm() {
|
||||
func (t *tui) handleServerAdd() {
|
||||
form := NewServerForm(ServerFormAdd, nil).
|
||||
OnSave(t.handleServerSave).
|
||||
OnCancel(t.returnToMain)
|
||||
OnCancel(t.handleFormCancel)
|
||||
t.app.SetRoot(form, true)
|
||||
}
|
||||
|
||||
func (t *tui) showEditServerForm() {
|
||||
func (t *tui) handleServerEdit() {
|
||||
if server, ok := t.serverList.GetSelectedServer(); ok {
|
||||
form := NewServerForm(ServerFormEdit, &server).
|
||||
OnSave(t.handleServerSave).
|
||||
OnCancel(t.returnToMain)
|
||||
OnCancel(t.handleFormCancel)
|
||||
t.app.SetRoot(form, true)
|
||||
}
|
||||
}
|
||||
@@ -99,16 +94,91 @@ func (t *tui) handleServerSave(server domain.Server, original *domain.Server) {
|
||||
}
|
||||
|
||||
t.refreshServerList()
|
||||
t.returnToMain()
|
||||
t.handleFormCancel()
|
||||
}
|
||||
|
||||
func (t *tui) deleteSelectedServer() {
|
||||
func (t *tui) handleServerDelete() {
|
||||
if server, ok := t.serverList.GetSelectedServer(); ok {
|
||||
_ = t.serverService.DeleteServer(server)
|
||||
t.refreshServerList()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) handleFormCancel() {
|
||||
t.returnToMain()
|
||||
}
|
||||
|
||||
func (t *tui) handleHelpShow() {
|
||||
t.showHelpModal()
|
||||
}
|
||||
|
||||
func (t *tui) handleModalClose() {
|
||||
t.returnToMain()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UI Display Functions (show UI elements/modals)
|
||||
// =============================================================================
|
||||
|
||||
func (t *tui) showSearchBar() {
|
||||
t.left.Clear()
|
||||
t.left.AddItem(t.searchBar, 3, 0, true)
|
||||
t.left.AddItem(t.serverList, 0, 1, false)
|
||||
t.app.SetFocus(t.searchBar)
|
||||
t.searchVisible = true
|
||||
}
|
||||
|
||||
func (t *tui) showConnectModal(server domain.Server) {
|
||||
msg := fmt.Sprintf("SSH to %s (%s@%s:%d)\n\nThis is a mock action.",
|
||||
server.Alias, server.User, server.Host, server.Port)
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(msg).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
t.handleModalClose()
|
||||
})
|
||||
|
||||
t.app.SetRoot(modal, true)
|
||||
}
|
||||
|
||||
func (t *tui) showHelpModal() {
|
||||
text := "Keyboard shortcuts:\n\n" +
|
||||
" ↑/↓ Navigate\n" +
|
||||
" Enter SSH connect (mock)\n" +
|
||||
" a Add server (mock)\n" +
|
||||
" e Edit server (mock)\n" +
|
||||
" d Delete entry (mock)\n" +
|
||||
" / Focus search\n" +
|
||||
" q Quit\n" +
|
||||
" ? Help\n"
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(text).
|
||||
AddButtons([]string{"Close"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
t.handleModalClose()
|
||||
})
|
||||
|
||||
t.app.SetRoot(modal, true)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UI State Management (hide UI elements)
|
||||
// =============================================================================
|
||||
|
||||
func (t *tui) hideSearchBar() {
|
||||
t.left.Clear()
|
||||
t.left.AddItem(t.hintBar, 1, 0, false)
|
||||
t.left.AddItem(t.serverList, 0, 1, true)
|
||||
t.app.SetFocus(t.serverList)
|
||||
t.searchVisible = false
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Internal Operations (perform actual work)
|
||||
// =============================================================================
|
||||
|
||||
func (t *tui) refreshServerList() {
|
||||
query := ""
|
||||
if t.searchVisible {
|
||||
@@ -118,16 +188,6 @@ func (t *tui) refreshServerList() {
|
||||
t.serverList.UpdateServers(filtered)
|
||||
}
|
||||
|
||||
func (t *tui) connectToSelectedServer() {
|
||||
if server, ok := t.serverList.GetSelectedServer(); ok {
|
||||
t.handleServerConnect(server)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tui) returnToMain() {
|
||||
t.app.SetRoot(t.root, true)
|
||||
}
|
||||
|
||||
func (t *tui) showHelpModal() {
|
||||
showHelpModal(t.app, t.root)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/rivo/tview"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ServerFormMode int
|
||||
@@ -122,6 +123,9 @@ func (sf *ServerForm) handleSave() {
|
||||
}
|
||||
|
||||
server := sf.dataToServer(data)
|
||||
if sf.original == nil {
|
||||
server.LastSeen = time.Now()
|
||||
}
|
||||
if sf.onSave != nil {
|
||||
sf.onSave(server, sf.original)
|
||||
}
|
||||
@@ -151,7 +155,6 @@ func (sf *ServerForm) dataToServer(data ServerFormData) domain.Server {
|
||||
}
|
||||
|
||||
_, status := sf.GetFormItem(6).(*tview.DropDown).GetCurrentOption()
|
||||
|
||||
return domain.Server{
|
||||
Alias: data.Alias,
|
||||
Host: data.Host,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// buildSplash creates a splash screen with a spinner; returns primitive and a stop function
|
||||
func buildSplash(app *tview.Application) (tview.Primitive, func()) {
|
||||
// Brand styling
|
||||
title := "[#FFFFFF::b]lazy[-][#55D7FF::b]ssh[-]"
|
||||
tagline := "[#AAAAAA]Fast server picker TUI • Loading your servers…[-]"
|
||||
spinnerFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
|
||||
text := tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
text.SetBorder(true).
|
||||
SetTitle(" 🚀 lazyssh ").
|
||||
SetBorderColor(tcell.Color238).
|
||||
SetTitleColor(tcell.Color250)
|
||||
|
||||
// layout to center the card
|
||||
box := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(tview.NewBox(), 0, 1, false).
|
||||
AddItem(text, 9, 0, false).
|
||||
AddItem(tview.NewBox(), 0, 1, false)
|
||||
|
||||
container := tview.NewFlex().SetDirection(tview.FlexColumn).
|
||||
AddItem(tview.NewBox(), 0, 1, false).
|
||||
AddItem(box, 0, 2, false).
|
||||
AddItem(tview.NewBox(), 0, 1, false)
|
||||
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(90 * time.Millisecond):
|
||||
frame := spinnerFrames[i%len(spinnerFrames)]
|
||||
|
||||
width := 20
|
||||
filled := i % (width + 1)
|
||||
prog := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
|
||||
|
||||
// rotate tips every ~12 frames
|
||||
i++
|
||||
app.QueueUpdateDraw(func() {
|
||||
text.SetText(
|
||||
"\n" +
|
||||
"[::b]" + title + "[-]\n" +
|
||||
"\n" + frame + " " + tagline + "\n" +
|
||||
"\n[#55D7FF]" + prog,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
stopFunc := func() { close(stop) }
|
||||
return container, stopFunc
|
||||
}
|
||||
+14
-591
@@ -1,115 +1,12 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Adembc/lazyssh/internal/core/domain"
|
||||
"github.com/Adembc/lazyssh/internal/core/ports"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// splash screen with spinner; returns primitive and a stop function
|
||||
func buildSplash(app *tview.Application) (tview.Primitive, func()) {
|
||||
// Brand styling
|
||||
title := "[#FFFFFF::b]lazy[-][#55D7FF::b]ssh[-]"
|
||||
tagline := "[#AAAAAA]Fast server picker TUI • Loading your servers…[-]"
|
||||
spinnerFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
// Helpful rotating tips
|
||||
tips := []string{
|
||||
"Press [::b]/[-] to search instantly",
|
||||
"Use [::b]↑/↓[-] to navigate",
|
||||
"Hit [::b]Enter[-] to open a connection (mock)",
|
||||
"Press [::b]?[-] for help",
|
||||
"Press [::b]q[-] to quit",
|
||||
}
|
||||
|
||||
text := tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
text.SetBorder(true).
|
||||
SetTitle(" 🚀 lazyssh ").
|
||||
SetBorderColor(tcell.Color238).
|
||||
SetTitleColor(tcell.Color250)
|
||||
|
||||
// layout to center the card
|
||||
box := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(tview.NewBox(), 0, 1, false).
|
||||
AddItem(text, 9, 0, false).
|
||||
AddItem(tview.NewBox(), 0, 1, false)
|
||||
|
||||
container := tview.NewFlex().SetDirection(tview.FlexColumn).
|
||||
AddItem(tview.NewBox(), 0, 1, false).
|
||||
AddItem(box, 0, 2, false).
|
||||
AddItem(tview.NewBox(), 0, 1, false)
|
||||
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(90 * time.Millisecond):
|
||||
frame := spinnerFrames[i%len(spinnerFrames)]
|
||||
// simple progress bar animation (width 20)
|
||||
width := 20
|
||||
filled := i % (width + 1)
|
||||
prog := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
|
||||
// rotate tips every ~12 frames
|
||||
tip := tips[(i/12)%len(tips)]
|
||||
i++
|
||||
app.QueueUpdateDraw(func() {
|
||||
text.SetText(
|
||||
"\n" +
|
||||
"[::b]" + title + "[-]\n" +
|
||||
"\n" + frame + " " + tagline + "\n" +
|
||||
"\n[#55D7FF]" + prog + "[-]\n" +
|
||||
"\n[#777777]" + tip + "[-]\n" +
|
||||
"\n[#555555]v" + AppVersion + " • " + RepoURL + "[-]",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
stopFunc := func() { close(stop) }
|
||||
return container, stopFunc
|
||||
}
|
||||
|
||||
func showHelpModal(app *tview.Application, root tview.Primitive) {
|
||||
text := "Keyboard shortcuts:\n\n" +
|
||||
" ↑/↓ Navigate\n" +
|
||||
" Enter SSH connect (mock)\n" +
|
||||
" a Add server (mock)\n" +
|
||||
" e Edit server (mock)\n" +
|
||||
" d Delete entry (mock)\n" +
|
||||
" / Focus search\n" +
|
||||
" q Quit\n" +
|
||||
" ? Help\n"
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(text).
|
||||
AddButtons([]string{"Close"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
app.SetRoot(root, true)
|
||||
})
|
||||
|
||||
app.SetRoot(modal, true)
|
||||
}
|
||||
|
||||
func showConnectModal(app *tview.Application, root tview.Primitive, s domain.Server) {
|
||||
msg := fmt.Sprintf("SSH to %s (%s@%s:%d)\n\nThis is a mock action.", s.Alias, s.User, s.Host, s.Port)
|
||||
modal := tview.NewModal().
|
||||
SetText(msg).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
app.SetRoot(root, true)
|
||||
})
|
||||
app.SetRoot(modal, true)
|
||||
}
|
||||
|
||||
type tui struct {
|
||||
app *tview.Application
|
||||
serverService ports.ServerService
|
||||
@@ -137,17 +34,9 @@ func NewTUI(ss ports.ServerService) *tui {
|
||||
}
|
||||
|
||||
func (t *tui) Run() error {
|
||||
splash, stop := buildSplash(t.app)
|
||||
t.app.SetRoot(splash, true)
|
||||
time.AfterFunc(1*time.Second, func() {
|
||||
stop()
|
||||
t.app.QueueUpdateDraw(func() {
|
||||
t.app.SetRoot(t.root, true)
|
||||
})
|
||||
})
|
||||
|
||||
t.app.EnableMouse(true)
|
||||
t.initializeTheme().buildComponents().buildLayout().bindEvents().loadInitialData()
|
||||
t.initializeTheme().buildComponents().buildLayout().bindEvents().loadInitialData().loadSplashScreen()
|
||||
|
||||
return t.app.Run()
|
||||
}
|
||||
@@ -165,17 +54,15 @@ func (t *tui) initializeTheme() *tui {
|
||||
func (t *tui) buildComponents() *tui {
|
||||
t.header = NewAppHeader(AppVersion, RepoURL)
|
||||
t.searchBar = NewSearchBar().
|
||||
OnSearch(t.handleSearch).
|
||||
OnSearch(t.handleSearchInput).
|
||||
OnEscape(t.hideSearchBar)
|
||||
t.hintBar = NewHintBar()
|
||||
t.serverList = NewServerList().
|
||||
OnSelection(t.handleServerConnect).
|
||||
OnSelectionChange(t.handleServerSelectionChange)
|
||||
t.details = NewServerDetails()
|
||||
t.statusBar = NewStatusBar()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *tui) buildLayout() *tui {
|
||||
|
||||
t.left = tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
@@ -199,7 +86,6 @@ func (t *tui) bindEvents() *tui {
|
||||
t.root.SetInputCapture(t.handleGlobalKeys)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *tui) loadInitialData() *tui {
|
||||
servers, _ := t.serverService.ListServers("")
|
||||
t.serverList.UpdateServers(servers)
|
||||
@@ -207,478 +93,15 @@ func (t *tui) loadInitialData() *tui {
|
||||
return t
|
||||
}
|
||||
|
||||
//func (t *tui) Run() error {
|
||||
// // Global theme (non-invasive)
|
||||
// tview.Styles.PrimitiveBackgroundColor = tcell.Color232 // near-black
|
||||
// tview.Styles.ContrastBackgroundColor = tcell.Color235 // dark gray
|
||||
// tview.Styles.BorderColor = tcell.Color238
|
||||
// tview.Styles.TitleColor = tcell.Color250
|
||||
// tview.Styles.PrimaryTextColor = tcell.Color252
|
||||
// tview.Styles.TertiaryTextColor = tcell.Color245
|
||||
// tview.Styles.SecondaryTextColor = tcell.Color245
|
||||
// tview.Styles.GraphicsColor = tcell.Color238
|
||||
//
|
||||
// servers, err := t.serverService.ListServers()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // Sort by alias for deterministic order
|
||||
// sort.Slice(servers, func(i, j int) bool { return servers[i].Alias < servers[j].Alias })
|
||||
//
|
||||
// // Header: enhanced two-row layout (bar + separator)
|
||||
// version := AppVersion
|
||||
// repoURL := RepoURL
|
||||
//
|
||||
// // Slightly lighter than body for a distinct bar
|
||||
// headerBg := tcell.Color234 // dark gray tint
|
||||
// // Left: App icon + stylized name
|
||||
// headerLeft := tview.NewTextView().
|
||||
// SetDynamicColors(true).
|
||||
// SetTextAlign(tview.AlignLeft)
|
||||
// headerLeft.SetBackgroundColor(headerBg)
|
||||
// stylizedName := "🚀 [#FFFFFF::b]lazy[-][#55D7FF::b]ssh[-]"
|
||||
// headerLeft.SetText(stylizedName)
|
||||
//
|
||||
// // Center: badges (version + type)
|
||||
// headerCenter := tview.NewTextView().
|
||||
// SetDynamicColors(true).
|
||||
// SetTextAlign(tview.AlignCenter)
|
||||
// headerCenter.SetBackgroundColor(headerBg)
|
||||
// // Render version as a pill/badge and add a secondary badge
|
||||
// headerCenter.SetText("[black:#2ECC71::b] " + version + " [-] [black:#3B82F6::b] TUI [-]")
|
||||
//
|
||||
// // Right: repo URL + current date/time
|
||||
// headerRight := tview.NewTextView().
|
||||
// SetDynamicColors(true).
|
||||
// SetTextAlign(tview.AlignRight)
|
||||
// headerRight.SetBackgroundColor(headerBg)
|
||||
// currentTime := time.Now().Format("Mon, 02 Jan 2006 15:04")
|
||||
// headerRight.SetText("[#55AAFF::u]🔗 " + repoURL + "[-] [#AAAAAA]• " + currentTime + "[-]")
|
||||
//
|
||||
// // Row 1: the main header bar with three columns
|
||||
// headerBar := tview.NewFlex().SetDirection(tview.FlexColumn).
|
||||
// AddItem(headerLeft, 0, 1, false).
|
||||
// AddItem(headerCenter, 0, 1, false).
|
||||
// AddItem(headerRight, 0, 1, false)
|
||||
//
|
||||
// // Row 2: a thin separator line
|
||||
// separator := tview.NewTextView().SetDynamicColors(true)
|
||||
// separator.SetBackgroundColor(tcell.Color235)
|
||||
// separator.SetText("[#444444]" + strings.Repeat("─", 200) + "[-]")
|
||||
//
|
||||
// // Combine into a two-row header
|
||||
// header := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
// AddItem(headerBar, 1, 0, false).
|
||||
// AddItem(separator, 1, 0, false)
|
||||
//
|
||||
// // Tab bar removed per request
|
||||
//
|
||||
// // Left panel: Search is hidden by default; a hint bar is shown until '/' is pressed
|
||||
// search := tview.NewInputField().
|
||||
// SetLabel(" 🔍 Search: ").
|
||||
// SetFieldWidth(30)
|
||||
// // When visible, give it a border and title for better UI
|
||||
// search.SetBorder(true).SetTitle("Search")
|
||||
// search.SetBorderColor(tcell.Color238).SetTitleColor(tcell.Color250)
|
||||
// search.SetFieldBackgroundColor(tcell.Color233).SetFieldTextColor(tcell.Color252)
|
||||
//
|
||||
// serverList := tview.NewList().ShowSecondaryText(false)
|
||||
// serverList.SetBorder(true)
|
||||
// serverList.SetTitle("Servers")
|
||||
// serverList.SetBorderColor(tcell.Color238).SetTitleColor(tcell.Color250)
|
||||
// serverList.SetSelectedBackgroundColor(tcell.Color24).SetSelectedTextColor(tcell.Color255)
|
||||
// serverList.SetHighlightFullLine(true)
|
||||
//
|
||||
// // Hint bar shown when search is hidden
|
||||
// hintBar := tview.NewTextView().SetDynamicColors(true)
|
||||
// hintBar.SetBackgroundColor(tcell.Color233)
|
||||
// hintBar.SetText("[#BBBBBB]Press [::b]/[-:-:b] to search… • ↑↓ Navigate • Enter SSH • a Add • e Edit • d Delete • ? Help[-]")
|
||||
//
|
||||
// // Right panel: Details
|
||||
// details := tview.NewTextView().SetDynamicColors(true).SetWrap(true)
|
||||
// details.SetBorder(true).SetTitle("Details")
|
||||
// details.SetBorderColor(tcell.Color238).SetTitleColor(tcell.Color250)
|
||||
//
|
||||
// // Status bar
|
||||
// status := tview.NewTextView().SetDynamicColors(true)
|
||||
// statusBg := tcell.Color235
|
||||
// status.SetBackgroundColor(statusBg)
|
||||
// status.SetTextAlign(tview.AlignCenter)
|
||||
// status.SetText("[white]↑↓[-] Navigate • [white]Enter[-] SSH • [white]a[-] Add • [white]e[-] Edit • [white]d[-] Delete • [white]/[-] Search • [white]q[-] Quit • [white]?[-] Help")
|
||||
//
|
||||
// // Define updateDetails before populate; declare var for closure use
|
||||
// var updateDetails func(server domain.Server)
|
||||
// // Root container placeholder (assigned later)
|
||||
// var root *tview.Flex
|
||||
//
|
||||
// // Populate list with filter logic
|
||||
// filtered := servers
|
||||
// populate := func() {
|
||||
// serverList.Clear()
|
||||
// for i := range filtered {
|
||||
// p, s2 := formatServerLine(filtered[i])
|
||||
// idx := i
|
||||
// serverList.AddItem(p, s2, 0, func() {
|
||||
// if updateDetails != nil {
|
||||
// updateDetails(filtered[idx])
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// if serverList.GetItemCount() > 0 {
|
||||
// serverList.SetCurrentItem(0)
|
||||
// if updateDetails != nil {
|
||||
// updateDetails(filtered[0])
|
||||
// }
|
||||
// } else {
|
||||
// details.SetText("No servers match the current filter.")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Will be defined later to refresh according to current search
|
||||
// var reapplyFilter func()
|
||||
//
|
||||
// // Delete currently selected server from memory and refresh
|
||||
// deleteSelected := func() {
|
||||
// idx := serverList.GetCurrentItem()
|
||||
// if idx < 0 || idx >= len(filtered) {
|
||||
// return
|
||||
// }
|
||||
// sel := filtered[idx]
|
||||
// // remove from servers slice
|
||||
// newServers := make([]domain.Server, 0, len(servers))
|
||||
// for _, s := range servers {
|
||||
// if !(s.Alias == sel.Alias && s.Host == sel.Host) {
|
||||
// newServers = append(newServers, s)
|
||||
// }
|
||||
// }
|
||||
// servers = newServers
|
||||
// reapplyFilter()
|
||||
// }
|
||||
//
|
||||
// // Now define updateDetails
|
||||
// updateDetails = func(s domain.Server) {
|
||||
// text := fmt.Sprintf("[::b]%s[-]\n\nHost: [white]%s[-]\nUser: [white]%s[-]\nPort: [white]%d[-]\nKey: [white]%s[-]\nTags: [white]%s[-]\nStatus: %s\nLast: %s\n\n[::b]Commands:[-]\n Enter: SSH connect\n a: Add new server\n e: Edit entry\n d: Delete entry",
|
||||
// s.Alias, s.Host, s.User, s.Port, s.Key, joinTags(s.Tags), statusIcon(s.Status), s.LastSeen.Format("2006-01-02 15:04"))
|
||||
// details.SetText(text)
|
||||
// }
|
||||
//
|
||||
// // Helper to re-apply current filter and repopulate
|
||||
// reapplyFilter = func() {
|
||||
// q := strings.TrimSpace(strings.ToLower(search.GetText()))
|
||||
// if q == "" {
|
||||
// filtered = servers
|
||||
// populate()
|
||||
// return
|
||||
// }
|
||||
// var out []domain.Server
|
||||
// for _, s := range servers {
|
||||
// hay := strings.ToLower(strings.Join([]string{
|
||||
// s.Alias,
|
||||
// s.Host,
|
||||
// s.User,
|
||||
// fmt.Sprint(s.Port),
|
||||
// strings.Join(s.Tags, ","),
|
||||
// s.Key,
|
||||
// }, " "))
|
||||
// if strings.Contains(hay, q) {
|
||||
// out = append(out, s)
|
||||
// }
|
||||
// }
|
||||
// filtered = out
|
||||
// populate()
|
||||
// }
|
||||
//
|
||||
// // Add new server form
|
||||
// addServer := func() {
|
||||
// aliasField := tview.NewInputField().SetLabel("Alias: ")
|
||||
// hostField := tview.NewInputField().SetLabel("Host/IP: ")
|
||||
// userField := tview.NewInputField().SetLabel("User: ").SetText("root")
|
||||
// portField := tview.NewInputField().SetLabel("Port: ").SetText("22")
|
||||
// keyField := tview.NewInputField().SetLabel("Key: ").SetText("~/.ssh/id_ed25519")
|
||||
// tagsField := tview.NewInputField().SetLabel("Tags (comma): ")
|
||||
// statusDD := tview.NewDropDown().SetLabel("Status: ")
|
||||
// statusOptions := []string{"online", "warn", "offline"}
|
||||
// statusDD.SetOptions(statusOptions, nil)
|
||||
// statusDD.SetCurrentOption(0)
|
||||
//
|
||||
// form := tview.NewForm().
|
||||
// AddFormItem(aliasField).
|
||||
// AddFormItem(hostField).
|
||||
// AddFormItem(userField).
|
||||
// AddFormItem(portField).
|
||||
// AddFormItem(keyField).
|
||||
// AddFormItem(tagsField).
|
||||
// AddFormItem(statusDD)
|
||||
// form.SetBorder(true).SetTitle("Add Server").SetTitleAlign(tview.AlignLeft)
|
||||
// form.SetBorderColor(tcell.Color238).SetTitleColor(tcell.Color250)
|
||||
// form.AddButton("Save", func() {
|
||||
// alias := strings.TrimSpace(aliasField.GetText())
|
||||
// host := strings.TrimSpace(hostField.GetText())
|
||||
// if alias == "" || host == "" {
|
||||
// return
|
||||
// }
|
||||
// user := strings.TrimSpace(userField.GetText())
|
||||
// p := 22
|
||||
// if v := strings.TrimSpace(portField.GetText()); v != "" {
|
||||
// if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
// p = n
|
||||
// }
|
||||
// }
|
||||
// key := strings.TrimSpace(keyField.GetText())
|
||||
// tags := []string{}
|
||||
// if ts := strings.TrimSpace(tagsField.GetText()); ts != "" {
|
||||
// for _, t := range strings.Split(ts, ",") {
|
||||
// if s := strings.TrimSpace(t); s != "" {
|
||||
// tags = append(tags, s)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _, status := statusDD.GetCurrentOption()
|
||||
// newS := domain.Server{Alias: alias, Host: host, User: user, Port: p, Key: key, Tags: tags, Status: status, LastSeen: time.Now()}
|
||||
// servers = append(servers, newS)
|
||||
// sort.Slice(servers, func(i, j int) bool { return servers[i].Alias < servers[j].Alias })
|
||||
// reapplyFilter()
|
||||
// // select the new item if it is visible
|
||||
// for i := range filtered {
|
||||
// if filtered[i].Alias == newS.Alias && filtered[i].Host == newS.Host {
|
||||
// serverList.SetCurrentItem(i)
|
||||
// updateDetails(filtered[i])
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// t.app.SetRoot(root, true)
|
||||
// })
|
||||
// form.AddButton("Cancel", func() { t.app.SetRoot(root, true) })
|
||||
// form.SetCancelFunc(func() { t.app.SetRoot(root, true) })
|
||||
// t.app.SetRoot(form, true)
|
||||
// }
|
||||
//
|
||||
// // Edit currently selected server
|
||||
// editSelected := func() {
|
||||
// idx := serverList.GetCurrentItem()
|
||||
// if idx < 0 || idx >= len(filtered) {
|
||||
// return
|
||||
// }
|
||||
// orig := filtered[idx]
|
||||
// aliasField := tview.NewInputField().SetLabel("Alias: ").SetText(orig.Alias)
|
||||
// hostField := tview.NewInputField().SetLabel("Host/IP: ").SetText(orig.Host)
|
||||
// userField := tview.NewInputField().SetLabel("User: ").SetText(orig.User)
|
||||
// portField := tview.NewInputField().SetLabel("Port: ").SetText(fmt.Sprint(orig.Port))
|
||||
// keyField := tview.NewInputField().SetLabel("Key: ").SetText(orig.Key)
|
||||
// tagsField := tview.NewInputField().SetLabel("Tags (comma): ").SetText(strings.Join(orig.Tags, ", "))
|
||||
// statusDD := tview.NewDropDown().SetLabel("Status: ")
|
||||
// statusOptions := []string{"online", "warn", "offline"}
|
||||
// statusDD.SetOptions(statusOptions, nil)
|
||||
// // set current option to existing status
|
||||
// curIdx := 0
|
||||
// for i, o := range statusOptions {
|
||||
// if o == orig.Status {
|
||||
// curIdx = i
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// statusDD.SetCurrentOption(curIdx)
|
||||
//
|
||||
// form := tview.NewForm().
|
||||
// AddFormItem(aliasField).
|
||||
// AddFormItem(hostField).
|
||||
// AddFormItem(userField).
|
||||
// AddFormItem(portField).
|
||||
// AddFormItem(keyField).
|
||||
// AddFormItem(tagsField).
|
||||
// AddFormItem(statusDD)
|
||||
// form.SetBorder(true).SetTitle("Edit Server").SetTitleAlign(tview.AlignLeft)
|
||||
// form.SetBorderColor(tcell.Color238).SetTitleColor(tcell.Color250)
|
||||
// form.AddButton("Save", func() {
|
||||
// alias := strings.TrimSpace(aliasField.GetText())
|
||||
// host := strings.TrimSpace(hostField.GetText())
|
||||
// if alias == "" || host == "" {
|
||||
// return
|
||||
// }
|
||||
// user := strings.TrimSpace(userField.GetText())
|
||||
// p := orig.Port
|
||||
// if v := strings.TrimSpace(portField.GetText()); v != "" {
|
||||
// if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
// p = n
|
||||
// }
|
||||
// }
|
||||
// key := strings.TrimSpace(keyField.GetText())
|
||||
// tags := []string{}
|
||||
// if ts := strings.TrimSpace(tagsField.GetText()); ts != "" {
|
||||
// for _, t := range strings.Split(ts, ",") {
|
||||
// if s := strings.TrimSpace(t); s != "" {
|
||||
// tags = append(tags, s)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _, status := statusDD.GetCurrentOption()
|
||||
// updated := domain.Server{Alias: alias, Host: host, User: user, Port: p, Key: key, Tags: tags, Status: status, LastSeen: orig.LastSeen}
|
||||
// // replace matching item in servers by original key alias+host
|
||||
// found := false
|
||||
// for i := range servers {
|
||||
// if servers[i].Alias == orig.Alias && servers[i].Host == orig.Host {
|
||||
// servers[i] = updated
|
||||
// found = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if !found {
|
||||
// servers = append(servers, updated)
|
||||
// }
|
||||
// sort.Slice(servers, func(i, j int) bool { return servers[i].Alias < servers[j].Alias })
|
||||
// reapplyFilter()
|
||||
// // select the updated item if visible
|
||||
// for i := range filtered {
|
||||
// if filtered[i].Alias == updated.Alias && filtered[i].Host == updated.Host {
|
||||
// serverList.SetCurrentItem(i)
|
||||
// updateDetails(filtered[i])
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// t.app.SetRoot(root, true)
|
||||
// })
|
||||
// form.AddButton("Cancel", func() { t.app.SetRoot(root, true) })
|
||||
// form.SetCancelFunc(func() { t.app.SetRoot(root, true) })
|
||||
// t.app.SetRoot(form, true)
|
||||
// }
|
||||
//
|
||||
// filter := func(q string) {
|
||||
// q = strings.TrimSpace(strings.ToLower(q))
|
||||
// if q == "" {
|
||||
// filtered = servers
|
||||
// populate()
|
||||
// return
|
||||
// }
|
||||
// var out []domain.Server
|
||||
// for _, s := range servers {
|
||||
// hay := strings.ToLower(strings.Join([]string{
|
||||
// s.Alias,
|
||||
// s.Host,
|
||||
// s.User,
|
||||
// fmt.Sprint(s.Port),
|
||||
// strings.Join(s.Tags, ","),
|
||||
// s.Key,
|
||||
// }, " "))
|
||||
// if strings.Contains(hay, q) {
|
||||
// out = append(out, s)
|
||||
// }
|
||||
// }
|
||||
// filtered = out
|
||||
// populate()
|
||||
// }
|
||||
//
|
||||
// // Declare togglers; define after left is created
|
||||
// var showSearchBar func()
|
||||
// var hideSearchBar func()
|
||||
//
|
||||
// search.SetChangedFunc(func(text string) { filter(text) })
|
||||
// search.SetDoneFunc(func(key tcell.Key) {
|
||||
// // Return focus to list on Escape or Enter and hide the search bar
|
||||
// if key == tcell.KeyEsc || key == tcell.KeyEnter {
|
||||
// if hideSearchBar != nil {
|
||||
// hideSearchBar()
|
||||
// } else {
|
||||
// t.app.SetFocus(serverList)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Initial populate
|
||||
// populate()
|
||||
//
|
||||
// // Layout main content area
|
||||
// var left *tview.Flex
|
||||
// left = tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
// AddItem(hintBar, 1, 0, false).
|
||||
// AddItem(serverList, 0, 1, true)
|
||||
//
|
||||
// right := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
// AddItem(details, 0, 1, false)
|
||||
//
|
||||
// content := tview.NewFlex().SetDirection(tview.FlexColumn).
|
||||
// AddItem(left, 0, 3, true).
|
||||
// AddItem(right, 0, 2, false)
|
||||
//
|
||||
// // Define togglers now that 'left' exists
|
||||
// showSearchBar = func() {
|
||||
// left.Clear()
|
||||
// left.AddItem(search, 3, 0, true)
|
||||
// left.AddItem(serverList, 0, 1, false)
|
||||
// t.app.SetFocus(search)
|
||||
// }
|
||||
// hideSearchBar = func() {
|
||||
// left.Clear()
|
||||
// left.AddItem(hintBar, 1, 0, false)
|
||||
// left.AddItem(serverList, 0, 1, true)
|
||||
// t.app.SetFocus(serverList)
|
||||
// }
|
||||
//
|
||||
// // Root layout: header, content, status
|
||||
// root = tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
// AddItem(header, 2, 0, false).
|
||||
// AddItem(content, 0, 1, true).
|
||||
// AddItem(status, 1, 0, false)
|
||||
//
|
||||
// // Keybindings
|
||||
// root.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
// // If the search input has focus, do not trigger global shortcuts.
|
||||
// if t.app.GetFocus() == search {
|
||||
// return event
|
||||
// }
|
||||
// switch event.Rune() {
|
||||
// case 'q':
|
||||
// t.app.Stop()
|
||||
// return nil
|
||||
// case '/':
|
||||
// if showSearchBar != nil {
|
||||
// showSearchBar()
|
||||
// } else {
|
||||
// t.app.SetFocus(search)
|
||||
// }
|
||||
// return nil
|
||||
// case 'a':
|
||||
// addServer()
|
||||
// return nil
|
||||
// case 'e':
|
||||
// editSelected()
|
||||
// return nil
|
||||
// case 'd':
|
||||
// deleteSelected()
|
||||
// return nil
|
||||
// case '?':
|
||||
// showHelpModal(t.app, root)
|
||||
// return nil
|
||||
// }
|
||||
// // Handle Enter at root level as connect for current selection
|
||||
// if event.Key() == tcell.KeyEnter {
|
||||
// idx := serverList.GetCurrentItem()
|
||||
// if idx >= 0 && idx < len(filtered) {
|
||||
// showConnectModal(t.app, root, filtered[idx])
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// return event
|
||||
// })
|
||||
//
|
||||
// // Also handle list-selected changes to update details
|
||||
// serverList.SetChangedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||
// if index >= 0 && index < len(filtered) {
|
||||
// updateDetails(filtered[index])
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Always show the splash screen for 1 second on startup
|
||||
// splash, stop := buildSplash(t.app)
|
||||
// t.app.SetRoot(splash, true)
|
||||
// time.AfterFunc(1*time.Second, func() {
|
||||
// stop()
|
||||
// t.app.QueueUpdateDraw(func() {
|
||||
// t.app.SetRoot(root, true)
|
||||
// })
|
||||
// })
|
||||
// t.app.EnableMouse(true)
|
||||
// if err := t.app.Run(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
func (t *tui) loadSplashScreen() *tui {
|
||||
splash, stop := buildSplash(t.app)
|
||||
t.app.SetRoot(splash, true)
|
||||
time.AfterFunc(SplashScreenDuration, func() {
|
||||
stop()
|
||||
t.app.QueueUpdateDraw(func() {
|
||||
t.app.SetRoot(t.root, true)
|
||||
})
|
||||
})
|
||||
return t
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user