Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 90 additions & 4 deletions cmd/app/input_components.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import (

// InputComponentState manages interactive input components
type InputComponentState struct {
searchInput textinput.Model
commandInput textinput.Model
searchInput textinput.Model
commandInput textinput.Model
usernameInput textinput.Model
passwordInput textinput.Model
}

// NewInputComponents creates a new input component state
Expand All @@ -34,9 +36,25 @@ func NewInputComponents() *InputComponentState {
commandInput.CharLimit = 200
commandInput.SetWidth(50)

// Create username input for login
usernameInput := textinput.New()
usernameInput.Placeholder = "Username"
usernameInput.CharLimit = 100
usernameInput.SetWidth(30)

// Create password input for login (masked)
passwordInput := textinput.New()
passwordInput.Placeholder = "Password"
passwordInput.CharLimit = 100
passwordInput.SetWidth(30)
passwordInput.EchoMode = textinput.EchoPassword
passwordInput.EchoCharacter = '•'

return &InputComponentState{
searchInput: searchInput,
commandInput: commandInput,
searchInput: searchInput,
commandInput: commandInput,
usernameInput: usernameInput,
passwordInput: passwordInput,
}
}

Expand Down Expand Up @@ -1256,6 +1274,74 @@ func (m *Model) handleSortCommand(arg string) (*Model, tea.Cmd) {
}
}

// Login input methods

// UpdateUsernameInput updates the username textinput component
func (ic *InputComponentState) UpdateUsernameInput(msg tea.Msg) tea.Cmd {
var cmd tea.Cmd
ic.usernameInput, cmd = ic.usernameInput.Update(msg)
return cmd
}

// UpdatePasswordInput updates the password textinput component
func (ic *InputComponentState) UpdatePasswordInput(msg tea.Msg) tea.Cmd {
var cmd tea.Cmd
ic.passwordInput, cmd = ic.passwordInput.Update(msg)
return cmd
}

// FocusUsernameInput focuses the username input
func (ic *InputComponentState) FocusUsernameInput() {
ic.usernameInput.Focus()
}

// FocusPasswordInput focuses the password input
func (ic *InputComponentState) FocusPasswordInput() {
ic.passwordInput.Focus()
}

// BlurLoginInputs removes focus from login inputs
func (ic *InputComponentState) BlurLoginInputs() {
ic.usernameInput.Blur()
ic.passwordInput.Blur()
}

// GetUsernameValue returns current username input value
func (ic *InputComponentState) GetUsernameValue() string {
return ic.usernameInput.Value()
}

// GetPasswordValue returns current password input value
func (ic *InputComponentState) GetPasswordValue() string {
return ic.passwordInput.Value()
}

// SetUsernameValue sets the username input value
func (ic *InputComponentState) SetUsernameValue(value string) {
ic.usernameInput.SetValue(value)
}

// SetPasswordValue sets the password input value
func (ic *InputComponentState) SetPasswordValue(value string) {
ic.passwordInput.SetValue(value)
}

// ClearLoginInputs clears both login inputs
func (ic *InputComponentState) ClearLoginInputs() {
ic.usernameInput.SetValue("")
ic.passwordInput.SetValue("")
}

// RenderUsernameInput renders the username input view
func (ic *InputComponentState) RenderUsernameInput() string {
return ic.usernameInput.View()
}

// RenderPasswordInput renders the password input view
func (ic *InputComponentState) RenderPasswordInput() string {
return ic.passwordInput.View()
}

// local helpers
func maxInt(a, b int) int {
if a > b {
Expand Down
112 changes: 112 additions & 0 deletions cmd/app/input_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,116 @@ func (m *Model) executeResourceSync() (tea.Model, tea.Cmd) {
)
}

// handleLoginModeKeys handles input when in login mode
func (m *Model) handleLoginModeKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// If loading, only allow cancel
if m.state.Modals.LoginLoading {
switch msg.String() {
case "esc", "ctrl+c":
m.state.Modals.LoginLoading = false
return m, nil
}
return m, nil
}

switch msg.String() {
case "tab", "down":
// Cycle through fields: username(0) -> password(1) -> save(2) -> login(3) -> cancel(4)
m.state.Modals.LoginFieldFocus = (m.state.Modals.LoginFieldFocus + 1) % 5
return m.updateLoginFocus()
case "shift+tab", "up":
// Reverse cycle
m.state.Modals.LoginFieldFocus = (m.state.Modals.LoginFieldFocus + 4) % 5
return m.updateLoginFocus()
case "enter":
switch m.state.Modals.LoginFieldFocus {
case 0: // Username field - move to password
m.state.Modals.LoginFieldFocus = 1
return m.updateLoginFocus()
case 1: // Password field - move to save checkbox
m.state.Modals.LoginFieldFocus = 2
return m.updateLoginFocus()
case 2: // Save checkbox - toggle and move to login button
m.state.Modals.LoginSaveCredentials = !m.state.Modals.LoginSaveCredentials
m.state.Modals.LoginFieldFocus = 3
return m.updateLoginFocus()
case 3: // Login button - submit
return m.submitLogin()
case 4: // Cancel button - cancel login
return m.cancelLogin()
}
return m, nil
case " ":
// Space toggles checkbox when focused
if m.state.Modals.LoginFieldFocus == 2 {
m.state.Modals.LoginSaveCredentials = !m.state.Modals.LoginSaveCredentials
}
return m, nil
case "esc":
return m.cancelLogin()
case "ctrl+c":
return m.cancelLogin()
default:
// Pass to focused input
switch m.state.Modals.LoginFieldFocus {
case 0:
cmd := m.inputComponents.UpdateUsernameInput(msg)
m.state.Modals.LoginUsername = m.inputComponents.GetUsernameValue()
return m, cmd
case 1:
cmd := m.inputComponents.UpdatePasswordInput(msg)
return m, cmd
}
return m, nil
}
}

