Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ internal/old_tui/
docs/node_modules/
docs/.vitepress/dist/
docs/.vitepress/cache/
test_files/
38 changes: 35 additions & 3 deletions internal/tui/app/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"fmt"
"sort"
"strings"
"time"

"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/yashranjan1/relay/internal/log"
loader "github.com/yashranjan1/relay/internal/tui/components/Loader"
toast "github.com/yashranjan1/relay/internal/tui/components/Toast"
"github.com/yashranjan1/relay/internal/tui/keybinds"
"github.com/yashranjan1/relay/internal/tui/messages"
Expand Down Expand Up @@ -40,6 +42,8 @@ type AppModel struct {
keys []key.Binding
help help.Model
errorMsg string
loading bool
loader *loader.Loader
}

func (a AppModel) Init() tea.Cmd {
Expand All @@ -56,11 +60,23 @@ func (a AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case messages.ItemAdded:
a.Views[Collections], cmd = a.Views[Collections].Update(messages.RefreshItemsList{})
cmds = append(cmds, cmd)
case messages.StartLoader:
a.loading = true
a.loader.SetMessage(msg.Message)
cmd = a.loader.Tick()
cmds = append(cmds, cmd)
case messages.Response:
a.loading = false
case messages.RemoveToast:
a.toast.RemoveToast(msg.ID)
case messages.AddToast:
cmd = a.toast.AddToast(msg.Type, msg.Message)
cmds = append(cmds, cmd)
case loader.TickMsg:
if a.loading {
cmd = a.loader.Update(msg)
cmds = append(cmds, cmd)
}
case tea.WindowSizeMsg:
a.height = msg.Height
a.width = msg.Width
Expand Down Expand Up @@ -92,6 +108,9 @@ func (a AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmd = a.Views[a.focusedView].OnFocus()
cmds = append(cmds, cmd)

if a.loading {
}

return a, tea.Batch(cmds...)

case tea.KeyPressMsg:
Expand Down Expand Up @@ -184,10 +203,21 @@ func (a AppModel) Header() string {
}

func (a AppModel) Footer() string {
name := styles.ApplyGradientToFooter("REQ")
name := styles.ApplyGradientToFooter("RELAY")
footerText := styles.FooterSegmentStyle.Render(a.Views[a.focusedView].GetFooterSegment())
version := styles.FooterVersionStyle.Width(a.width - lipgloss.Width(name) - lipgloss.Width(footerText)).Render(a.ctx.Version)
return lipgloss.JoinHorizontal(lipgloss.Left, name, footerText, version)
version := styles.FooterVersionStyle.Render(a.ctx.Version)

var loader string
residualWidth := a.width - (lipgloss.Width(name) + lipgloss.Width(footerText) + lipgloss.Width(version))
style := styles.LoaderStyle.Width(residualWidth)

if a.loading {
loader = style.Render(a.loader.View())
} else {
loader = style.Render("")
}

return lipgloss.JoinHorizontal(lipgloss.Left, name, footerText, loader, version)
}

func NewAppModel(ctx *Context) AppModel {
Expand All @@ -202,6 +232,8 @@ func NewAppModel(ctx *Context) AppModel {
ctx: ctx,
help: help.New(),
keys: appKeybinds,
loader: loader.NewLoader(50*time.Millisecond, 9),
loading: false,
toast: toasts,
}

Expand Down
81 changes: 81 additions & 0 deletions internal/tui/components/Loader/component.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package loader

import (
"math"
"strings"
"time"

tea "charm.land/bubbletea/v2"
"github.com/yashranjan1/relay/internal/tui/styles"
)

type TickMsg time.Time

type Loader struct {
start time.Time
speed time.Duration
message string
direction int
index int
width int
}

func NewLoader(speed time.Duration, width int) *Loader {
return &Loader{
start: time.Now(),
speed: speed,
direction: 1,
width: width,
message: " Generating commit",
}
}

func (l *Loader) SetMessage(message string) {
l.message = message
}

func (l *Loader) Tick() tea.Cmd {
return tea.Tick(l.speed, func(t time.Time) tea.Msg {
return TickMsg(t)
})
}

func (l *Loader) Update(msg tea.Msg) tea.Cmd {
switch msg.(type) {
case TickMsg:
l.index += l.direction
if l.index >= l.width-1 {
l.direction = -1
} else if l.index <= 0 {
l.direction = 1
}
return l.Tick()
}
return nil
}

func (l *Loader) View() string {
var result strings.Builder

for i := 0; i < l.width; i++ {
ch := "■"

dist := float64(abs(i-l.index)) / float64(l.width)
sin := math.Sin((1.0 - dist) * math.Pi / 2)
alpha := math.Pow(sin, 8)

char := styles.GetLoaderCharStyle(alpha)(ch)

result.WriteString(char)

}
result.WriteString(styles.LoaderMessageStyle.Render(l.message))
return result.String()
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}
3 changes: 3 additions & 0 deletions internal/tui/components/OptionsProvider/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func (o OptionsProvider[T, U]) Update(msg tea.Msg) (OptionsProvider[T, U], tea.C
}
return o, func() tea.Msg { return messages.DeleteItem{ItemID: int64(o.GetSelected().ID)} }
case key.Matches(msg, o.keys.Choose):
if o.GetSelected().ID == -1 {
return o, tea.Batch(cmds...)
}
return o, func() tea.Msg {
return messages.ChooseItem[Option]{
Item: o.GetSelected(),
Expand Down
1 change: 1 addition & 0 deletions internal/tui/components/ViewPort/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (v *Viewport) EraseState() {
v.status = 0
v.body = ""
v.headers = nil
v.viewport.SetContent(v.body)
}

func (v *Viewport) SetWidth(width int) {
Expand Down
4 changes: 4 additions & 0 deletions internal/tui/messages/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ type Response struct {
}

type RefreshItemsList struct{}

type StartLoader struct {
Message string
}
6 changes: 4 additions & 2 deletions internal/tui/styles/app-styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ var (
footerNameStyle = lipgloss.NewStyle().Bold(true).Background(AppTheme.FooterNameBG)
footerNameBGStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).Padding(0, 3, 0)
FooterSegmentStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).Foreground(AppTheme.FooterSegmentFG)
FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).AlignHorizontal(lipgloss.Right).PaddingRight(2).Foreground(AppTheme.FooterSegmentFG)
LoaderStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).AlignHorizontal(lipgloss.Right)
FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).AlignHorizontal(lipgloss.Right).Padding(0, 2).Foreground(AppTheme.FooterSegmentFG)
TabHeadingInactive = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true)
TabHeadingActive = lipgloss.NewStyle().Background(AppTheme.Accent).Foreground(AppTheme.HeadingForeground).Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true)
HelpStyle = lipgloss.NewStyle().Padding(1, 0, 1, 2)
Expand All @@ -20,7 +21,8 @@ func initAppStyles() {
footerNameStyle = lipgloss.NewStyle().Bold(true).Background(AppTheme.FooterNameBG)
footerNameBGStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).Padding(0, 3, 0)
FooterSegmentStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).Foreground(AppTheme.FooterSegmentFG)
FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).AlignHorizontal(lipgloss.Right).PaddingRight(2).Foreground(AppTheme.FooterSegmentFG)
LoaderStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).AlignHorizontal(lipgloss.Right)
FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).AlignHorizontal(lipgloss.Right).Padding(0, 2).Foreground(AppTheme.FooterSegmentFG)
TabHeadingInactive = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true)
TabHeadingActive = lipgloss.NewStyle().Background(AppTheme.Accent).Foreground(AppTheme.HeadingForeground).Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true)
HelpStyle = lipgloss.NewStyle().Padding(1, 0, 1, 2)
Expand Down
77 changes: 72 additions & 5 deletions internal/tui/styles/functions.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package styles

