diff --git a/internal/client/client.go b/internal/client/client.go index 22c9d30..a60c1e8 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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: diff --git a/internal/client/ui/auth/auth.go b/internal/client/ui/auth/auth.go index 0cabe38..e26e88c 100644 --- a/internal/client/ui/auth/auth.go +++ b/internal/client/ui/auth/auth.go @@ -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()) } + diff --git a/internal/client/ui/chat/chat.go b/internal/client/ui/chat/chat.go index 1fd4537..b87071e 100644 --- a/internal/client/ui/chat/chat.go +++ b/internal/client/ui/chat/chat.go @@ -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 diff --git a/internal/client/ui/loadscreen/loadscreen.go b/internal/client/ui/loadscreen/loadscreen.go new file mode 100644 index 0000000..6639f7d --- /dev/null +++ b/internal/client/ui/loadscreen/loadscreen.go @@ -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 +} diff --git a/internal/client/ui/ui.go b/internal/client/ui/ui.go index 355fbfb..a208576 100644 --- a/internal/client/ui/ui.go +++ b/internal/client/ui/ui.go @@ -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)