feat: add WIP loading screen, messed around with custom spinners

This commit is contained in:
2024-11-09 21:36:11 +02:00
parent 8d97e7000c
commit 1b2c191496
5 changed files with 261 additions and 29 deletions

View File

@@ -1,13 +1,11 @@
package client
import (
// "context"
// "crypto/ed25519"
"log"
"reflect"
tea "github.com/charmbracelet/bubbletea"
// "github.com/kyren223/eko/internal/client/gateway"
"github.com/kyren223/eko/internal/client/ui"
"github.com/kyren223/eko/internal/client/ui/auth"
"github.com/kyren223/eko/pkg/assert"
@@ -54,7 +52,12 @@ func (m model) View() string {
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
ui.Width, ui.Height = msg.Width, msg.Height
return m, nil
case ui.ModelTransition:
log.Println("Transition model from", reflect.TypeOf(m.model).String(), "to", reflect.TypeOf(msg.Model).String())
m.model = msg.Model
return m, nil
default:

View File

@@ -3,6 +3,7 @@ package auth
import (
"crypto/ed25519"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"log"
@@ -19,6 +20,7 @@ import (
"github.com/kyren223/eko/internal/client/ui"
authfield "github.com/kyren223/eko/internal/client/ui/auth/field"
"github.com/kyren223/eko/internal/client/ui/choicepopup"
"github.com/kyren223/eko/internal/client/ui/loadscreen"
"github.com/kyren223/eko/pkg/assert"
)
@@ -70,9 +72,6 @@ ___] | |__] | \| | | \|
)
type Model struct {
width int
height int
focusIndex int
fields []authfield.Model
@@ -196,15 +195,15 @@ func (m Model) View() string {
Padding(0, (vp.Width-width)/2-1, 1)
result := lipgloss.Place(
m.width, m.height,
ui.Width, ui.Height,
lipgloss.Center, lipgloss.Center,
vp.View(),
)
if m.popup != nil {
popup := m.popup.View()
x := (m.width - lipgloss.Width(popup)) / 2
y := (m.height - lipgloss.Height(popup)) / 2
x := (ui.Width - lipgloss.Width(popup)) / 2
y := (ui.Height - lipgloss.Height(popup)) / 2
result = ui.PlaceOverlay(x, y, popup, result)
}
@@ -213,10 +212,6 @@ func (m Model) View() string {
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
return m, nil
case tea.KeyMsg:
key := msg.Type
switch key {
@@ -364,6 +359,8 @@ func (m *Model) ButtonPressed(msg tea.Msg) tea.Cmd {
}
func (m *Model) Signup() tea.Cmd {
username := m.fields[usernameField].Input.Value()
assert.Assert(len(username) != 0, "username must not be empty")
passphrase := m.fields[passphraseField].Input.Value()
confirmation := m.fields[passphraseConfirmField].Input.Value()
@@ -387,7 +384,7 @@ func (m *Model) Signup() tea.Cmd {
assert.NotNil(errors.Unwrap(err), "there should always be an error to unwrap", "err", err)
return nil
}
file, err := os.OpenFile(privateKeyFilepath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
file, err := os.OpenFile(privateKeyFilepath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if errors.Is(err, os.ErrExist) {
info, e := os.Stat(privateKeyFilepath)
assert.NoError(e, "if file exists it should be fine to stat it")
@@ -408,12 +405,33 @@ func (m *Model) Signup() tea.Cmd {
assert.NotNil(errors.Unwrap(err), "there should always be an error to unwrap", "err", err)
return nil
}
defer file.Close()
// file.Write()
file.Close()
os.Remove(privateKeyFilepath)
_, privKey, err := ed25519.GenerateKey(nil)
if err != nil {
m.fields[privateKeyField].Input.Err = errors.New("Failed private key generation")
log.Println("ed25519 generate key error:", err)
return nil
}
var pemBlock *pem.Block
if hasPassphrase {
pemBlock, err = ssh.MarshalPrivateKeyWithPassphrase(privKey, username, []byte(passphrase))
} else {
pemBlock, err = ssh.MarshalPrivateKey(privKey, username)
}
if err != nil {
m.fields[privateKeyField].Input.Err = errors.New("Failed private key marshaling")
log.Println("ssh marshaling error:", err)
return nil
}
err = pem.Encode(file, pemBlock)
if err != nil {
m.fields[privateKeyField].Input.Err = errors.New("Failed writing to disk")
log.Println("pem encoding to file error:", err)
return nil
}
return tea.Quit
return authenticate(privKey)
}
func (m *Model) signin() tea.Cmd {
@@ -472,9 +490,7 @@ func (m *Model) signin() tea.Cmd {
return nil
}
_ = privKey
return tea.Quit
return authenticate(*privKey)
}
func createPopup(content string, leftChoices, rightChoices []string) *choicepopup.Model {
@@ -507,11 +523,7 @@ func expandPath(path string) string {
return path
}
func test() {
// pubKey, privKey, err := ed25519.GenerateKey(nil)
// sshPrivKey, err := ssh.NewSignerFromSigner(privKey)
// ssh.MarshalPrivateKey()
// ssh.MarshalAuthorizedKey()
// ssh.ParseRawPrivateKey()
// ssh.ParseRawPrivateKeyWithPassphrase()
func authenticate(privKey ed25519.PrivateKey) tea.Cmd {
return ui.Transition(loadscreen.New())
}

View File

@@ -59,7 +59,7 @@ func (m Model) View() string {
) + ""
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.messagebox.Viewport.Width = msg.Width

View File

@@ -0,0 +1,208 @@
package loadscreen
import (
"log"
"time"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/kyren223/eko/internal/client/ui"
"github.com/kyren223/eko/pkg/assert"
)
var (
style = lipgloss.NewStyle().Border(lipgloss.ThickBorder()).Padding(1)
_ = spinner.Spinner{
Frames: []string{
"██▓▓▒▒\n ░░\n ",
"▓▓▒▒░░\n██ \n ",
"▒▒░░ \n▓▓ \n██ ",
"░░ \n▒▒ \n▓▓██ ",
" \n░░ \n▒▒▓▓██",
" \n ██\n░░▒▒▓▓",
" ██\n ▓▓\n ░░▒▒",
" ██▓▓\n ▒▒\n ░░",
},
FPS: time.Second / 8,
}
apple = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")).Render("██")
// loadingFrames = circleTrail(5, 5, 0, true, " ", "░░", "▒▒", "▓▓", "██")
// loadingFrames = circleTrail(5, 5, 0, true, " ", "░░", "▒▒", "▓▓", "██", " ", " ", " ", apple)
// loadingFrames = circleTrail(5, 5, 0, true, " ", "░░", "░░", "▒▒", "▒▒", "▓▓", "▓▓", "██", " ", " ", " ", apple)
loadingFrames = func() []string {
green := lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00"))
trail := []string{"░░", "░░", "▒▒", "▒▒", "▓▓", "██", " ", " "}
for i := range trail {
trail[i] = green.Render(trail[i])
}
trail = append(trail, apple)
return circleTrail(5, 5, 0, true, " ", trail...)
}()
loading = spinner.Spinner{
Frames: loadingFrames,
FPS: time.Second / time.Duration(len(loadingFrames)),
}
)
func circleTrail(width int, height int, offset int, clockwise bool, bg string, trail ...string) (states []string) {
slots := (width+height)*2 - 4
slotWidth := lipgloss.Width(bg)
slotHeight := lipgloss.Height(bg)
assert.Assert(slots-len(trail) >= 0, "not enough space to fit trail")
assert.AddData("slotWidth", slotWidth)
assert.AddData("slotHeight", slotHeight)
defer assert.RemoveData("slotWidth")
defer assert.RemoveData("slotHeight")
for _, elem := range trail {
w := lipgloss.Width(elem)
h := lipgloss.Height(elem)
assert.AddData("elemWidth", w)
assert.AddData("elemHeight", h)
assert.Assert(slotWidth == w, "bg and all trails must have the same height")
assert.Assert(slotHeight == h, "bg and all trails must have the same height")
assert.RemoveData("elemWidth")
assert.RemoveData("elemHeight")
}
matrix := make([][]string, height)
for i := range matrix {
matrix[i] = make([]string, width)
}
for i := 0; i < slots; i++ {
for y := range matrix {
for x := range matrix[y] {
matrix[y][x] = bg
}
}
x, y := 0, 0
for j := 0; j < i+offset; j++ {
if clockwise {
if y == 0 && x != width-1 {
x++
} else if x == width-1 && y != height-1 {
y++
} else if y == height-1 && x != 0 {
x--
} else if x == 0 && y != 0 {
y--
}
} else {
if x == 0 && y != height-1 {
y++
} else if y == height-1 && x != width-1 {
x++
} else if x == width-1 && y != 0 {
y--
} else if y == 0 && x != 0 {
x--
}
}
}
for _, trail := range trail {
matrix[y][x] = trail
if clockwise {
if y == 0 && x != width-1 {
x++
} else if x == width-1 && y != height-1 {
y++
} else if y == height-1 && x != 0 {
x--
} else if x == 0 && y != 0 {
y--
}
} else {
if x == 0 && y != height-1 {
y++
} else if y == height-1 && x != width-1 {
x++
} else if x == width-1 && y != 0 {
y--
} else if y == 0 && x != 0 {
x--
}
}
}
var rows []string
for _, row := range matrix {
rows = append(rows, lipgloss.JoinHorizontal(0, row...))
}
state := lipgloss.JoinVertical(0, rows...)
states = append(states, state)
}
return states
}
type Model struct {
sp spinner.Model
content string
Style lipgloss.Style
delta time.Time
index int
}
func New() Model {
return Model{
sp: spinner.New(spinner.WithSpinner(loading)),
content: "Update Failed - retrying in 3 sec...",
Style: style,
}
}
func (m Model) Init() tea.Cmd {
return m.sp.Tick
}
func (m Model) View() string {
vp := viewport.New(40, 5)
vp.Style = m.Style
vp.SetContent(m.content)
result := lipgloss.JoinVertical(lipgloss.Center, m.sp.View(), vp.View())
return lipgloss.Place(
ui.Width, ui.Height,
lipgloss.Center, lipgloss.Center,
result,
)
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
key := msg.Type
switch key {
case tea.KeyCtrlC:
return m, tea.Quit
case tea.KeyCtrlT:
return m, m.sp.Tick
}
case spinner.TickMsg:
if m.index == 0 {
m.delta = time.Now()
}
delta := time.Since(m.delta)
log.Printf("Frame %v Delta %v\n", m.index, delta)
m.index++
m.delta = time.Now()
var cmd tea.Cmd
m.sp, cmd = m.sp.Update(msg)
return m, cmd
}
return m, nil
}

View File

@@ -15,10 +15,19 @@ import (
"github.com/kyren223/eko/pkg/assert"
)
var Width int
var Height int
type ModelTransition struct {
Model tea.Model
}
func Transition(model tea.Model) tea.Cmd {
return func() tea.Msg {
return ModelTransition{Model: model}
}
}
func AddBorderHeader(header string, headerOffset int, style lipgloss.Style, render string) string {
b := style.GetBorderStyle()
body := style.UnsetBorderTop().Render(render)