current commit
260516.2 · vt: prune tab stops after resize (#1105)
// Copyright 2026 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package vt

import (
	"slices"
	"testing"

	"github.com/gdamore/tcell/v3/color"
)

type noMouseBackend struct {
	mb *mockBackend
}

func (n noMouseBackend) GetPrivateMode(pm PrivateMode) ModeStatus { return n.mb.GetPrivateMode(pm) }
func (n noMouseBackend) SetPrivateMode(pm PrivateMode, status ModeStatus) error {
	return n.mb.SetPrivateMode(pm, status)
}
func (n noMouseBackend) GetSize() Coord           { return n.mb.GetSize() }
func (n noMouseBackend) Colors() int              { return n.mb.Colors() }
func (n noMouseBackend) Put(pos Coord, cell Cell) { n.mb.Put(pos, cell) }
func (n noMouseBackend) GetPosition() Coord       { return n.mb.GetPosition() }
func (n noMouseBackend) SetPosition(pos Coord)    { n.mb.SetPosition(pos) }
func (n noMouseBackend) Reset()                   { n.mb.Reset() }
func (n noMouseBackend) RaiseResize()             { n.mb.RaiseResize() }
func (n noMouseBackend) Buffering(enabled bool)   { n.mb.Buffering(enabled) }
func (n noMouseBackend) SetCursor(cs CursorStyle) { n.mb.SetCursor(cs) }

func TestEmulatorModeHelpers(t *testing.T) {
	mb := NewMockBackend().(*mockBackend)
	em := NewEmulator(mb).(*emulator)

	ansiKeys := em.ansiModeKeys()
	if !slices.Contains(ansiKeys, AmNewLineMode) {
		t.Fatalf("expected AmNewLineMode in ansi mode keys, got %v", ansiKeys)
	}

	privateKeys := em.privateModeKeys()
	if !slices.Contains(privateKeys, PmAutoMargin) {
		t.Fatalf("expected PmAutoMargin in private mode keys, got %v", privateKeys)
	}

	em.setAnsiMode(AmNewLineMode, ModeOn)
	if got := em.getAnsiMode(AmNewLineMode); got != ModeOn {
		t.Fatalf("setAnsiMode did not update changeable mode: got %v", got)
	}

	em.setAnsiMode(AmNewLineMode, ModeNA)
	if got := em.getAnsiMode(AmNewLineMode); got != ModeOn {
		t.Fatalf("setAnsiMode changed mode for non-changeable status: got %v", got)
	}

	em.ansiModes[AmInsertReplace] = ModeOnLocked
	em.setAnsiMode(AmInsertReplace, ModeOff)
	if got := em.getAnsiMode(AmInsertReplace); got != ModeOnLocked {
		t.Fatalf("setAnsiMode changed locked mode: got %v", got)
	}

	em.setAnsiMode(AnsiMode(9999), ModeOn)
	if _, ok := em.ansiModes[AnsiMode(9999)]; ok {
		t.Fatalf("setAnsiMode created an unknown mode entry")
	}
}

func TestEmulatorUpdateMouseReporting(t *testing.T) {
	mb := NewMockBackend().(*mockBackend)
	em := NewEmulator(mb).(*emulator)

	em.localModes[PmMouseButton] = ModeOn
	em.updateMouseReporting()
	if em.mouseReports != MouseButtons {
		t.Fatalf("expected mouse buttons reporting, got %v", em.mouseReports)
	}

	em.localModes[PmMouseDrag] = ModeOn
	em.updateMouseReporting()
	if em.mouseReports != MouseDrag {
		t.Fatalf("expected mouse drag reporting, got %v", em.mouseReports)
	}

	em.localModes[PmMouseMotion] = ModeOn
	em.updateMouseReporting()
	if em.mouseReports != MouseMotion {
		t.Fatalf("expected mouse motion reporting, got %v", em.mouseReports)
	}

	em.localModes[PmMouseButton] = ModeOff
	em.localModes[PmMouseDrag] = ModeOff
	em.localModes[PmMouseMotion] = ModeOff
	em.localModes[PmMouseX10] = ModeOn
	em.updateMouseReporting()
	if em.mouseReports != MouseButtons {
		t.Fatalf("expected mouse X10 reporting to map to buttons, got %v", em.mouseReports)
	}

	em.localModes[PmMouseX10] = ModeOff
	em.updateMouseReporting()
	if em.mouseReports != MouseDisabled {
		t.Fatalf("expected mouse reporting disabled, got %v", em.mouseReports)
	}

	noMouse := NewEmulator(noMouseBackend{mb: NewMockBackend().(*mockBackend)}).(*emulator)
	noMouse.updateMouseReporting()
}

func TestMockBackendHelpers(t *testing.T) {
	mb := NewMockBackend().(*mockBackend)

	style := BaseStyle.WithFg(color.Red).WithBg(color.Blue)
	mb.SetStyle(style)
	if mb.style != style {
		t.Fatalf("SetStyle did not update backend style")
	}

	mb.SetMouse(MouseMotion)
	mb.Buffering(true)
	mb.Buffering(false)

	MockOptNoBlit{}.SetMockOpt(mb)
}

func TestApplyResizePrunesTabStops(t *testing.T) {
	em := NewEmulator(NewMockBackend(MockOptSize{X: 80, Y: 24})).(*emulator)
	em.tabStops = []Col{8, 16, 24, 32, 40, 48}

	em.applyResize(Coord{X: 32, Y: 24})

	if got, want := em.tabStops, []Col{8, 16, 24}; !slices.Equal(got, want) {
		t.Fatalf("unexpected tab stops after resize: got %v, want %v", got, want)
	}

	em.setPosition(Coord{X: 0, Y: 0})
	for _, want := range []Col{8, 16, 24, 31} {
		em.nextTab()
		if got := em.getPosition().X; got != want {
			t.Fatalf("unexpected tab position after resize: got %d, want %d", got, want)
		}
	}
}

func TestPutRuneGraphemeExtensions(t *testing.T) {
	t.Parallel()

	newEm := func() *emulator {
		em := NewEmulator(NewMockBackend(MockOptSize{X: 8, Y: 1}, MockOptColors(0))).(*emulator)
		em.style = BaseStyle.WithFg(color.White).WithBg(color.Black)
		em.defaultStyle = em.style
		em.localModes[PmGraphemeClusters] = ModeOn
		em.localModes[PmAutoMargin] = ModeOn
		em.cells[0].S = em.style
		em.cells[0].W = 1
		return em
	}

	t.Run("combining", func(t *testing.T) {
		em := newEm()
		em.cells[0].C = "e"
		em.lastIndex = 1
		em.pos = Coord{X: 1, Y: 0}

		em.putRune('\u0301')

		if got := em.cells[0].C; got != "e\u0301" {
			t.Fatalf("unexpected cluster: %q", got)
		}
		if got := em.cells[0].W; got != 1 {
			t.Fatalf("unexpected width: got %d, want 1", got)
		}
	})

	t.Run("variation-selector", func(t *testing.T) {
		em := newEm()
		em.cells[0].C = "\u2764"
		em.lastIndex = 1
		em.pos = Coord{X: 1, Y: 0}

		em.putRune('\uFE0F')

		if got := em.cells[0].W; got != 2 {
			t.Fatalf("unexpected width: got %d, want 2", got)
		}
	})

	t.Run("regional-indicator", func(t *testing.T) {
		em := newEm()
		em.cells[0].C = "\U0001F1E6"
		em.lastIndex = 1
		em.pos = Coord{X: 1, Y: 0}

		em.putRune('\U0001F1E7')

		if got := em.cells[0].W; got != 2 {
			t.Fatalf("unexpected width: got %d, want 2", got)
		}
	})
}