diff --git a/server/cmd/cat.go b/server/cmd/cat.go index c5c62707479974d4cf420c61b35b5c047439b808..f8bfd9004b7a0ddd5150d61b0725bfa9297ebab9 100644 --- a/server/cmd/cat.go +++ b/server/cmd/cat.go @@ -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, diff --git a/tui/about/bubble.go b/tui/about/bubble.go deleted file mode 100644 index 9205268e97a49a16846cd81c4cea4d1fd28800e9..0000000000000000000000000000000000000000 --- a/tui/about/bubble.go +++ /dev/null @@ -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 -} diff --git a/tui/bubble.go b/tui/bubble.go deleted file mode 100644 index e23442472c4260ccae1a77523e1e8071212380d9..0000000000000000000000000000000000000000 --- a/tui/bubble.go +++ /dev/null @@ -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...) -} diff --git a/tui/common/consts.go b/tui/common/consts.go deleted file mode 100644 index c915df177fbfe6ca0c6c67c58ae246b39d633f6a..0000000000000000000000000000000000000000 --- a/tui/common/consts.go +++ /dev/null @@ -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"), - ) -) diff --git a/tui/common/error.go b/tui/common/error.go deleted file mode 100644 index b9ecc9594b04ed7a793fee776af2f3835ffa6715..0000000000000000000000000000000000000000 --- a/tui/common/error.go +++ /dev/null @@ -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()), - ) -} diff --git a/tui/common/formatter.go b/tui/common/formatter.go deleted file mode 100644 index 11143ec48a2694437cf63ae9e7728c2ad61198dc..0000000000000000000000000000000000000000 --- a/tui/common/formatter.go +++ /dev/null @@ -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 -} diff --git a/tui/common/git.go b/tui/common/git.go deleted file mode 100644 index 43cb53ca46a632ef64b7685a5186ef9070449f0f..0000000000000000000000000000000000000000 --- a/tui/common/git.go +++ /dev/null @@ -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) -} diff --git a/tui/common/help.go b/tui/common/help.go deleted file mode 100644 index 5bc1a9a8df577879b048fa0bceee2dad4089676d..0000000000000000000000000000000000000000 --- a/tui/common/help.go +++ /dev/null @@ -1,10 +0,0 @@ -package common - -type BubbleHelper interface { - Help() []HelpEntry -} - -type HelpEntry struct { - Key string - Value string -} diff --git a/tui/common/reset.go b/tui/common/reset.go deleted file mode 100644 index fe92b47154c9fe0850563dea7e9f87550c281c36..0000000000000000000000000000000000000000 --- a/tui/common/reset.go +++ /dev/null @@ -1,7 +0,0 @@ -package common - -import tea "github.com/charmbracelet/bubbletea" - -type BubbleReset interface { - Reset() tea.Msg -} diff --git a/tui/common/utils.go b/tui/common/utils.go deleted file mode 100644 index 2cafee64e60735a350363506c578dcf2bb9cb193..0000000000000000000000000000000000000000 --- a/tui/common/utils.go +++ /dev/null @@ -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 -} diff --git a/tui/log/bubble.go b/tui/log/bubble.go deleted file mode 100644 index 30c35069b9930fdb4b97c27e0d8b77117c9fe655..0000000000000000000000000000000000000000 --- a/tui/log/bubble.go +++ /dev/null @@ -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 "" - } -} diff --git a/tui/refs/bubble.go b/tui/refs/bubble.go deleted file mode 100644 index 7a794c2b6af8fbe1c89f726541459edac60fda2d..0000000000000000000000000000000000000000 --- a/tui/refs/bubble.go +++ /dev/null @@ -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() -} diff --git a/tui/tree/bubble.go b/tui/tree/bubble.go deleted file mode 100644 index 74a5ccb3c8b84181179ba2322cf2281d649233e4..0000000000000000000000000000000000000000 --- a/tui/tree/bubble.go +++ /dev/null @@ -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()) -} diff --git a/tui/viewport/viewport_patch.go b/tui/viewport/viewport_patch.go deleted file mode 100644 index 4163b5c40ee90025690f1f4d97d01ac12d310584..0000000000000000000000000000000000000000 --- a/tui/viewport/viewport_patch.go +++ /dev/null @@ -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() -} diff --git a/ui/common/style.go b/ui/common/style.go new file mode 100644 index 0000000000000000000000000000000000000000..434df0d85127a5b162a1f40c6b808a857d7e545c --- /dev/null +++ b/ui/common/style.go @@ -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 +} diff --git a/ui/components/code/code.go b/ui/components/code/code.go index 9ef9dbe9e398077ecb12fc2bff03915601aac523..0df64c5cbe53a5be7aacb2e0213aefad62c23bb8 100644 --- a/ui/components/code/code.go +++ b/ui/components/code/code.go @@ -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 -} diff --git a/ui/components/copy/copy.go b/ui/components/copy/copy.go deleted file mode 100644 index 3eeccdd738844a56633c665d86af4b38490f4b47..0000000000000000000000000000000000000000 --- a/ui/components/copy/copy.go +++ /dev/null @@ -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) -} diff --git a/ui/components/footer/footer.go b/ui/components/footer/footer.go index eb97f8b9820780b77975217e62cbb414fe65678f..91b92952e96cf2abe65eed3f7c0615b4d869dd7e 100644 --- a/ui/components/footer/footer.go +++ b/ui/components/footer/footer.go @@ -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. diff --git a/ui/components/header/header.go b/ui/components/header/header.go index 075cf64ac3a46abe8f375c0c9d51103302e20d31..b490ab1e3f64aac66777a2ec48bf79df10a1c822 100644 --- a/ui/components/header/header.go +++ b/ui/components/header/header.go @@ -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. diff --git a/ui/components/yankable/yankable.go b/ui/components/yankable/yankable.go deleted file mode 100644 index 5539ca2355f609507537accab80c758bc3ca86d0..0000000000000000000000000000000000000000 --- a/ui/components/yankable/yankable.go +++ /dev/null @@ -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 -} diff --git a/ui/ui.go b/ui/ui.go index 87ef9c805a99439ec44669c3468f7ef6e0f755a9..34b76bf85f9b918d1d6d0cf3298bf280522608e3 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -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 }