Skip to content

View wraps on macOS Terminal.app despite calculating widths #1471

@amoeba

Description

@amoeba

Describe the bug

The code sample below shows what might be a bug which is that, only on macOS Terminal.app, trying to render a View to the full-width of the terminal causes wrapping which breaks what I'm trying to achieve. All other terminals (including iTerm, kitty, Alacritty) render as expected.

I may be using the APIs incorrectly or not using an API I should be.

Setup

Please complete the following information along with version numbers, if applicable.

  • OS: macOS 15
  • Shell: zsh, fish
  • Terminal Emulator: Terminal.app
  • Terminal Multiplexer: None

To Reproduce
Steps to reproduce the behavior:

  1. Create a go package and go run the source

Source Code

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/progress"
	"github.com/charmbracelet/bubbles/spinner"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type model struct {
	n        int
	current  int
	spinner  spinner.Model
	progress progress.Model
	done     bool
	width    int
}

type tickMsg struct{}

func newModel() model {
	s := spinner.New()
	s.Spinner = spinner.Dot
	p := progress.New()

	return model{
		n:        10,
		spinner:  s,
		progress: p,
		width:    80,
	}
}

func (m model) Init() tea.Cmd {
	return tea.Batch(m.spinner.Tick, tick())
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		return m, tea.Quit
	case tea.WindowSizeMsg:
		m.width = msg.Width
		return m, nil
	case tickMsg:
		if m.current >= m.n {
			m.done = true
			return m, tea.Quit
		}

		m.current++
		percent := float64(m.current) / float64(m.n)
		cmd := m.progress.SetPercent(percent)

		return m, tea.Batch(cmd, tick())
	default:
		var cmds []tea.Cmd

		var cmd tea.Cmd
		m.spinner, cmd = m.spinner.Update(msg)
		cmds = append(cmds, cmd)

		progressModel, cmd := m.progress.Update(msg)
		m.progress = progressModel.(progress.Model)
		cmds = append(cmds, cmd)

		return m, tea.Batch(cmds...)
	}
}

func (m model) View() string {
	if m.done || m.current >= m.n {
		return ""
	}

	status := fmt.Sprintf("%s Running... ", m.spinner.View())

	progressBarWidth := m.width / 2
	m.progress.Width = progressBarWidth
	progressBar := m.progress.View()

	statusWidth := lipgloss.Width(status)
	progressWidth := lipgloss.Width(progressBar)
	gapWidth := m.width - statusWidth - progressWidth

	// This is what we want to call
	gap := strings.Repeat(" ", gapWidth)

	// This is what we have to call on macOS Terminal.app
	// gap := strings.Repeat(" ", gapWidth - 1)

	return status + gap + progressBar
}

func tick() tea.Cmd {
	return tea.Tick(300*time.Millisecond, func(time.Time) tea.Msg {
		return tickMsg{}
	})
}

func main() {
	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
		fmt.Printf("Error: %v\n", err)
	}
}

Expected behavior

I expect to be able to do my width calculation like shown in the source code snippet and to see rendering like what's shown in the iTerm 2 GIF.

Screenshots

Here's Terminal.app, showing that the line wraps which makes the rendering look wonky:

Image

Here's iTerm2, showing the desired behavior:

Image

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions