package views import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "net/http" "os" "github.com/boozec/rahanna/network" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var chess string = ` A B C D E F G H +---------------+ 8 |♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜| 8 7 |♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟| 7 6 |. . . . . . . .| 6 5 |. . . . . . . .| 5 4 |. . . . . . . .| 4 3 |. . . . . . . .| 3 2 |♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙| 2 1 |♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖| 1 +---------------+ A B C D E F G H ` type playKeyMap struct { EnterNewPlay key.Binding StartNewPlay key.Binding GoLogout key.Binding Quit key.Binding } type playResponse struct { Name string `json:"name"` Error string `json:"error"` } var defaultPlayKeyMap = playKeyMap{ EnterNewPlay: key.NewBinding( key.WithKeys("alt+E", "alt+e"), key.WithHelp("Alt+E", "Enter a play using code"), ), StartNewPlay: key.NewBinding( key.WithKeys("alt+s", "alt+s"), key.WithHelp("Alt+S", "Start a new play"), ), GoLogout: key.NewBinding( key.WithKeys("alt+Q", "alt+q"), key.WithHelp("Alt+Q", "Logout"), ), Quit: key.NewBinding( key.WithKeys("Q", "q"), key.WithHelp(" Q", "Quit"), ), } type PlayModelPage int const ( LandingPage PlayModelPage = iota InsertCodePage StartPlayPage ) type PlayModel struct { width int height int err error keys playKeyMap namePrompt textinput.Model page PlayModelPage isLoading bool playName string } func NewPlayModel(width, height int) PlayModel { namePrompt := textinput.New() namePrompt.Prompt = " " namePrompt.TextStyle = inputStyle namePrompt.Placeholder = "rectangular-lake" namePrompt.Focus() namePrompt.CharLimit = 23 namePrompt.Width = 23 return PlayModel{ width: width, height: height, err: nil, keys: defaultPlayKeyMap, namePrompt: namePrompt, page: LandingPage, isLoading: false, playName: "", } } func (m PlayModel) Init() tea.Cmd { ClearScreen() return textinput.Blink } func (m PlayModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if exit := handleExit(msg); exit != nil { return m, exit } switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height return m, nil case tea.KeyMsg: switch { case key.Matches(msg, m.keys.EnterNewPlay): m.page = InsertCodePage return m, nil case key.Matches(msg, m.keys.StartNewPlay): m.page = StartPlayPage if !m.isLoading { m.isLoading = true return m, m.newPlayCallback() } case key.Matches(msg, m.keys.GoLogout): if err := os.Remove(".rahannarc"); err != nil { m.err = err return m, nil } return m, SwitchModelCmd(NewAuthModel(m.width, m.height+1)) case key.Matches(msg, m.keys.Quit): return m, tea.Quit case msg.Type == tea.KeyEnter: if m.page == InsertCodePage { m.err = errors.New("Can't join for now...") } } case playResponse: m.isLoading = false if msg.Error != "" { m.err = fmt.Errorf(msg.Error) } else { m.playName = msg.Name } return m, nil case error: m.isLoading = false m.err = msg } var cmd tea.Cmd = nil if m.page == InsertCodePage { m.namePrompt, cmd = m.namePrompt.Update(msg) } return m, tea.Batch(cmd) } func (m PlayModel) View() string { formWidth := getFormWidth(m.width) // Error message formError := "" if m.err != nil { formError = fmt.Sprintf("Error: %v", m.err.Error()) } var content string switch m.page { case LandingPage: content = chess m.namePrompt.Blur() case InsertCodePage: m.namePrompt.Focus() content = m.namePrompt.View() statusMsg := fmt.Sprintf("Press %s to join", lipgloss.NewStyle().Italic(true).Render("Enter")) if m.isLoading { statusMsg = "Loading..." } content = lipgloss.NewStyle(). Align(lipgloss.Center). Width(m.width). Render( lipgloss.JoinVertical(lipgloss.Left, lipgloss.NewStyle().Width(23).Render("Insert play code:"), m.namePrompt.View(), lipgloss.NewStyle(). Align(lipgloss.Center). PaddingTop(2). Width(23). Bold(true). Render(statusMsg), ), ) case StartPlayPage: statusMsg := fmt.Sprintf("Share `%s` to your friend", lipgloss.NewStyle().Italic(true).Foreground(lipgloss.Color("#F39C12")).Render(m.playName)) if m.isLoading { statusMsg = "Loading..." } content = lipgloss.NewStyle(). Align(lipgloss.Center). Width(m.width). Render(statusMsg) } windowContent := lipgloss.JoinVertical( lipgloss.Center, windowStyle. Width(formWidth). Render(lipgloss.JoinVertical( lipgloss.Center, errorStyle.Align(lipgloss.Center).Width(formWidth-4).Render(formError), content, )), ) enterKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.EnterNewPlay.Help().Key), m.keys.EnterNewPlay.Help().Desc) startKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.StartNewPlay.Help().Key), m.keys.StartNewPlay.Help().Desc) logoutKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.GoLogout.Help().Key), m.keys.GoLogout.Help().Desc) quitKey := fmt.Sprintf("%s %s", altCodeStyle.Render(m.keys.Quit.Help().Key), m.keys.Quit.Help().Desc) // Vertically align the buttons buttons := lipgloss.JoinVertical( lipgloss.Left, enterKey, startKey, logoutKey, quitKey, ) centeredContent := lipgloss.JoinVertical( lipgloss.Center, getLogo(m.width), windowContent, lipgloss.NewStyle().MarginTop(2).Render(buttons), ) return lipgloss.Place( m.width, m.height, lipgloss.Center, lipgloss.Center, centeredContent, ) } func (m PlayModel) newPlayCallback() tea.Cmd { return func() tea.Msg { f, err := os.Open(".rahannarc") if err != nil { return playResponse{Error: err.Error()} } defer f.Close() scanner := bufio.NewScanner(f) var authorization string for scanner.Scan() { authorization = scanner.Text() } if err := scanner.Err(); err != nil { fmt.Println("Error during scanning:", err) } url := os.Getenv("API_BASE") + "/play" payload, err := json.Marshal(map[string]string{ "ip": network.GetOutboundIP().String(), }) if err != nil { return playResponse{Error: err.Error()} } req, err := http.NewRequest("POST", url, bytes.NewReader(payload)) if err != nil { return playResponse{Error: err.Error()} } req.Header.Set("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization)) client := &http.Client{} resp, err := client.Do(req) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { var response playResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return playResponse{Error: fmt.Sprintf("HTTP error: %d, unable to decode body", resp.StatusCode)} } return playResponse{Error: response.Error} } var response playResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return playResponse{Error: fmt.Sprintf("Error decoding JSON: %v", err)} } return response } }