mirror of
https://github.com/ollama/ollama.git
synced 2026-03-27 02:58:43 +07:00
Defensively handle environments without a display server to ensure signin remains usable on headless VMs and SSH sessions. - Skip calling xdg-open when neither DISPLAY nor WAYLAND_DISPLAY is set, preventing silent failures or unexpected browser handlers - Render the signin URL as plain text instead of wrapping it in OSC 8 hyperlink escape sequences, which can be garbled or hidden by terminals that don't support them
147 lines
3.2 KiB
Go
147 lines
3.2 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/ollama/ollama/api"
|
|
"github.com/ollama/ollama/cmd/launch"
|
|
)
|
|
|
|
type signInTickMsg struct{}
|
|
|
|
type signInCheckMsg struct {
|
|
signedIn bool
|
|
userName string
|
|
}
|
|
|
|
type signInModel struct {
|
|
modelName string
|
|
signInURL string
|
|
spinner int
|
|
width int
|
|
userName string
|
|
cancelled bool
|
|
}
|
|
|
|
func (m signInModel) Init() tea.Cmd {
|
|
return tea.Tick(200*time.Millisecond, func(t time.Time) tea.Msg {
|
|
return signInTickMsg{}
|
|
})
|
|
}
|
|
|
|
func (m signInModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.WindowSizeMsg:
|
|
wasSet := m.width > 0
|
|
m.width = msg.Width
|
|
if wasSet {
|
|
return m, tea.EnterAltScreen
|
|
}
|
|
return m, nil
|
|
|
|
case tea.KeyMsg:
|
|
switch msg.Type {
|
|
case tea.KeyCtrlC, tea.KeyEsc:
|
|
m.cancelled = true
|
|
return m, tea.Quit
|
|
}
|
|
|
|
case signInTickMsg:
|
|
m.spinner++
|
|
if m.spinner%5 == 0 {
|
|
return m, tea.Batch(
|
|
tea.Tick(200*time.Millisecond, func(t time.Time) tea.Msg {
|
|
return signInTickMsg{}
|
|
}),
|
|
checkSignIn,
|
|
)
|
|
}
|
|
return m, tea.Tick(200*time.Millisecond, func(t time.Time) tea.Msg {
|
|
return signInTickMsg{}
|
|
})
|
|
|
|
case signInCheckMsg:
|
|
if msg.signedIn {
|
|
m.userName = msg.userName
|
|
return m, tea.Quit
|
|
}
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (m signInModel) View() string {
|
|
if m.userName != "" {
|
|
return ""
|
|
}
|
|
return renderSignIn(m.modelName, m.signInURL, m.spinner, m.width)
|
|
}
|
|
|
|
func renderSignIn(modelName, signInURL string, spinner, width int) string {
|
|
spinnerFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
|
frame := spinnerFrames[spinner%len(spinnerFrames)]
|
|
|
|
urlColor := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("117"))
|
|
urlWrap := lipgloss.NewStyle().PaddingLeft(2)
|
|
if width > 4 {
|
|
urlWrap = urlWrap.Width(width - 4)
|
|
}
|
|
|
|
var s strings.Builder
|
|
|
|
fmt.Fprintf(&s, "To use %s, please sign in.\n\n", selectorSelectedItemStyle.Render(modelName))
|
|
|
|
s.WriteString("Navigate to:\n")
|
|
s.WriteString(urlWrap.Render(urlColor.Render(signInURL)))
|
|
s.WriteString("\n\n")
|
|
|
|
s.WriteString(lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "242", Dark: "246"}).Render(
|
|
frame + " Waiting for sign in to complete..."))
|
|
s.WriteString("\n\n")
|
|
|
|
s.WriteString(selectorHelpStyle.Render("esc cancel"))
|
|
|
|
return lipgloss.NewStyle().PaddingLeft(2).Render(s.String())
|
|
}
|
|
|
|
func checkSignIn() tea.Msg {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return signInCheckMsg{signedIn: false}
|
|
}
|
|
user, err := client.Whoami(context.Background())
|
|
if err == nil && user != nil && user.Name != "" {
|
|
return signInCheckMsg{signedIn: true, userName: user.Name}
|
|
}
|
|
return signInCheckMsg{signedIn: false}
|
|
}
|
|
|
|
// RunSignIn shows a bubbletea sign-in dialog and polls until the user signs in or cancels.
|
|
func RunSignIn(modelName, signInURL string) (string, error) {
|
|
launch.OpenBrowser(signInURL)
|
|
|
|
m := signInModel{
|
|
modelName: modelName,
|
|
signInURL: signInURL,
|
|
}
|
|
|
|
p := tea.NewProgram(m)
|
|
finalModel, err := p.Run()
|
|
if err != nil {
|
|
return "", fmt.Errorf("error running sign-in: %w", err)
|
|
}
|
|
|
|
fm := finalModel.(signInModel)
|
|
if fm.cancelled {
|
|
return "", ErrCancelled
|
|
}
|
|
|
|
return fm.userName, nil
|
|
}
|