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 }