Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
nightly:
strategy:
matrix:
go-version: [1.23.x]
go-version: [1.25.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
release:
strategy:
matrix:
go-version: [1.23.x]
go-version: [1.25.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.19.x, 1.23.x]
go-version: [1.25.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
25 changes: 8 additions & 17 deletions cmd/micro/micro.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"flag"
"fmt"
"io"
Expand All @@ -16,7 +17,8 @@ import (
"syscall"
"time"

"github.com/go-errors/errors"
"github.com/gdamore/tcell/v3"
goerrors "github.com/go-errors/errors"
isatty "github.com/mattn/go-isatty"
"github.com/micro-editor/micro/v2/internal/action"
"github.com/micro-editor/micro/v2/internal/buffer"
Expand All @@ -25,7 +27,6 @@ import (
"github.com/micro-editor/micro/v2/internal/screen"
"github.com/micro-editor/micro/v2/internal/shell"
"github.com/micro-editor/micro/v2/internal/util"
"github.com/micro-editor/tcell/v2"
lua "github.com/yuin/gopher-lua"
)

Expand Down Expand Up @@ -393,7 +394,7 @@ func main() {
if e, ok := err.(*lua.ApiError); ok {
fmt.Println("Lua API error:", e)
} else {
fmt.Println("Micro encountered an error:", errors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/micro-editor/micro/issues")
fmt.Println("Micro encountered an error:", goerrors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/micro-editor/micro/issues")
}
// immediately backup all buffers with unsaved changes
for _, b := range buffer.OpenBuffers {
Expand Down Expand Up @@ -463,19 +464,9 @@ func main() {
config.SetAutoTime(a)
}

screen.Events = make(chan tcell.Event)

// Here is the event loop which runs in a separate thread
go func() {
for {
screen.Lock()
e := screen.Screen.PollEvent()
screen.Unlock()
if e != nil {
screen.Events <- e
}
}
}()
// screen.Events is populated by screen.Init() with the tcell v3
// EventQ() channel; the former manual PollEvent goroutine is
// no longer needed (tcell v3 drives its own reader loop).

// clear the drawchan so we don't redraw excessively
// if someone requested a redraw before we started displaying
Expand Down Expand Up @@ -542,7 +533,7 @@ func DoEvent() {
if e, ok := event.(*tcell.EventError); ok {
log.Println("tcell event error: ", e.Error())

if e.Err() == io.EOF {
if errors.Is(e, io.EOF) {
// shutdown due to terminal closing/becoming inaccessible
exit(0)
}
Expand Down
155 changes: 107 additions & 48 deletions cmd/micro/micro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,54 @@ import (
"log"
"os"
"testing"
"time"

"github.com/gdamore/tcell/v3"
"github.com/gdamore/tcell/v3/vt"
"github.com/go-errors/errors"
"github.com/micro-editor/micro/v2/internal/action"
"github.com/micro-editor/micro/v2/internal/buffer"
"github.com/micro-editor/micro/v2/internal/config"
"github.com/micro-editor/micro/v2/internal/screen"
"github.com/micro-editor/micro/v2/internal/util"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert"
)

var tempDir string
var sim tcell.SimulationScreen
var mt vt.MockTerm

// settleEvents waits briefly for the tcell reader goroutine to parse
// whatever bytes MockTerm just delivered into EventKey/EventMouse
// events on screen.Events, then returns. Unlike v2's SimulationScreen
// (which exposed a synchronous PollEvent), v3 parses asynchronously,
// so we have to give the reader a window to catch up before draining.
func settleEvents() {
deadline := time.Now().Add(200 * time.Millisecond)
for time.Now().Before(deadline) {
if len(screen.Events) > 0 || len(screen.DrawChan()) > 0 {
return
}
time.Sleep(2 * time.Millisecond)
}
}

func init() {
screen.Events = make(chan tcell.Event, 8)
// drainEvents runs DoEvent once per pending queue entry. Each DoEvent
// call consumes one event from screen.Events (or one Redraw token).
// After draining, a final short settle pass catches any tail events
// the processed action may have posted (e.g. a command prompt).
func drainEvents() {
for {
settleEvents()
if len(screen.Events) == 0 && len(screen.DrawChan()) == 0 {
return
}
for len(screen.Events) > 0 || len(screen.DrawChan()) > 0 {
DoEvent()
}
}
}

func startup(args []string) (tcell.SimulationScreen, error) {
func startup(args []string) (vt.MockTerm, error) {
var err error

tempDir, err = os.MkdirTemp("", "micro_test")
Expand All @@ -47,7 +76,7 @@ func startup(args []string) (tcell.SimulationScreen, error) {
return nil, err
}

s, err := screen.InitSimScreen()
mtLocal, err := screen.InitMockScreen()
if err != nil {
return nil, err
}
Expand All @@ -62,8 +91,7 @@ func startup(args []string) (tcell.SimulationScreen, error) {
b.Backup()
}
}
// Print the stack trace too
log.Fatalf(errors.Wrap(err, 2).ErrorStack())
log.Fatalf("%s", errors.Wrap(err, 2).ErrorStack())
}
}()

Expand Down Expand Up @@ -94,60 +122,94 @@ func startup(args []string) (tcell.SimulationScreen, error) {
return nil, err
}

s.InjectResize()
handleEvent()
w, h := screen.Screen.Size()
mtLocal.SetSize(vt.Coord{X: vt.Col(w), Y: vt.Row(h)})
drainEvents()

return s, nil
return mtLocal, nil
}

func cleanup() {
os.RemoveAll(tempDir)
}

func handleEvent() {
screen.Lock()
e := screen.Screen.PollEvent()
screen.Unlock()
if e != nil {
screen.Events <- e
// keyBytes translates a (tcell.Key, rune, mod) injection into the raw
// byte sequence tcell's input parser expects. For plain runes the
// payload is the UTF-8 encoding; for control bytes (Ctrl-A .. Ctrl-Z,
// Enter, Tab, Backspace, Esc) it's the single-byte code; for the
// arrow keys it's the legacy CSI escape. This covers every key
// currently exercised by the test suite. Unknown combinations panic
// so a missing mapping fails loudly rather than silently no-opping.
func keyBytes(key tcell.Key, r rune, _ tcell.ModMask) []byte {
switch key {
case tcell.KeyRune:
return []byte(string(r))
case tcell.KeyEnter:
return []byte{'\r'}
case tcell.KeyEscape:
return []byte{0x1b}
case tcell.KeyTab:
return []byte{'\t'}
case tcell.KeyBackspace:
return []byte{0x08}
case tcell.KeyBackspace2:
return []byte{0x7f}
case tcell.KeyUp:
return []byte{0x1b, '[', 'A'}
case tcell.KeyDown:
return []byte{0x1b, '[', 'B'}
case tcell.KeyRight:
return []byte{0x1b, '[', 'C'}
case tcell.KeyLeft:
return []byte{0x1b, '[', 'D'}
}

for len(screen.DrawChan()) > 0 || len(screen.Events) > 0 {
DoEvent()
if key >= tcell.KeyCtrlA && key <= tcell.KeyCtrlZ {
// tcell v3 reshuffled the Key constants: KeyCtrlA is 65, not 1.
// Convert back to the ASCII control byte (0x01..0x1A).
return []byte{byte(key-tcell.KeyCtrlA) + 1}
}
if r != 0 {
return []byte(string(r))
}
panic(fmt.Sprintf("keyBytes: unsupported key 0x%x", key))
}

func injectKey(key tcell.Key, r rune, mod tcell.ModMask) {
sim.InjectKey(key, r, mod)
handleEvent()
mt.SendRaw(keyBytes(key, r, mod))
drainEvents()
}

func injectMouse(x, y int, buttons tcell.ButtonMask, mod tcell.ModMask) {
sim.InjectMouse(x, y, buttons, mod)
handleEvent()
func injectString(str string) {
mt.SendRaw([]byte(str))
drainEvents()
}

func injectString(str string) {
// the tcell simulation screen event channel can only handle
// 10 events at once, so we need to divide up the key events
// into chunks of 10 and handle the 10 events before sending
// another chunk of events
iters := len(str) / 10
extra := len(str) % 10

for i := 0; i < iters; i++ {
s := i * 10
e := i*10 + 10
sim.InjectKeyBytes([]byte(str[s:e]))
for i := 0; i < 10; i++ {
handleEvent()
}
// tcellButtonToVt maps a tcell ButtonMask to the single vt.Button it
// corresponds to. micro_test only ever passes Button1 or ButtonNone;
// for ButtonNone (release) the caller must remember the button that
// was last pressed — vt.MouseEvent requires a non-nil Button even
// for release events. We assume Button1 for all releases, which is
// correct for every call site below.
func tcellButtonToVt(b tcell.ButtonMask) vt.Button {
switch {
case b&tcell.Button1 != 0:
return vt.Button1
case b&tcell.Button2 != 0:
return vt.Button2
case b&tcell.Button3 != 0:
return vt.Button3
}
return vt.Button1
}

sim.InjectKeyBytes([]byte(str[len(str)-extra:]))
for i := 0; i < extra; i++ {
handleEvent()
}
func injectMouse(x, y int, buttons tcell.ButtonMask, _ tcell.ModMask) {
down := buttons != tcell.ButtonNone
mt.MouseEvent(vt.MouseEvent{
Position: vt.Coord{X: vt.Col(x), Y: vt.Row(y)},
Button: tcellButtonToVt(buttons),
Down: down,
})
drainEvents()
}

func openFile(file string) {
Expand Down Expand Up @@ -187,7 +249,7 @@ func createTestFile(t *testing.T, content string) string {

func TestMain(m *testing.M) {
var err error
sim, err = startup([]string{})
mt, err = startup([]string{})
if err != nil {
log.Fatalln(err)
}
Expand All @@ -211,7 +273,6 @@ func TestSimpleEdit(t *testing.T) {
injectKey(tcell.KeyUp, 0, tcell.ModNone)
injectString("first line")

// test both kinds of backspace
for i := 0; i < len("ne"); i++ {
injectKey(tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone)
}
Expand Down Expand Up @@ -336,5 +397,3 @@ func TestMultiCursor(t *testing.T) {
func TestSettingsPersistence(t *testing.T) {
// TODO
}

// more tests (rendering, tabs, plugins)?
19 changes: 11 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,41 @@ module github.com/micro-editor/micro/v2
require (
github.com/blang/semver v3.5.1+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/gdamore/tcell/v3 v3.4.0
github.com/go-errors/errors v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.16
github.com/micro-editor/json5 v1.0.1-micro
github.com/micro-editor/tcell/v2 v2.0.13
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5
github.com/mitchellh/go-homedir v1.1.0
github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0
github.com/yuin/gopher-lua v1.1.1
github.com/zyedidia/clipper v0.1.1
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
golang.org/x/text v0.4.0
golang.org/x/text v0.38.0
gopkg.in/yaml.v2 v2.2.8
layeh.com/gopher-luar v1.0.11
)

require (
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/zyedidia/poller v1.0.1 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
)

replace github.com/kballard/go-shellquote => github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5

replace layeh.com/gopher-luar v1.0.11 => github.com/layeh/gopher-luar v1.0.11

go 1.19
replace github.com/gdamore/tcell/v3 => github.com/micro-editor/tcell/v3 v3.0.0-20260629163637-dedaf40206a5

go 1.25.0
Loading