import (
stdcolor "image/color"
"fmt"
"image/color"
"strconv"
"strings"

"charm.land/lipgloss/v2"
)

func gradientText(text string, startColor, endColor stdcolor.Color, base, additional lipgloss.Style) string {
func gradientText(text string, startColor, endColor color.Color, base, additional lipgloss.Style) string {
n := len(text)
result := ""

Expand All @@ -21,17 +24,81 @@ func gradientText(text string, startColor, endColor stdcolor.Color, base, additi
return additional.Render(result)
}

func interpolateColor(start, end stdcolor.Color, ratio float64) stdcolor.Color {
func interpolateColor(start, end color.Color, ratio float64) color.Color {
r1, g1, b1, _ := start.RGBA()
r2, g2, b2, _ := end.RGBA()

r := uint8(float64(r1>>8) + (float64(r2>>8)-float64(r1>>8))*ratio)
g := uint8(float64(g1>>8) + (float64(g2>>8)-float64(g1>>8))*ratio)
b := uint8(float64(b1>>8) + (float64(b2>>8)-float64(b1>>8))*ratio)

return stdcolor.RGBA{R: r, G: g, B: b, A: 0xff}
return color.RGBA{R: r, G: g, B: b, A: 0xff}
}

func ApplyGradientToFooter(text string) string {
return gradientText("REQ", AppTheme.FooterNameFGFrom, AppTheme.FooterNameFGTo, footerNameStyle, footerNameBGStyle)
return gradientText(text, AppTheme.FooterNameFGFrom, AppTheme.FooterNameFGTo, footerNameStyle, footerNameBGStyle)
}

func ParseHex(s string) (color.RGBA, error) {
s = strings.TrimPrefix(s, "#")

if len(s) == 3 {
s = string([]byte{s[0], s[0], s[1], s[1], s[2], s[2]})
}

if len(s) != 6 {
return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s)
}

val, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s)
}

return color.RGBA{
R: uint8(val >> 16),
G: uint8(val >> 8),
B: uint8(val),
A: 255,
}, nil
}