// updateLoginFocus updates which login input is focused
func (m *Model) updateLoginFocus() (tea.Model, tea.Cmd) {
m.inputComponents.BlurLoginInputs()
switch m.state.Modals.LoginFieldFocus {
case 0:
m.inputComponents.FocusUsernameInput()
case 1:
m.inputComponents.FocusPasswordInput()
}
return m, nil
}

// submitLogin handles login form submission
func (m *Model) submitLogin() (tea.Model, tea.Cmd) {
username := m.inputComponents.GetUsernameValue()
password := m.inputComponents.GetPasswordValue()

if username == "" || password == "" {
errMsg := "Username and password are required"
m.state.Modals.LoginError = &errMsg
return m, nil
}

// Clear error and start loading
m.state.Modals.LoginError = nil
m.state.Modals.LoginLoading = true
m.inputComponents.BlurLoginInputs()

// Send login submit message
return m, func() tea.Msg {
return model.LoginSubmitMsg{
Username: username,
Password: password,
SaveCredentials: m.state.Modals.LoginSaveCredentials,
}
}
}

// cancelLogin handles login cancellation
func (m *Model) cancelLogin() (tea.Model, tea.Cmd) {
m.inputComponents.BlurLoginInputs()
m.inputComponents.ClearLoginInputs()
m.state.Mode = model.ModeAuthRequired
return m, nil
}

// handleAuthRequiredModeKeys handles input when authentication is required
func (m *Model) handleAuthRequiredModeKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
Expand Down Expand Up @@ -1384,6 +1494,8 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m.handleK9sContextSelectKeys(msg)
case model.ModeK9sError:
return m.handleK9sErrorModeKeys(msg)
case model.ModeLogin, model.ModeLoginLoading:
return m.handleLoginModeKeys(msg)
}

// Tree view keys when in normal mode.
Expand Down
29 changes: 29 additions & 0 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ func (e *PortForwardModeError) Error() string {
return "ArgoCD is configured for port-forward mode"
}

// NoTokenError indicates that no auth token was found but server URL is available
type NoTokenError struct {
ServerURL string
Insecure bool
}

func (e *NoTokenError) Error() string {
return "no auth token found"
}

// appVersion is the Argonaut version shown in the ASCII banner.
// Override at build time: go build -ldflags "-X main.appVersion=1.16.0"
var appVersion = "dev"
Expand Down Expand Up @@ -323,6 +333,16 @@ func main() {
// Set mode to show core detection view
m.state.Mode = model.ModeCoreDetected
m.state.Server = nil
} else if noTokenErr, isNoToken := err.(*NoTokenError); isNoToken {
// No token found - set up for auto-login
cblog.With("component", "app").Info("No auth token found, will attempt auto-login", "serverURL", noTokenErr.ServerURL)
m.state.Server = &model.Server{
BaseURL: noTokenErr.ServerURL,
Insecure: noTokenErr.Insecure,
}
// Initialize login modal state with server URL
m.initLoginModal(noTokenErr.ServerURL, noTokenErr.Insecure)
// Mode will be set by validateAuthentication() based on auto-login result
} else {
cblog.With("component", "app").Error("Could not load Argo CD config", "err", err)
cblog.With("component", "app").Info("Please run 'argocd login' to configure and authenticate")
Expand Down Expand Up @@ -428,6 +448,15 @@ func loadArgoConfig(overridePath string) (*model.Server, error) {
return nil, &CoreModeError{}
}
}

// Check if it's a missing token error - return NoTokenError with server URL for login
if strings.Contains(err.Error(), "auth token") || strings.Contains(err.Error(), "no token") || strings.Contains(err.Error(), "argocd login") {
// Try to get server URL without token for login flow
if serverURL, insecure, urlErr := cfg.GetServerURLForCurrentContext(); urlErr == nil {
return nil, &NoTokenError{ServerURL: serverURL, Insecure: insecure}
}
}

return nil, fmt.Errorf("failed to parse server config: %w", err)
}

Expand Down
26 changes: 26 additions & 0 deletions cmd/app/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,32 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

return m, tea.Batch(func() tea.Msg { return model.SetModeMsg{Mode: model.ModeAuthRequired} })

// Login messages
case model.AutoLoginAttemptMsg:
// Set mode to show loading while attempting auto-login
m.state.Mode = model.ModeLoginLoading
m.state.Modals.LoginServerURL = msg.ServerURL
m.state.Modals.InitialLoading = false
return m, m.handleAutoLoginAttempt(msg)

case model.AutoLoginResultMsg:
return m.handleAutoLoginResult(msg)

case model.LoginSubmitMsg:
return m, m.handleLoginSubmit(msg)

case model.LoginSuccessMsg:
return m.handleLoginSuccess(msg)

case model.LoginErrorMsg:
return m.handleLoginError(msg)

case model.LoginCancelMsg:
m.inputComponents.BlurLoginInputs()
m.inputComponents.ClearLoginInputs()
m.state.Mode = model.ModeAuthRequired
return m, nil

// Navigation update messages
case model.NavigationUpdateMsg:
if msg.NewView != nil {
Expand Down
Loading
Loading