mirror of
https://github.com/Kyren223/eko.git
synced 2026-07-12 23:30:24 +00:00
feat: add WIP loading screen, messed around with custom spinners
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
208
internal/client/ui/loadscreen/loadscreen.go
Normal file
208
internal/client/ui/loadscreen/loadscreen.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user