mirror of
https://github.com/Kyren223/eko.git
synced 2025-09-05 13:08:20 +00:00
274 lines
7.6 KiB
Go
274 lines
7.6 KiB
Go
// Eko: A terminal based social media platform
|
|
// Copyright (C) 2025 Kyren223
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
charmansi "github.com/charmbracelet/x/ansi"
|
|
"github.com/mattn/go-runewidth"
|
|
"github.com/muesli/ansi"
|
|
"github.com/muesli/reflow/truncate"
|
|
|
|
"github.com/kyren223/eko/internal/client/ui/colors"
|
|
"github.com/kyren223/eko/pkg/assert"
|
|
"github.com/kyren223/eko/pkg/snowflake"
|
|
)
|
|
|
|
const (
|
|
MinWidth = 85
|
|
MinHeight = 25
|
|
Center lipgloss.Position = 0.499
|
|
)
|
|
|
|
var (
|
|
Width int
|
|
Height int
|
|
Program *tea.Program
|
|
|
|
UserStyle = func() lipgloss.Style { return lipgloss.NewStyle().Foreground(colors.Purple).SetString("") }
|
|
AdminStyle = func() lipgloss.Style { return lipgloss.NewStyle().Foreground(colors.Red).Bold(true).SetString("") }
|
|
OwnerStyle = func() lipgloss.Style { return AdminStyle().Foreground(colors.Gold).SetString("") }
|
|
TrustedUserStyle = func() lipgloss.Style { return UserStyle().Foreground(colors.Turquoise).SetString("") }
|
|
TrustedMemberStyle = func() lipgloss.Style { return UserStyle().SetString("") }
|
|
TrustedAdminStyle = func() lipgloss.Style { return AdminStyle().SetString("") }
|
|
TrustedOwnerStyle = func() lipgloss.Style { return OwnerStyle().SetString("") }
|
|
UntrustedSymbol = func() string { return lipgloss.NewStyle().Foreground(colors.Red).Render("") }
|
|
BlockedSymbol = func() string { return lipgloss.NewStyle().Foreground(colors.Red).Render(" ") }
|
|
)
|
|
|
|
var NewAuth func() tea.Model
|
|
|
|
// Used to update a model with a "fake" message
|
|
type EmptyMsg struct{}
|
|
|
|
type ModelTransition struct {
|
|
Model tea.Model
|
|
}
|
|
|
|
func Transition(model tea.Model) tea.Cmd {
|
|
return func() tea.Msg {
|
|
return ModelTransition{Model: model}
|
|
}
|
|
}
|
|
|
|
type QuitMsg struct{}
|
|
|
|
type ProfilePopupMsg struct {
|
|
User snowflake.ID
|
|
}
|
|
|
|
type BanViewPopupMsg struct {
|
|
Network snowflake.ID
|
|
User snowflake.ID
|
|
}
|
|
|
|
type BanReasonPopupMsg struct {
|
|
Network snowflake.ID
|
|
User snowflake.ID
|
|
}
|
|
|
|
func AddBorderHeader(header string, headerOffset int, style lipgloss.Style, render string) string {
|
|
b := style.GetBorderStyle()
|
|
body := style.UnsetBorderTop().Render(render)
|
|
|
|
bodyWidth, headerWidth := lipgloss.Width(body), lipgloss.Width(header)
|
|
leftCornerWidth, rightCornerWidth := lipgloss.Width(b.TopLeft), lipgloss.Width(b.TopRight)
|
|
topWidth := bodyWidth - leftCornerWidth - rightCornerWidth
|
|
|
|
leftWidth := headerOffset
|
|
rightWidth := topWidth - leftWidth - headerWidth
|
|
assert.Assert(leftWidth >= 0, "left width cannot be negative", "leftWidth", leftWidth)
|
|
assert.Assert(rightWidth >= 0, "right width cannot be negative", "rightWidth", rightWidth)
|
|
|
|
topStyle := lipgloss.NewStyle().
|
|
Background(style.GetBorderTopBackground()).
|
|
Foreground(style.GetBorderTopForeground())
|
|
|
|
left := b.TopLeft + strings.Repeat(b.Top, leftWidth)
|
|
right := topStyle.Render(strings.Repeat(b.Top, rightWidth) + b.TopRight)
|
|
|
|
borderTop := lipgloss.NewStyle().
|
|
Inline(true).
|
|
MaxWidth(bodyWidth).
|
|
Background(style.GetBorderTopBackground()).
|
|
Foreground(style.GetBorderTopForeground()).
|
|
Render(fmt.Sprintf("%s%s%s", left, header, right))
|
|
|
|
return lipgloss.JoinVertical(lipgloss.Left, borderTop, body)
|
|
}
|
|
|
|
// PlaceOverlay places fg on top of bg.
|
|
func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string {
|
|
fgLines, fgWidth := getLines(fg)
|
|
bgLines, bgWidth := getLines(bg)
|
|
bgHeight := len(bgLines)
|
|
fgHeight := len(fgLines)
|
|
|
|
if fgWidth >= bgWidth && fgHeight >= bgHeight {
|
|
// FIXME: return fg or bg?
|
|
return fg
|
|
}
|
|
// TODO: allow placement outside of the bg box?
|
|
x = clamp(x, 0, bgWidth-fgWidth)
|
|
y = clamp(y, 0, bgHeight-fgHeight)
|
|
|
|
ws := &whitespace{}
|
|
for _, opt := range opts {
|
|
opt(ws)
|
|
}
|
|
|
|
var b strings.Builder
|
|
for i, bgLine := range bgLines {
|
|
if i > 0 {
|
|
b.WriteByte('\n')
|
|
}
|
|
if i < y || i >= y+fgHeight {
|
|
b.WriteString(bgLine)
|
|
continue
|
|
}
|
|
|
|
pos := 0
|
|
if x > 0 {
|
|
left := truncate.String(bgLine, uint(x))
|
|
pos = ansi.PrintableRuneWidth(left)
|
|
b.WriteString(left)
|
|
if pos < x {
|
|
b.WriteString(ws.render(x - pos))
|
|
pos = x
|
|
}
|
|
}
|
|
|
|
fgLine := fgLines[i-y]
|
|
b.WriteString(fgLine)
|
|
pos += ansi.PrintableRuneWidth(fgLine)
|
|
|
|
right := cutLeft(bgLine, pos)
|
|
bgWidth := ansi.PrintableRuneWidth(bgLine)
|
|
rightWidth := ansi.PrintableRuneWidth(right)
|
|
if rightWidth <= bgWidth-pos {
|
|
b.WriteString(ws.render(bgWidth - rightWidth - pos))
|
|
}
|
|
|
|
b.WriteString(right)
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// cutLeft cuts printable characters from the left.
|
|
// This function is heavily based on muesli's ansi and truncate packages.
|
|
func cutLeft(s string, cutWidth int) string {
|
|
var (
|
|
pos int
|
|
isAnsi bool
|
|
ab bytes.Buffer
|
|
b bytes.Buffer
|
|
)
|
|
for _, c := range s {
|
|
var w int
|
|
if c == ansi.Marker || isAnsi {
|
|
isAnsi = true
|
|
ab.WriteRune(c)
|
|
if ansi.IsTerminator(c) {
|
|
isAnsi = false
|
|
if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) {
|
|
ab.Reset()
|
|
}
|
|
}
|
|
} else {
|
|
w = runewidth.RuneWidth(c)
|
|
}
|
|
|
|
if pos >= cutWidth {
|
|
if b.Len() == 0 {
|
|
if ab.Len() > 0 {
|
|
b.Write(ab.Bytes())
|
|
}
|
|
if pos-cutWidth > 1 {
|
|
b.WriteByte(' ')
|
|
continue
|
|
}
|
|
}
|
|
b.WriteRune(c)
|
|
}
|
|
pos += w
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func getLines(s string) (lines []string, widest int) {
|
|
lines = strings.Split(s, "\n")
|
|
|
|
for _, l := range lines {
|
|
w := charmansi.StringWidth(l)
|
|
if widest < w {
|
|
widest = w
|
|
}
|
|
}
|
|
|
|
return lines, widest
|
|
}
|
|
|
|
func clamp(v, lower, upper int) int {
|
|
return min(max(v, lower), upper)
|
|
}
|
|
|
|
/*
|
|
🭊🭂██🭍🬿
|
|
██████
|
|
🭥🭓██🭞🭚
|
|
|
|
🭠🭘 🭣🭕
|
|
|
|
🭏🬽 🭈🭄
|
|
*/
|
|
|
|
func IconStyle(icon string, iconFg, iconBg, bg lipgloss.Color) lipgloss.Style {
|
|
bgStyle := lipgloss.NewStyle().Background(iconBg).Foreground(bg)
|
|
top := bgStyle.Render("🭠🭘 🭣🭕")
|
|
middle := lipgloss.NewStyle().Width(6).Align(lipgloss.Center).
|
|
Background(iconBg).Foreground(iconFg).Render(icon)
|
|
bgStyle2 := lipgloss.NewStyle().Foreground(iconBg)
|
|
bottom := bgStyle2.Render("🭥🭓██🭞🭚")
|
|
combined := lipgloss.JoinVertical(lipgloss.Left, top, middle, bottom)
|
|
return lipgloss.NewStyle().SetString(combined)
|
|
}
|
|
|
|
var notifications = []string{
|
|
"", "", "", "", "", "", "", "", "", "",
|
|
}
|
|
|
|
func IconStyleNotif(icon string, iconFg, iconBg, bg lipgloss.Color, count int) lipgloss.Style {
|
|
notifStyle := lipgloss.NewStyle().Background(iconBg).Foreground(colors.Red)
|
|
notif := notifications[max(0, min(10, count)-1)]
|
|
notif = notifStyle.Render(notif)
|
|
|
|
bgStyle := lipgloss.NewStyle().Background(iconBg).Foreground(bg)
|
|
top := bgStyle.Render("🭠🭘 🭣🭕")
|
|
middle := lipgloss.NewStyle().Width(6).Align(lipgloss.Center).
|
|
Background(iconBg).Foreground(iconFg).Render(icon)
|
|
bgStyle2 := lipgloss.NewStyle().Foreground(iconBg).Background(bg)
|
|
bottom := bgStyle2.Render("🭥🭓██") + notif + bgStyle.Render("")
|
|
combined := lipgloss.JoinVertical(lipgloss.Left, top, middle, bottom)
|
|
return lipgloss.NewStyle().SetString(combined)
|
|
}
|