func FakeOpacity(fg, bg color.RGBA, alpha float64) (string, error) {

blend := func(fg, bg uint8) uint8 {
return uint8(float64(fg)*alpha + float64(bg)*(1-alpha))
}

result := color.RGBA{
R: blend(fg.R, bg.R),
G: blend(fg.G, bg.G),
B: blend(fg.B, bg.B),
}

return fmt.Sprintf("#%02X%02X%02X", result.R, result.G, result.B), nil
}

func GetLoaderCharStyle(alpha float64) func(...string) string {
fgR, fgG, fgB, _ := AppTheme.Accent.RGBA()

fg := color.RGBA{
R: uint8(fgR),
G: uint8(fgG),
B: uint8(fgB),
}

bgR, bgG, bgB, _ := AppTheme.FooterSegmentBG.RGBA()

bg := color.RGBA{
R: uint8(bgR),
G: uint8(bgG),
B: uint8(bgB),
}

color, _ := FakeOpacity(fg, bg, alpha)

return lipgloss.NewStyle().
Foreground(lipgloss.Color(color)).
Background(AppTheme.FooterSegmentBG).
Render
}
19 changes: 19 additions & 0 deletions internal/tui/styles/loader-styles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package styles

import (
"charm.land/lipgloss/v2"
)

var (
LoaderMessageStyle = lipgloss.NewStyle().
PaddingRight(2).
Foreground(AppTheme.Accent).
Background(AppTheme.FooterSegmentBG)
)

func initLoaderStyles() {
LoaderMessageStyle = lipgloss.NewStyle().
PaddingRight(2).
Foreground(AppTheme.Accent).
Background(AppTheme.FooterSegmentBG)
}
2 changes: 1 addition & 1 deletion internal/tui/styles/request-styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func UrlInputStyle(active bool) lipgloss.Style {
}

func ResponseContentStyle(height, width int) func(...string) string {
return lipgloss.NewStyle().Padding(1, 0, 1, 1).Height(height).Width(width).Render
return lipgloss.NewStyle().Padding(1, 0, 0, 1).Height(height).Width(width).Render
}

func ResponseStyle(active bool) func(...string) string {
Expand Down
1 change: 1 addition & 0 deletions internal/tui/styles/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ func SetTheme(newTheme Theme) {
initRequestStyles()
initCollectionStyles()
initAppStyles()
initLoaderStyles()
}
Loading
Loading