206 lines
5.5 KiB
Go
206 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// ============================================================================
|
|
// STYLING DEFINITIONS (Lipgloss)
|
|
// ============================================================================
|
|
var (
|
|
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
|
|
blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
|
cursorStyle = focusedStyle.Copy()
|
|
noStyle = lipgloss.NewStyle()
|
|
|
|
// A nice box around the login screen
|
|
dialogBoxStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("87")).
|
|
Padding(1, 4).
|
|
MarginTop(1).
|
|
MarginLeft(2)
|
|
)
|
|
|
|
// ============================================================================
|
|
// 1. THE MODEL
|
|
// ============================================================================
|
|
type loginModel struct {
|
|
focusIndex int
|
|
inputs []textinput.Model
|
|
cursorMode textinput.CursorMode
|
|
|
|
// Flags to return back to the main application
|
|
isSubmitted bool
|
|
isCanceled bool
|
|
errorMessage string
|
|
}
|
|
|
|
func initialModell() loginModel {
|
|
m := loginModel{
|
|
inputs: make([]textinput.Model, 2),
|
|
}
|
|
|
|
var t textinput.Model
|
|
for i := range m.inputs {
|
|
t = textinput.New()
|
|
t.Cursor.Style = cursorStyle
|
|
t.CharLimit = 32
|
|
|
|
switch i {
|
|
case 0:
|
|
t.Placeholder = "Username"
|
|
t.Focus()
|
|
t.PromptStyle = focusedStyle
|
|
t.TextStyle = focusedStyle
|
|
case 1:
|
|
t.Placeholder = "Password"
|
|
t.EchoMode = textinput.EchoPassword // Masks the password with *
|
|
t.EchoCharacter = '•'
|
|
}
|
|
|
|
m.inputs[i] = t
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// ============================================================================
|
|
// 2. THE INIT FUNCTION (Runs once on startup)
|
|
// ============================================================================
|
|
func (m loginModel) Init() tea.Cmd {
|
|
return textinput.Blink
|
|
}
|
|
|
|
// ============================================================================
|
|
// 3. THE UPDATE FUNCTION (Handles keypresses)
|
|
// ============================================================================
|
|
func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "ctrl+c", "esc":
|
|
m.isCanceled = true
|
|
return m, tea.Quit
|
|
|
|
// Handle switching between username and password fields
|
|
case "tab", "shift+tab", "enter", "up", "down":
|
|
s := msg.String()
|
|
|
|
// If they press Enter on the password field, submit the form
|
|
if s == "enter" && m.focusIndex == len(m.inputs)-1 {
|
|
m.isSubmitted = true
|
|
return m, tea.Quit
|
|
}
|
|
|
|
// Cycle through inputs
|
|
if s == "up" || s == "shift+tab" {
|
|
m.focusIndex--
|
|
} else {
|
|
m.focusIndex++
|
|
}
|
|
|
|
if m.focusIndex > len(m.inputs)-1 {
|
|
m.focusIndex = 0
|
|
} else if m.focusIndex < 0 {
|
|
m.focusIndex = len(m.inputs) - 1
|
|
}
|
|
|
|
// Apply focus states
|
|
cmds := make([]tea.Cmd, len(m.inputs))
|
|
for i := 0; i <= len(m.inputs)-1; i++ {
|
|
if i == m.focusIndex {
|
|
cmds[i] = m.inputs[i].Focus()
|
|
m.inputs[i].PromptStyle = focusedStyle
|
|
m.inputs[i].TextStyle = focusedStyle
|
|
continue
|
|
}
|
|
m.inputs[i].Blur()
|
|
m.inputs[i].PromptStyle = noStyle
|
|
m.inputs[i].TextStyle = noStyle
|
|
}
|
|
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
}
|
|
|
|
// Handle character inputs and blinking
|
|
cmd := m.updateInputs(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
func (m *loginModel) updateInputs(msg tea.Msg) tea.Cmd {
|
|
cmds := make([]tea.Cmd, len(m.inputs))
|
|
for i := range m.inputs {
|
|
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
|
}
|
|
return tea.Batch(cmds...)
|
|
}
|
|
|
|
// ============================================================================
|
|
// 4. THE VIEW FUNCTION (Renders the UI)
|
|
// ============================================================================
|
|
func (m loginModel) View() string {
|
|
// Build the UI string
|
|
var b strings.Builder
|
|
|
|
b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("78")).Render("✈️ SKYRAMA TERMINAL LOGIN\n\n"))
|
|
|
|
for i := range m.inputs {
|
|
b.WriteString(m.inputs[i].View())
|
|
if i < len(m.inputs)-1 {
|
|
b.WriteRune('\n')
|
|
}
|
|
}
|
|
|
|
button := &lipgloss.Style{}
|
|
if m.focusIndex == len(m.inputs) {
|
|
button = &focusedStyle
|
|
} else {
|
|
button = &blurredStyle
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
if m.errorMessage != "" {
|
|
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render("⚠️ " + m.errorMessage + "\n"))
|
|
} else {
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString(button.Render("[ Submit ]"))
|
|
b.WriteString(blurredStyle.Render("\n\n(esc to quit)"))
|
|
|
|
// Wrap the whole thing in our dialog box
|
|
return dialogBoxStyle.Render(b.String()) + "\n"
|
|
}
|
|
|
|
// ============================================================================
|
|
// THE WRAPPER FUNCTION (Call this from your app)
|
|
// ============================================================================
|
|
// RunLoginScreen starts the TUI and returns the username, password, and a boolean
|
|
// indicating if the user completed the form (true) or pressed escape (false).
|
|
func RunLoginScreen() (username string, password string, submitted bool) {
|
|
|
|
p := tea.NewProgram(initialModell())
|
|
|
|
// Run the Bubble Tea program synchronously
|
|
finalModel, err := p.Run()
|
|
if err != nil {
|
|
fmt.Printf("Error running login screen: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Cast the returned tea.Model back into our specific loginModel
|
|
if m, ok := finalModel.(loginModel); ok {
|
|
if m.isSubmitted {
|
|
return m.inputs[0].Value(), m.inputs[1].Value(), true
|
|
}
|
|
}
|
|
|
|
return "", "", false
|
|
}
|