Detailed changes
@@ -8,7 +8,7 @@ import (
gansi "github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/soft-serve/config"
- "github.com/charmbracelet/soft-serve/tui/common"
+ "github.com/charmbracelet/soft-serve/ui/common"
gitwish "github.com/charmbracelet/wish/git"
"github.com/muesli/termenv"
"github.com/spf13/cobra"
@@ -109,7 +109,7 @@ func withFormatting(p, c string) (string, error) {
Language: lang,
}
r := strings.Builder{}
- styles := common.DefaultStyles()
+ styles := common.StyleConfig()
styles.CodeBlock.Margin = &zero
rctx := gansi.NewRenderContext(gansi.Options{
Styles: styles,
@@ -1,122 +0,0 @@
-package about
-
-import (
- "github.com/charmbracelet/bubbles/viewport"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/charmbracelet/soft-serve/tui/common"
- "github.com/charmbracelet/soft-serve/tui/refs"
- vp "github.com/charmbracelet/soft-serve/tui/viewport"
- "github.com/muesli/reflow/wrap"
-)
-
-type Bubble struct {
- readmeViewport *vp.ViewportBubble
- repo common.GitRepo
- styles *style.Styles
- height int
- heightMargin int
- width int
- widthMargin int
- ref *git.Reference
-}
-
-func NewBubble(repo common.GitRepo, styles *style.Styles, width, wm, height, hm int) *Bubble {
- b := &Bubble{
- readmeViewport: &vp.ViewportBubble{
- Viewport: &viewport.Model{},
- },
- repo: repo,
- styles: styles,
- widthMargin: wm,
- heightMargin: hm,
- }
- b.SetSize(width, height)
- return b
-}
-func (b *Bubble) Init() tea.Cmd {
- return b.reset
-}
-
-func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- b.SetSize(msg.Width, msg.Height)
- // XXX: if we find that longer readmes take more than a few
- // milliseconds to render we may need to move Glamour rendering into a
- // command.
- md, err := b.glamourize()
- if err != nil {
- return b, nil
- }
- b.readmeViewport.Viewport.SetContent(md)
- case tea.KeyMsg:
- switch msg.String() {
- case "R":
- return b, b.reset
- }
- case refs.RefMsg:
- b.ref = msg
- return b, b.reset
- }
- rv, cmd := b.readmeViewport.Update(msg)
- b.readmeViewport = rv.(*vp.ViewportBubble)
- cmds = append(cmds, cmd)
- return b, tea.Batch(cmds...)
-}
-
-func (b *Bubble) SetSize(w, h int) {
- b.width = w
- b.height = h
- b.readmeViewport.Viewport.Width = w - b.widthMargin
- b.readmeViewport.Viewport.Height = h - b.heightMargin
-}
-
-func (b *Bubble) GotoTop() {
- b.readmeViewport.Viewport.GotoTop()
-}
-
-func (b *Bubble) View() string {
- return b.readmeViewport.View()
-}
-
-func (b *Bubble) Help() []common.HelpEntry {
- return nil
-}
-
-func (b *Bubble) glamourize() (string, error) {
- w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
- rm, rp := b.repo.Readme()
- if rm == "" {
- return b.styles.AboutNoReadme.Render("No readme found."), nil
- }
- f, err := common.RenderFile(rp, rm, w)
- if err != nil {
- return "", err
- }
- // For now, hard-wrap long lines in Glamour that would otherwise break the
- // layout when wrapping. This may be due to #43 in Reflow, which has to do
- // with a bug in the way lines longer than the given width are wrapped.
- //
- // https://github.com/muesli/reflow/issues/43
- //
- // TODO: solve this upstream in Glamour/Reflow.
- return wrap.String(f, w), nil
-}
-
-func (b *Bubble) reset() tea.Msg {
- md, err := b.glamourize()
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- head, err := b.repo.HEAD()
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- b.ref = head
- b.readmeViewport.Viewport.SetContent(md)
- b.GotoTop()
- return nil
-}
@@ -1,155 +0,0 @@
-package tui
-
-import (
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/charmbracelet/soft-serve/tui/about"
- "github.com/charmbracelet/soft-serve/tui/common"
- "github.com/charmbracelet/soft-serve/tui/log"
- "github.com/charmbracelet/soft-serve/tui/refs"
- "github.com/charmbracelet/soft-serve/tui/tree"
-)
-
-const (
- repoNameMaxWidth = 32
-)
-
-type state int
-
-const (
- aboutState state = iota
- refsState
- logState
- treeState
-)
-
-type Bubble struct {
- state state
- repo common.GitRepo
- height int
- heightMargin int
- width int
- widthMargin int
- style *style.Styles
- boxes []tea.Model
- ref *git.Reference
-}
-
-func NewBubble(repo common.GitRepo, styles *style.Styles, width, wm, height, hm int) *Bubble {
- b := &Bubble{
- repo: repo,
- state: aboutState,
- width: width,
- widthMargin: wm,
- height: height,
- heightMargin: hm,
- style: styles,
- boxes: make([]tea.Model, 4),
- }
- heightMargin := hm + lipgloss.Height(b.headerView())
- b.boxes[aboutState] = about.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
- b.boxes[refsState] = refs.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
- b.boxes[logState] = log.NewBubble(repo, b.style, width, wm, height, heightMargin)
- b.boxes[treeState] = tree.NewBubble(repo, b.style, width, wm, height, heightMargin)
- return b
-}
-
-func (b *Bubble) Init() tea.Cmd {
- return b.setupCmd
-}
-
-func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- cmds := make([]tea.Cmd, 0)
- switch msg := msg.(type) {
- case tea.KeyMsg:
- if b.repo.Name() != "config" {
- switch msg.String() {
- case "R":
- b.state = aboutState
- case "B":
- b.state = refsState
- case "C":
- b.state = logState
- case "F":
- b.state = treeState
- }
- }
- case tea.WindowSizeMsg:
- b.width = msg.Width
- b.height = msg.Height
- for i, bx := range b.boxes {
- m, cmd := bx.Update(msg)
- b.boxes[i] = m
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
- case refs.RefMsg:
- b.state = treeState
- b.ref = msg
- for i, bx := range b.boxes {
- m, cmd := bx.Update(msg)
- b.boxes[i] = m
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
- }
- m, cmd := b.boxes[b.state].Update(msg)
- b.boxes[b.state] = m
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- return b, tea.Batch(cmds...)
-}
-
-func (b *Bubble) Help() []common.HelpEntry {
- h := []common.HelpEntry{}
- h = append(h, b.boxes[b.state].(common.BubbleHelper).Help()...)
- if b.repo.Name() != "config" {
- h = append(h, common.HelpEntry{Key: "R", Value: "readme"})
- h = append(h, common.HelpEntry{Key: "F", Value: "files"})
- h = append(h, common.HelpEntry{Key: "C", Value: "commits"})
- h = append(h, common.HelpEntry{Key: "B", Value: "branches"})
- }
- return h
-}
-
-func (b *Bubble) Reference() *git.Reference {
- return b.ref
-}
-
-func (b *Bubble) headerView() string {
- // TODO better header, tabs?
- return ""
-}
-
-func (b *Bubble) View() string {
- header := b.headerView()
- return header + b.boxes[b.state].View()
-}
-
-func (b *Bubble) setupCmd() tea.Msg {
- head, err := b.repo.HEAD()
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- b.ref = head
- cmds := make([]tea.Cmd, 0)
- for _, bx := range b.boxes {
- if bx != nil {
- initCmd := bx.Init()
- if initCmd != nil {
- msg := initCmd()
- switch msg := msg.(type) {
- case common.ErrMsg:
- return msg
- }
- }
- cmds = append(cmds, initCmd)
- }
- }
- return tea.Batch(cmds...)
-}
@@ -1,28 +0,0 @@
-package common
-
-import (
- "time"
-
- "github.com/charmbracelet/bubbles/key"
-)
-
-// Some constants were copied from https://docs.gitea.io/en-us/config-cheat-sheet/#git-git
-
-const (
- GlamourMaxWidth = 120
- RepoNameMaxWidth = 32
- MaxDiffLines = 1000
- MaxDiffFiles = 100
- MaxPatchWait = time.Second * 3
-)
-
-var (
- PrevPage = key.NewBinding(
- key.WithKeys("pgup", "b", "u"),
- key.WithHelp("pgup", "prev page"),
- )
- NextPage = key.NewBinding(
- key.WithKeys("pgdown", "f", "d"),
- key.WithHelp("pgdn", "next page"),
- )
-)
@@ -1,36 +0,0 @@
-package common
-
-import (
- "errors"
-
- "github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/internal/tui/style"
-)
-
-var (
- ErrDiffTooLong = errors.New("diff is too long")
- ErrDiffFilesTooLong = errors.New("diff files are too long")
- ErrBinaryFile = errors.New("binary file")
- ErrFileTooLarge = errors.New("file is too large")
- ErrInvalidFile = errors.New("invalid file")
-)
-
-type ErrMsg struct {
- Err error
-}
-
-func (e ErrMsg) Error() string {
- return e.Err.Error()
-}
-
-func (e ErrMsg) View(s *style.Styles) string {
- return e.ViewWithPrefix(s, "")
-}
-
-func (e ErrMsg) ViewWithPrefix(s *style.Styles, prefix string) string {
- return lipgloss.JoinHorizontal(
- lipgloss.Top,
- s.ErrorTitle.Render(prefix),
- s.ErrorBody.Render(e.Error()),
- )
-}
@@ -1,88 +0,0 @@
-package common
-
-import (
- "strings"
-
- "github.com/alecthomas/chroma/lexers"
- "github.com/charmbracelet/glamour"
- gansi "github.com/charmbracelet/glamour/ansi"
- "github.com/muesli/termenv"
-)
-
-var (
- RenderCtx = DefaultRenderCtx()
- Styles = DefaultStyles()
-)
-
-func DefaultStyles() gansi.StyleConfig {
- noColor := ""
- s := glamour.DarkStyleConfig
- s.Document.StylePrimitive.Color = &noColor
- s.CodeBlock.Chroma.Text.Color = &noColor
- s.CodeBlock.Chroma.Name.Color = &noColor
- return s
-}
-
-func DefaultRenderCtx() gansi.RenderContext {
- return gansi.NewRenderContext(gansi.Options{
- ColorProfile: termenv.TrueColor,
- Styles: DefaultStyles(),
- })
-}
-
-func NewRenderCtx(worldwrap int) gansi.RenderContext {
- return gansi.NewRenderContext(gansi.Options{
- ColorProfile: termenv.TrueColor,
- Styles: DefaultStyles(),
- WordWrap: worldwrap,
- })
-}
-
-func Glamourize(w int, md string) (string, error) {
- if w > GlamourMaxWidth {
- w = GlamourMaxWidth
- }
- tr, err := glamour.NewTermRenderer(
- glamour.WithStyles(DefaultStyles()),
- glamour.WithWordWrap(w),
- )
-
- if err != nil {
- return "", err
- }
- mdt, err := tr.Render(md)
- if err != nil {
- return "", err
- }
- return mdt, nil
-}
-
-func RenderFile(path, content string, width int) (string, error) {
- lexer := lexers.Fallback
- if path == "" {
- lexer = lexers.Analyse(content)
- } else {
- lexer = lexers.Match(path)
- }
- lang := ""
- if lexer != nil && lexer.Config() != nil {
- lang = lexer.Config().Name
- }
- formatter := &gansi.CodeBlockElement{
- Code: content,
- Language: lang,
- }
- if lang == "markdown" {
- md, err := Glamourize(width, content)
- if err != nil {
- return "", err
- }
- return md, nil
- }
- r := strings.Builder{}
- err := formatter.Render(&r, RenderCtx)
- if err != nil {
- return "", err
- }
- return r.String(), nil
-}
@@ -1,16 +0,0 @@
-package common
-
-import (
- "github.com/charmbracelet/soft-serve/git"
-)
-
-type GitRepo interface {
- Name() string
- Readme() (string, string)
- HEAD() (*git.Reference, error)
- CommitsByPage(*git.Reference, int, int) (git.Commits, error)
- CountCommits(*git.Reference) (int64, error)
- Diff(*git.Commit) (*git.Diff, error)
- References() ([]*git.Reference, error)
- Tree(*git.Reference, string) (*git.Tree, error)
-}
@@ -1,10 +0,0 @@
-package common
-
-type BubbleHelper interface {
- Help() []HelpEntry
-}
-
-type HelpEntry struct {
- Key string
- Value string
-}
@@ -1,7 +0,0 @@
-package common
-
-import tea "github.com/charmbracelet/bubbletea"
-
-type BubbleReset interface {
- Reset() tea.Msg
-}
@@ -1,17 +0,0 @@
-package common
-
-import "github.com/muesli/reflow/truncate"
-
-func TruncateString(s string, max int, tail string) string {
- if max < 0 {
- max = 0
- }
- return truncate.StringWithTail(s, uint(max), tail)
-}
-
-func Max(a, b int) int {
- if a > b {
- return a
- }
- return b
-}
@@ -1,383 +0,0 @@
-package log
-
-import (
- "fmt"
- "io"
- "strings"
- "time"
-
- "github.com/charmbracelet/bubbles/list"
- "github.com/charmbracelet/bubbles/spinner"
- "github.com/charmbracelet/bubbles/viewport"
- tea "github.com/charmbracelet/bubbletea"
- gansi "github.com/charmbracelet/glamour/ansi"
- "github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/charmbracelet/soft-serve/tui/common"
- "github.com/charmbracelet/soft-serve/tui/refs"
- vp "github.com/charmbracelet/soft-serve/tui/viewport"
-)
-
-var (
- diffChroma = &gansi.CodeBlockElement{
- Code: "",
- Language: "diff",
- }
- waitBeforeLoading = time.Millisecond * 300
-)
-
-type itemsMsg struct{}
-
-type commitMsg *git.Commit
-
-type countMsg int64
-
-type sessionState int
-
-const (
- logState sessionState = iota
- commitState
- errorState
-)
-
-type item struct {
- *git.Commit
-}
-
-func (i item) Title() string {
- if i.Commit != nil {
- return strings.Split(i.Commit.Message, "\n")[0]
- }
- return ""
-}
-
-func (i item) FilterValue() string { return i.Title() }
-
-type itemDelegate struct {
- style *style.Styles
-}
-
-func (d itemDelegate) Height() int { return 1 }
-func (d itemDelegate) Spacing() int { return 0 }
-func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
-func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
- i, ok := listItem.(item)
- if !ok {
- return
- }
- if i.Commit == nil {
- return
- }
-
- hash := i.ID.String()
- leftMargin := d.style.LogItemSelector.GetMarginLeft() +
- d.style.LogItemSelector.GetWidth() +
- d.style.LogItemHash.GetMarginLeft() +
- d.style.LogItemHash.GetWidth() +
- d.style.LogItemInactive.GetMarginLeft()
- title := common.TruncateString(i.Title(), m.Width()-leftMargin, "…")
- if index == m.Index() {
- fmt.Fprint(w, d.style.LogItemSelector.Render(">")+
- d.style.LogItemHash.Bold(true).Render(hash[:7])+
- d.style.LogItemActive.Render(title))
- } else {
- fmt.Fprint(w, d.style.LogItemSelector.Render(" ")+
- d.style.LogItemHash.Render(hash[:7])+
- d.style.LogItemInactive.Render(title))
- }
-}
-
-type Bubble struct {
- repo common.GitRepo
- count int64
- list list.Model
- state sessionState
- commitViewport *vp.ViewportBubble
- ref *git.Reference
- style *style.Styles
- width int
- widthMargin int
- height int
- heightMargin int
- error common.ErrMsg
- spinner spinner.Model
- loading bool
- loadingStart time.Time
- selectedCommit *git.Commit
- nextPage int
-}
-
-func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
- l := list.New([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
- l.SetShowFilter(false)
- l.SetShowHelp(false)
- l.SetShowPagination(true)
- l.SetShowStatusBar(false)
- l.SetShowTitle(false)
- l.SetFilteringEnabled(false)
- l.DisableQuitKeybindings()
- l.KeyMap.NextPage = common.NextPage
- l.KeyMap.PrevPage = common.PrevPage
- s := spinner.New()
- s.Spinner = spinner.Dot
- s.Style = styles.Spinner
- b := &Bubble{
- commitViewport: &vp.ViewportBubble{
- Viewport: &viewport.Model{},
- },
- repo: repo,
- style: styles,
- state: logState,
- width: width,
- widthMargin: widthMargin,
- height: height,
- heightMargin: heightMargin,
- list: l,
- spinner: s,
- }
- b.SetSize(width, height)
- return b
-}
-
-func (b *Bubble) countCommits() tea.Msg {
- if b.ref == nil {
- ref, err := b.repo.HEAD()
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- b.ref = ref
- }
- count, err := b.repo.CountCommits(b.ref)
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- return countMsg(count)
-}
-
-func (b *Bubble) updateItems() tea.Msg {
- if b.count == 0 {
- b.count = int64(b.countCommits().(countMsg))
- }
- count := b.count
- items := make([]list.Item, count)
- page := b.nextPage
- limit := b.list.Paginator.PerPage
- skip := page * limit
- // CommitsByPage pages start at 1
- cc, err := b.repo.CommitsByPage(b.ref, page+1, limit)
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- for i, c := range cc {
- idx := i + skip
- if int64(idx) >= count {
- break
- }
- items[idx] = item{c}
- }
- b.list.SetItems(items)
- b.SetSize(b.width, b.height)
- return itemsMsg{}
-}
-
-func (b *Bubble) Help() []common.HelpEntry {
- return nil
-}
-
-func (b *Bubble) GotoTop() {
- b.commitViewport.Viewport.GotoTop()
-}
-
-func (b *Bubble) Init() tea.Cmd {
- return nil
-}
-
-func (b *Bubble) SetSize(width, height int) {
- b.width = width
- b.height = height
- b.commitViewport.Viewport.Width = width - b.widthMargin
- b.commitViewport.Viewport.Height = height - b.heightMargin
- b.list.SetSize(width-b.widthMargin, height-b.heightMargin)
- b.list.Styles.PaginationStyle = b.style.LogPaginator.Copy().Width(width - b.widthMargin)
-}
-
-func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- cmds := make([]tea.Cmd, 0)
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- b.SetSize(msg.Width, msg.Height)
- cmds = append(cmds, b.updateItems)
-
- case tea.KeyMsg:
- switch msg.String() {
- case "C":
- b.count = 0
- b.loading = true
- b.loadingStart = time.Now().Add(-waitBeforeLoading) // always show spinner
- b.list.Select(0)
- b.nextPage = 0
- return b, tea.Batch(b.updateItems, b.spinner.Tick)
- case "enter", "right", "l":
- if b.state == logState {
- i := b.list.SelectedItem()
- if i != nil {
- c, ok := i.(item)
- if ok {
- b.selectedCommit = c.Commit
- }
- }
- cmds = append(cmds, b.loadCommit, b.spinner.Tick)
- }
- case "esc", "left", "h":
- if b.state != logState {
- b.state = logState
- b.selectedCommit = nil
- }
- }
- switch b.state {
- case logState:
- curPage := b.list.Paginator.Page
- m, cmd := b.list.Update(msg)
- b.list = m
- if m.Paginator.Page != curPage {
- b.loading = true
- b.loadingStart = time.Now()
- b.list.Paginator.Page = curPage
- b.nextPage = m.Paginator.Page
- cmds = append(cmds, b.updateItems, b.spinner.Tick)
- }
- cmds = append(cmds, cmd)
- case commitState:
- rv, cmd := b.commitViewport.Update(msg)
- b.commitViewport = rv.(*vp.ViewportBubble)
- cmds = append(cmds, cmd)
- }
- return b, tea.Batch(cmds...)
- case itemsMsg:
- b.loading = false
- b.list.Paginator.Page = b.nextPage
- if b.state != commitState {
- b.state = logState
- }
- case countMsg:
- b.count = int64(msg)
- case common.ErrMsg:
- b.error = msg
- b.state = errorState
- b.loading = false
- return b, nil
- case commitMsg:
- b.loading = false
- b.state = commitState
- case refs.RefMsg:
- b.ref = msg
- b.count = 0
- cmds = append(cmds, b.countCommits)
- case spinner.TickMsg:
- if b.loading {
- s, cmd := b.spinner.Update(msg)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- b.spinner = s
- }
- }
-
- return b, tea.Batch(cmds...)
-}
-
-func (b *Bubble) loadPatch(c *git.Commit) error {
- var patch strings.Builder
- style := b.style.LogCommit.Copy().Width(b.width - b.widthMargin - b.style.LogCommit.GetHorizontalFrameSize())
- p, err := b.repo.Diff(c)
- if err != nil {
- return err
- }
- stats := strings.Split(p.Stats().String(), "\n")
- for i, l := range stats {
- ch := strings.Split(l, "|")
- if len(ch) > 1 {
- adddel := ch[len(ch)-1]
- adddel = strings.ReplaceAll(adddel, "+", b.style.LogCommitStatsAdd.Render("+"))
- adddel = strings.ReplaceAll(adddel, "-", b.style.LogCommitStatsDel.Render("-"))
- stats[i] = strings.Join(ch[:len(ch)-1], "|") + "|" + adddel
- }
- }
- patch.WriteString(b.renderCommit(c))
- fpl := len(p.Files)
- if fpl > common.MaxDiffFiles {
- patch.WriteString("\n" + common.ErrDiffFilesTooLong.Error())
- } else {
- patch.WriteString("\n" + strings.Join(stats, "\n"))
- }
- if fpl <= common.MaxDiffFiles {
- ps := ""
- if len(strings.Split(ps, "\n")) > common.MaxDiffLines {
- patch.WriteString("\n" + common.ErrDiffTooLong.Error())
- } else {
- patch.WriteString("\n" + b.renderDiff(p))
- }
- }
- content := style.Render(patch.String())
- b.commitViewport.Viewport.SetContent(content)
- b.GotoTop()
- return nil
-}
-
-func (b *Bubble) loadCommit() tea.Msg {
- b.loading = true
- b.loadingStart = time.Now()
- c := b.selectedCommit
- if err := b.loadPatch(c); err != nil {
- return common.ErrMsg{Err: err}
- }
- return commitMsg(c)
-}
-
-func (b *Bubble) renderCommit(c *git.Commit) string {
- s := strings.Builder{}
- // FIXME: lipgloss prints empty lines when CRLF is used
- // sanitize commit message from CRLF
- msg := strings.ReplaceAll(c.Message, "\r\n", "\n")
- s.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n",
- b.style.LogCommitHash.Render("commit "+c.ID.String()),
- b.style.LogCommitAuthor.Render(fmt.Sprintf("Author: %s <%s>", c.Author.Name, c.Author.Email)),
- b.style.LogCommitDate.Render("Date: "+c.Committer.When.Format(time.UnixDate)),
- b.style.LogCommitBody.Render(msg),
- ))
- return s.String()
-}
-
-func (b *Bubble) renderDiff(diff *git.Diff) string {
- var s strings.Builder
- var pr strings.Builder
- diffChroma.Code = diff.Patch()
- err := diffChroma.Render(&pr, common.RenderCtx)
- if err != nil {
- s.WriteString(fmt.Sprintf("\n%s", err.Error()))
- } else {
- s.WriteString(fmt.Sprintf("\n%s", pr.String()))
- }
- return s.String()
-}
-
-func (b *Bubble) View() string {
- if b.loading && b.loadingStart.Add(waitBeforeLoading).Before(time.Now()) {
- msg := fmt.Sprintf("%s loading commit", b.spinner.View())
- if b.selectedCommit == nil {
- msg += "s"
- }
- msg += "…"
- return msg
- }
- switch b.state {
- case logState:
- return b.list.View()
- case errorState:
- return b.error.ViewWithPrefix(b.style, "Error")
- case commitState:
- return b.commitViewport.View()
- default:
- return ""
- }
-}
@@ -1,185 +0,0 @@
-package refs
-
-import (
- "fmt"
- "io"
- "sort"
-
- "github.com/charmbracelet/bubbles/list"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/charmbracelet/soft-serve/tui/common"
-)
-
-type RefMsg = *git.Reference
-
-type item struct {
- *git.Reference
-}
-
-func (i item) Short() string {
- return i.Reference.Name().Short()
-}
-
-func (i item) FilterValue() string { return i.Short() }
-
-type items []item
-
-func (cl items) Len() int { return len(cl) }
-func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
-func (cl items) Less(i, j int) bool {
- return cl[i].Short() < cl[j].Short()
-}
-
-type itemDelegate struct {
- style *style.Styles
-}
-
-func (d itemDelegate) Height() int { return 1 }
-func (d itemDelegate) Spacing() int { return 0 }
-func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
-func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
- s := d.style
- i, ok := listItem.(item)
- if !ok {
- return
- }
-
- ref := i.Short()
- if i.Reference.IsTag() {
- ref = s.RefItemTag.Render(ref)
- }
- ref = s.RefItemBranch.Render(ref)
- refMaxWidth := m.Width() -
- s.RefItemSelector.GetMarginLeft() -
- s.RefItemSelector.GetWidth() -
- s.RefItemInactive.GetMarginLeft()
- ref = common.TruncateString(ref, refMaxWidth, "…")
- if index == m.Index() {
- fmt.Fprint(w, s.RefItemSelector.Render(">")+
- s.RefItemActive.Render(ref))
- } else {
- fmt.Fprint(w, s.LogItemSelector.Render(" ")+
- s.RefItemInactive.Render(ref))
- }
-}
-
-type Bubble struct {
- repo common.GitRepo
- list list.Model
- style *style.Styles
- width int
- widthMargin int
- height int
- heightMargin int
- ref *git.Reference
-}
-
-func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
- head, err := repo.HEAD()
- if err != nil {
- return nil
- }
- l := list.NewModel([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
- l.SetShowFilter(false)
- l.SetShowHelp(false)
- l.SetShowPagination(true)
- l.SetShowStatusBar(false)
- l.SetShowTitle(false)
- l.SetFilteringEnabled(false)
- l.DisableQuitKeybindings()
- b := &Bubble{
- repo: repo,
- style: styles,
- width: width,
- height: height,
- widthMargin: widthMargin,
- heightMargin: heightMargin,
- list: l,
- ref: head,
- }
- b.SetSize(width, height)
- return b
-}
-
-func (b *Bubble) SetBranch(ref *git.Reference) (tea.Model, tea.Cmd) {
- b.ref = ref
- return b, func() tea.Msg {
- return RefMsg(ref)
- }
-}
-
-func (b *Bubble) reset() tea.Cmd {
- cmd := b.updateItems()
- b.SetSize(b.width, b.height)
- return cmd
-}
-
-func (b *Bubble) Init() tea.Cmd {
- return nil
-}
-
-func (b *Bubble) SetSize(width, height int) {
- b.width = width
- b.height = height
- b.list.SetSize(width-b.widthMargin, height-b.heightMargin)
- b.list.Styles.PaginationStyle = b.style.RefPaginator.Copy().Width(width - b.widthMargin)
-}
-
-func (b *Bubble) Help() []common.HelpEntry {
- return nil
-}
-
-func (b *Bubble) updateItems() tea.Cmd {
- its := make(items, 0)
- tags := make(items, 0)
- refs, err := b.repo.References()
- if err != nil {
- return func() tea.Msg { return common.ErrMsg{Err: err} }
- }
- for _, r := range refs {
- if r.IsTag() {
- tags = append(tags, item{r})
- } else if r.IsBranch() {
- its = append(its, item{r})
- }
- }
- sort.Sort(its)
- sort.Sort(tags)
- its = append(its, tags...)
- itt := make([]list.Item, len(its))
- for i, it := range its {
- itt[i] = it
- }
- return b.list.SetItems(itt)
-}
-
-func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- cmds := make([]tea.Cmd, 0)
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- b.SetSize(msg.Width, msg.Height)
-
- case tea.KeyMsg:
- switch msg.String() {
- case "B":
- return b, b.reset()
- case "enter", "right", "l":
- if b.list.Index() >= 0 {
- ref := b.list.SelectedItem().(item).Reference
- return b.SetBranch(ref)
- }
- }
- }
-
- l, cmd := b.list.Update(msg)
- b.list = l
- cmds = append(cmds, cmd)
-
- return b, tea.Batch(cmds...)
-}
-
-func (b *Bubble) View() string {
- return b.list.View()
-}
@@ -1,341 +0,0 @@
-package tree
-
-import (
- "fmt"
- "io"
- "io/fs"
- "path/filepath"
- "strings"
-
- "github.com/charmbracelet/bubbles/list"
- "github.com/charmbracelet/bubbles/viewport"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/charmbracelet/soft-serve/tui/common"
- "github.com/charmbracelet/soft-serve/tui/refs"
- vp "github.com/charmbracelet/soft-serve/tui/viewport"
- "github.com/dustin/go-humanize"
-)
-
-type fileMsg struct {
- content string
-}
-
-type sessionState int
-
-const (
- treeState sessionState = iota
- fileState
- errorState
-)
-
-type item struct {
- entry *git.TreeEntry
-}
-
-func (i item) Name() string {
- return i.entry.Name()
-}
-
-func (i item) Mode() fs.FileMode {
- return i.entry.Mode()
-}
-
-func (i item) FilterValue() string { return i.Name() }
-
-type items []item
-
-func (cl items) Len() int { return len(cl) }
-func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
-func (cl items) Less(i, j int) bool {
- if cl[i].entry.IsTree() && cl[j].entry.IsTree() {
- return cl[i].Name() < cl[j].Name()
- } else if cl[i].entry.IsTree() {
- return true
- } else if cl[j].entry.IsTree() {
- return false
- } else {
- return cl[i].Name() < cl[j].Name()
- }
-}
-
-type itemDelegate struct {
- style *style.Styles
-}
-
-func (d itemDelegate) Height() int { return 1 }
-func (d itemDelegate) Spacing() int { return 0 }
-func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
-func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
- s := d.style
- i, ok := listItem.(item)
- if !ok {
- return
- }
-
- name := i.Name()
- size := humanize.Bytes(uint64(i.entry.Size()))
- if i.entry.IsTree() {
- size = ""
- name = s.TreeFileDir.Render(name)
- }
- var cs lipgloss.Style
- mode := i.Mode()
- if index == m.Index() {
- cs = s.TreeItemActive
- fmt.Fprint(w, s.TreeItemSelector.Render(">"))
- } else {
- cs = s.TreeItemInactive
- fmt.Fprint(w, s.TreeItemSelector.Render(" "))
- }
- leftMargin := s.TreeItemSelector.GetMarginLeft() +
- s.TreeItemSelector.GetWidth() +
- s.TreeFileMode.GetMarginLeft() +
- s.TreeFileMode.GetWidth() +
- cs.GetMarginLeft()
- rightMargin := s.TreeFileSize.GetMarginLeft() + lipgloss.Width(size)
- name = common.TruncateString(name, m.Width()-leftMargin-rightMargin, "…")
- sizeStyle := s.TreeFileSize.Copy().
- Width(m.Width() -
- leftMargin -
- s.TreeFileSize.GetMarginLeft() -
- lipgloss.Width(name)).
- Align(lipgloss.Right)
- fmt.Fprint(w, s.TreeFileMode.Render(mode.String())+
- cs.Render(name)+
- sizeStyle.Render(size))
-}
-
-type Bubble struct {
- repo common.GitRepo
- list list.Model
- style *style.Styles
- width int
- widthMargin int
- height int
- heightMargin int
- path string
- state sessionState
- error common.ErrMsg
- fileViewport *vp.ViewportBubble
- lastSelected []int
- ref *git.Reference
-}
-
-func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
- l := list.New([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
- l.SetShowFilter(false)
- l.SetShowHelp(false)
- l.SetShowPagination(true)
- l.SetShowStatusBar(false)
- l.SetShowTitle(false)
- l.SetFilteringEnabled(false)
- l.DisableQuitKeybindings()
- l.KeyMap.NextPage = common.NextPage
- l.KeyMap.PrevPage = common.PrevPage
- l.Styles.NoItems = styles.TreeNoItems
- b := &Bubble{
- fileViewport: &vp.ViewportBubble{
- Viewport: &viewport.Model{},
- },
- repo: repo,
- style: styles,
- width: width,
- height: height,
- widthMargin: widthMargin,
- heightMargin: heightMargin,
- list: l,
- state: treeState,
- }
- b.SetSize(width, height)
- return b
-}
-
-func (b *Bubble) reset() tea.Cmd {
- b.path = ""
- b.state = treeState
- b.lastSelected = make([]int, 0)
- cmd := b.updateItems()
- return cmd
-}
-
-func (b *Bubble) Init() tea.Cmd {
- head, err := b.repo.HEAD()
- if err != nil {
- return func() tea.Msg {
- return common.ErrMsg{Err: err}
- }
- }
- b.ref = head
- return nil
-}
-
-func (b *Bubble) SetSize(width, height int) {
- b.width = width
- b.height = height
- b.fileViewport.Viewport.Width = width - b.widthMargin
- b.fileViewport.Viewport.Height = height - b.heightMargin
- b.list.SetSize(width-b.widthMargin, height-b.heightMargin)
- b.list.Styles.PaginationStyle = b.style.LogPaginator.Copy().Width(width - b.widthMargin)
-}
-
-func (b *Bubble) Help() []common.HelpEntry {
- return nil
-}
-
-func (b *Bubble) updateItems() tea.Cmd {
- files := make([]list.Item, 0)
- dirs := make([]list.Item, 0)
- t, err := b.repo.Tree(b.ref, b.path)
- if err != nil {
- return func() tea.Msg { return common.ErrMsg{Err: err} }
- }
- ents, err := t.Entries()
- if err != nil {
- return func() tea.Msg { return common.ErrMsg{Err: err} }
- }
- ents.Sort()
- for _, e := range ents {
- if e.IsTree() {
- dirs = append(dirs, item{e})
- } else {
- files = append(files, item{e})
- }
- }
- cmd := b.list.SetItems(append(dirs, files...))
- b.list.Select(0)
- b.SetSize(b.width, b.height)
- return cmd
-}
-
-func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- cmds := make([]tea.Cmd, 0)
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- b.SetSize(msg.Width, msg.Height)
-
- case tea.KeyMsg:
- if b.state == errorState {
- ref, _ := b.repo.HEAD()
- b.ref = ref
- return b, tea.Batch(b.reset(), func() tea.Msg {
- return ref
- })
- }
-
- switch msg.String() {
- case "F":
- return b, b.reset()
- case "enter", "right", "l":
- if len(b.list.Items()) > 0 && b.state == treeState {
- index := b.list.Index()
- item := b.list.SelectedItem().(item)
- mode := item.Mode()
- b.path = filepath.Join(b.path, item.Name())
- if mode.IsDir() {
- b.lastSelected = append(b.lastSelected, index)
- cmds = append(cmds, b.updateItems())
- } else {
- b.lastSelected = append(b.lastSelected, index)
- cmds = append(cmds, b.loadFile(item))
- }
- }
- case "esc", "left", "h":
- if b.state != treeState {
- b.state = treeState
- }
- p := filepath.Dir(b.path)
- b.path = p
- cmds = append(cmds, b.updateItems())
- index := 0
- if len(b.lastSelected) > 0 {
- index = b.lastSelected[len(b.lastSelected)-1]
- b.lastSelected = b.lastSelected[:len(b.lastSelected)-1]
- }
- b.list.Select(index)
- }
-
- case refs.RefMsg:
- b.ref = msg
- return b, b.reset()
-
- case common.ErrMsg:
- b.error = msg
- b.state = errorState
- return b, nil
-
- case fileMsg:
- content := b.renderFile(msg)
- b.fileViewport.Viewport.SetContent(content)
- b.fileViewport.Viewport.GotoTop()
- b.state = fileState
- }
-
- switch b.state {
- case fileState:
- rv, cmd := b.fileViewport.Update(msg)
- b.fileViewport = rv.(*vp.ViewportBubble)
- cmds = append(cmds, cmd)
- case treeState:
- l, cmd := b.list.Update(msg)
- b.list = l
- cmds = append(cmds, cmd)
- }
-
- return b, tea.Batch(cmds...)
-}
-
-func (b *Bubble) View() string {
- switch b.state {
- case treeState:
- return b.list.View()
- case errorState:
- return b.error.ViewWithPrefix(b.style, "Error")
- case fileState:
- return b.fileViewport.View()
- default:
- return ""
- }
-}
-
-func (b *Bubble) loadFile(i item) tea.Cmd {
- return func() tea.Msg {
- f := i.entry.File()
- if i.Mode().IsDir() || f == nil {
- return common.ErrMsg{Err: common.ErrInvalidFile}
- }
- bin, err := f.IsBinary()
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- if bin {
- return common.ErrMsg{Err: common.ErrBinaryFile}
- }
- c, err := f.Bytes()
- if err != nil {
- return common.ErrMsg{Err: err}
- }
- return fileMsg{
- content: string(c),
- }
- }
-}
-
-func (b *Bubble) renderFile(m fileMsg) string {
- s := strings.Builder{}
- c := m.content
- if len(strings.Split(c, "\n")) > common.MaxDiffLines {
- s.WriteString(b.style.TreeNoItems.Render(common.ErrFileTooLarge.Error()))
- } else {
- w := b.width - b.widthMargin - b.style.RepoBody.GetHorizontalFrameSize()
- f, err := common.RenderFile(b.path, m.content, w)
- if err != nil {
- s.WriteString(err.Error())
- } else {
- s.WriteString(f)
- }
- }
- return b.style.TreeFileContent.Copy().Width(b.width - b.widthMargin).Render(s.String())
-}
@@ -1,24 +0,0 @@
-package viewport
-
-import (
- "github.com/charmbracelet/bubbles/viewport"
- tea "github.com/charmbracelet/bubbletea"
-)
-
-type ViewportBubble struct {
- Viewport *viewport.Model
-}
-
-func (v *ViewportBubble) Init() tea.Cmd {
- return nil
-}
-
-func (v *ViewportBubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- vp, cmd := v.Viewport.Update(msg)
- v.Viewport = &vp
- return v, cmd
-}
-
-func (v *ViewportBubble) View() string {
- return v.Viewport.View()
-}
@@ -0,0 +1,18 @@
+package common
+
+import (
+ "github.com/charmbracelet/glamour"
+ gansi "github.com/charmbracelet/glamour/ansi"
+)
+
+// StyleConfig returns the default Glamour style configuration.
+func StyleConfig() gansi.StyleConfig {
+ noColor := ""
+ s := glamour.DarkStyleConfig
+ // This fixes an issue with the default style config. For example
+ // highlighting empty spaces with red in Dockerfile type.
+ s.Document.StylePrimitive.Color = &noColor
+ s.CodeBlock.Chroma.Text.Color = &noColor
+ s.CodeBlock.Chroma.Name.Color = &noColor
+ return s
+}
@@ -36,7 +36,7 @@ func New(c common.Common, content, extension string) *Code {
Viewport: vp.New(c),
NoContentStyle: c.Styles.CodeNoContent.Copy(),
}
- st := styleConfig()
+ st := common.StyleConfig()
r.styleConfig = st
r.renderContext = gansi.NewRenderContext(gansi.Options{
ColorProfile: termenv.TrueColor,
@@ -196,14 +196,3 @@ func (r *Code) renderFile(path, content string, width int) (string, error) {
}
return s.String(), nil
}
-
-func styleConfig() gansi.StyleConfig {
- noColor := ""
- s := glamour.DarkStyleConfig
- // This fixes an issue with the default style config. For example
- // highlighting empty spaces with red in Dockerfile type.
- s.Document.StylePrimitive.Color = &noColor
- s.CodeBlock.Chroma.Text.Color = &noColor
- s.CodeBlock.Chroma.Name.Color = &noColor
- return s
-}
@@ -1,64 +0,0 @@
-package copy
-
-import (
- "github.com/aymanbagabas/go-osc52"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
-)
-
-// CopyMsg is a message that is sent when the user copies text.
-type CopyMsg string
-
-// CopyCmd is a command that copies text to the clipboard using OSC52.
-func CopyCmd(output *osc52.Output, str string) tea.Cmd {
- return func() tea.Msg {
- output.Copy(str)
- return CopyMsg(str)
- }
-}
-
-type Copy struct {
- output *osc52.Output
- text string
- copied bool
- CopiedStyle lipgloss.Style
- TextStyle lipgloss.Style
-}
-
-func New(output *osc52.Output, text string) *Copy {
- copy := &Copy{
- output: output,
- text: text,
- }
- return copy
-}
-
-func (c *Copy) SetText(text string) {
- c.text = text
-}
-
-func (c *Copy) Init() tea.Cmd {
- c.copied = false
- return nil
-}
-
-func (c *Copy) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg.(type) {
- case CopyMsg:
- c.copied = true
- default:
- c.copied = false
- }
- return c, nil
-}
-
-func (c *Copy) View() string {
- if c.copied {
- return c.CopiedStyle.String()
- }
- return c.TextStyle.Render(c.text)
-}
-
-func (c *Copy) CopyCmd() tea.Cmd {
- return CopyCmd(c.output, c.text)
-}
@@ -1,6 +1,8 @@
package footer
import (
+ "strings"
+
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
@@ -54,7 +56,7 @@ func (f *Footer) View() string {
}
s := f.common.Styles.Footer.Copy().Width(f.common.Width)
helpView := f.help.View(f.keymap)
- return s.Render(helpView)
+ return s.Render(strings.TrimSpace(helpView))
}
// ShortHelp returns the short help key bindings.
@@ -24,8 +24,7 @@ func New(c common.Common, text string) *Header {
// SetSize implements common.Component.
func (h *Header) SetSize(width, height int) {
- h.common.Width = width
- h.common.Height = height
+ h.common.SetSize(width, height)
}
// Init implements tea.Model.
@@ -1,61 +0,0 @@
-package yankable
-
-import (
- "io"
-
- "github.com/aymanbagabas/go-osc52"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
-)
-
-type Yankable struct {
- yankStyle lipgloss.Style
- style lipgloss.Style
- text string
- clicked bool
- osc52 *osc52.Output
-}
-
-func New(w io.Writer, environ []string, style, yankStyle lipgloss.Style, text string) *Yankable {
- return &Yankable{
- yankStyle: yankStyle,
- style: style,
- text: text,
- clicked: false,
- osc52: osc52.NewOutput(w, environ),
- }
-}
-
-func (y *Yankable) SetText(text string) {
- y.text = text
-}
-
-func (y *Yankable) Init() tea.Cmd {
- return nil
-}
-
-func (y *Yankable) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.MouseMsg:
- switch msg.Type {
- case tea.MouseRight:
- y.clicked = true
- return y, y.copy()
- }
- default:
- y.clicked = false
- }
- return y, nil
-}
-
-func (y *Yankable) View() string {
- if y.clicked {
- return y.yankStyle.String()
- }
- return y.style.Render(y.text)
-}
-
-func (y *Yankable) copy() tea.Cmd {
- y.osc52.Copy(y.text)
- return nil
-}
@@ -65,6 +65,8 @@ func (ui *UI) getMargins() (wm, hm int) {
wm = ui.common.Styles.App.GetHorizontalFrameSize()
hm = ui.common.Styles.App.GetVerticalFrameSize() +
ui.common.Styles.Header.GetHeight() +
+ ui.common.Styles.Header.GetVerticalFrameSize() +
+ ui.common.Styles.Footer.GetVerticalFrameSize() +
ui.footer.Height()
return
}