Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2ff708f
chore: update Go toolchain and CI matrix
gdamore Mar 29, 2026
9b89b30
fix(vt): synchronize emulator and mock state
gdamore Apr 2, 2026
64ee32a
test(vt): avoid repeat timing boundary flake
gdamore Apr 2, 2026
4aedf03
test(vt): cover emulator mode helpers
gdamore Apr 2, 2026
94ae18d
Add screen option to disable alt screen
gdamore Apr 2, 2026
6f49191
test(vt): avoid caps lock repeat timing flake
gdamore Apr 2, 2026
3cc2acf
chore(deps): bump github.com/lucasb-eyer/go-colorful from 1.3.0 to 1.4.0
dependabot[bot] Mar 30, 2026
a9202ca
chore(deps): bump golang.org/x/text from 0.33.0 to 0.35.0
dependabot[bot] Mar 16, 2026
193aaa7
chore(deps): bump codecov/codecov-action from 5 to 6
dependabot[bot] Apr 2, 2026
062febf
chore(deps): bump golang.org/x/sys from 0.41.0 to 0.42.0
dependabot[bot] Apr 2, 2026
1ce3aa5
test(vt): avoid Windows repeat timing flake
gdamore Apr 2, 2026
bea6721
fix(tty): flush input before start (fixes #1045)
gdamore Apr 3, 2026
dfd7185
chore(deps): bump golang.org/x/term from 0.40.0 to 0.41.0
dependabot[bot] Apr 2, 2026
0387a59
ci: only run the CI on main and on PRs
gdamore Apr 4, 2026
14b46da
fix(mouse): work around Ghostty false motion press bug
noborus Apr 6, 2026
c517008
Fix expected bell count in beep_test.go
wangzhione Apr 9, 2026
21c482b
style: unify OSC spelling in comments
CatsDeservePets Apr 6, 2026
1f1348a
fix(input): handle ESC during CSI and SS3 parse states per ECMA-48
ModeEngage Apr 11, 2026
73733d6
test: use firstKey helper to capture first event, not last
ModeEngage Apr 11, 2026
e384873
chore(deps): bump golang.org/x/sys from 0.42.0 to 0.43.0
dependabot[bot] Apr 13, 2026
d053f9e
feat(events): add Unwrap() to EventError
Tubbles Apr 17, 2026
5a90190
chore(deps): bump golang.org/x/text from 0.35.0 to 0.36.0
dependabot[bot] Apr 19, 2026
759b3fd
chore(deps): bump golang.org/x/term from 0.41.0 to 0.42.0
dependabot[bot] Apr 19, 2026
14f3ad4
Move away from uniseg to faster clipperhouse/displaywidth
gdamore Apr 19, 2026
2efac63
demos: count repeat key strokes in mouse demo
gdamore Apr 19, 2026
ade6a46
Key mishandling on Windows when using Alacritty or WezTerm (fixes #1062)
gdamore Apr 20, 2026
7e0c93c
Sanitize OSC 8 links (fixes #1061)
gdamore Apr 20, 2026
29b8586
Sanitize OSC 8 links (fixes #1061)
gdamore Apr 20, 2026
9ae29a0
fix: possible panic in getConsoleInput if no event returned
AntoineGS Apr 20, 2026
8cf3a1b
Sanitize titles and notifications (fixes #1066)
gdamore Apr 21, 2026
35af4bc
feat: Optionally sanitize content before putting to the terminal (fix…
gdamore Apr 22, 2026
8956f2d
test: additional coverage tests
gdamore Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
name: linux
on: [push]

on:
pull_request:
push:
branches:
- main

jobs:
build:
name: build
runs-on: [ubuntu-latest]
strategy:
fail-fast: false
matrix:
go: ["stable", "oldstable"]
steps:
Expand All @@ -30,6 +37,6 @@ jobs:
run: script -q -e -c "go test -coverpkg=github.com/gdamore/tcell/v3/... -covermode=count -coverprofile=coverage.txt ./..."

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
11 changes: 9 additions & 2 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
name: macos
on: [push]

on:
pull_request:
push:
branches:
- main

jobs:
build:
name: build
runs-on: [macos-latest]
strategy:
fail-fast: false
matrix:
go: ["stable", "oldstable"]
steps:
Expand All @@ -30,6 +37,6 @@ jobs:
run: script -q /dev/null go test -coverpkg="github.com/gdamore/tcell/v3/..." -covermode=count -coverprofile="coverage.txt" ./...

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
9 changes: 8 additions & 1 deletion .github/workflows/webasm.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
name: webasm
on: [push]

on:
pull_request:
push:
branches:
- main

jobs:
build:
name: build
runs-on: [ubuntu-latest]
strategy:
fail-fast: false
matrix:
go: ["stable", "oldstable"]
steps:
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
name: windows
on: [push]
on:
pull_request:
push:
branches:
- main
jobs:
build:
name: build
runs-on: [windows-latest]
strategy:
fail-fast: false
matrix:
go: ["stable", "oldstable"]
steps:
Expand All @@ -27,6 +32,6 @@ jobs:
run: go test -coverpkg="github.com/gdamore/tcell/v3/..." -covermode=count -coverprofile="coverage.txt" ./...

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
7 changes: 7 additions & 0 deletions .superset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"setup": [
"go mod download"
],
"teardown": [],
"run": []
}
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use the `gopls` MCP server by default when working on Go code in this repository, unless I explicitly ask you not to.
13 changes: 13 additions & 0 deletions _demos/mouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ func main() {
lchar := '*'
bstr := ""
lks := ""
lkey := ""
kcnt := 0
pstr := ""
ecnt := 0
pasting := false
Expand Down Expand Up @@ -180,6 +182,12 @@ func main() {
s.Sync()
s.Put(w-1, h-1, "R", st)
case *tcell.EventKey:
if ev.Name() == lkey {
kcnt++
} else {
lkey = ev.Name()
kcnt = 1
}
s.Put(w-2, h-2, ev.Str(), st)
if pasting {
s.Put(w-1, h-1, "P", st)
Expand Down Expand Up @@ -224,6 +232,11 @@ func main() {
s.Clear()
}
}
if ks := fmt.Sprintf("K%d", kcnt); w >= len(ks) {
s.PutStrStyled(w-len(ks), h-1, ks, st)
} else {
s.PutStrStyled(0, h-1, ks, st)
}
lks = ev.Name()
case *tcell.EventPaste:
pasting = ev.Start()
Expand Down
34 changes: 18 additions & 16 deletions cell.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2025 The TCell Authors
// 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.
Expand All @@ -14,10 +14,6 @@

package tcell

import (
"github.com/rivo/uniseg"
)

type cell struct {
currStr string
lastStr string
Expand Down Expand Up @@ -46,28 +42,34 @@ func (c *cell) setDirty(dirty bool) {
//
// CellBuffer is not thread safe.
type CellBuffer struct {
w int
h int
cells []cell
w int
h int
cells []cell
sanitizeContent bool
}

// Put a single styled grapheme using the given string and style
// at the same location. Note that only the first grapheme in the string
// will be displayed, using only the 1 or 2 (depending on width) cells
// located at x, y. It returns the rest of the string, and the width used.
func (cb *CellBuffer) Put(x int, y int, str string, style Style) (string, int) {
if cb.sanitizeContent {
str = stripOSCControlsIfNeeded(str)
}
return cb.put(x, y, str, style)
}

func (cb *CellBuffer) put(x int, y int, str string, style Style) (string, int) {
var width int = 0
if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
var cl string
c := &cb.cells[(y*cb.w)+x]
state := -1
for width == 0 && str != "" {
var g string
g, str, width, state = uniseg.FirstGraphemeClusterInString(str, state)
cl += g
if g == "" {
break
}
g := textWidthOptions.StringGraphemes(str)
for width == 0 && g.Next() {
cluster := g.Value()
cl += cluster
width = g.Width()
str = str[len(cluster):]
}

// Wide characters: we want to mark the "wide" cells
Expand Down
56 changes: 56 additions & 0 deletions cell_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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 tcell

import (
"testing"
)

func BenchmarkCellBufferPutCurrent(b *testing.B) {
benchCellBufferPut(b, "current", false, func(cb *CellBuffer, x, y int, str string, style Style) (string, int) {
return cb.Put(x, y, str, style)
})
}

func BenchmarkCellBufferPutSanitizedCurrent(b *testing.B) {
benchCellBufferPut(b, "sanitized", true, func(cb *CellBuffer, x, y int, str string, style Style) (string, int) {
return cb.Put(x, y, str, style)
})
}

func benchCellBufferPut(b *testing.B, name string, sanitize bool, put func(*CellBuffer, int, int, string, Style) (string, int)) {
cases := []struct {
name string
str string
}{
{name: "ascii", str: "Hello, terminal"},
{name: "combining", str: "e\u0301"},
{name: "wide", str: "宽"},
{name: "emoji", str: "👩‍🚀"},
}

for _, tc := range cases {
b.Run(name+"/"+tc.name, func(b *testing.B) {
cb := &CellBuffer{w: 8, h: 1, cells: make([]cell, 8), sanitizeContent: sanitize}
style := Style{}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
cb.cells[0] = cell{}
_, _ = put(cb, 0, 0, tc.str, style)
}
})
}
}
Loading