From 79e4e24b35f6af358d32cd3c46f4af4086fed909 Mon Sep 17 00:00:00 2001 From: INTFRAME Date: Thu, 3 Sep 2026 03:01:32 +0700 Subject: [PATCH 1/2] Add `ambiguouswidth` option The width of characters with the East Asian Ambiguous width property is currently decided by the locale environment variables, since that is what go-runewidth autodetects. Terminals usually draw these characters one column wide regardless of the locale, so in an East Asian locale micro misaligns the text and puts the cursor in the wrong column, with no way to override it. Add a global-only `ambiguouswidth` option, in the same shape as `truecolor`: `auto` (the default, i.e. the current behavior), `single` and `double`. --- internal/action/command.go | 2 ++ internal/config/settings.go | 3 +++ internal/screen/screen.go | 3 +++ internal/util/util.go | 22 ++++++++++++++++++++++ internal/util/util_test.go | 25 +++++++++++++++++++++++++ runtime/help/options.md | 17 +++++++++++++++++ 6 files changed, 72 insertions(+) diff --git a/internal/action/command.go b/internal/action/command.go index cd97c5222d..d117c0c6da 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -601,6 +601,8 @@ func doSetGlobalOptionNative(option string, nativeValue any) error { } else { config.SetAutoTime(0) } + } else if option == "ambiguouswidth" { + util.SetAmbiguousWidth(nativeValue.(string)) } else if option == "paste" { screen.Screen.SetPaste(nativeValue.(bool)) } else if option == "clipboard" { diff --git a/internal/config/settings.go b/internal/config/settings.go index 45f6b3aad6..a7e7669096 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -21,6 +21,7 @@ type optionValidator func(string, any) error // a list of settings that need option validators var optionValidators = map[string]optionValidator{ + "ambiguouswidth": validateChoice, "autosave": validateNonNegativeValue, "clipboard": validateChoice, "colorcolumn": validateNonNegativeValue, @@ -41,6 +42,7 @@ var optionValidators = map[string]optionValidator{ // a list of settings with pre-defined choices var OptionChoices = map[string][]string{ + "ambiguouswidth": {"auto", "single", "double"}, "clipboard": {"internal", "external", "terminal"}, "fileformat": {"unix", "dos"}, "helpsplit": {"hsplit", "vsplit"}, @@ -110,6 +112,7 @@ var defaultCommonSettings = map[string]any{ // a list of settings that should only be globally modified and their // default values var DefaultGlobalOnlySettings = map[string]any{ + "ambiguouswidth": "auto", "autosave": float64(0), "clipboard": "external", "colorscheme": "default", diff --git a/internal/screen/screen.go b/internal/screen/screen.go index 4b2c564249..86746bbacf 100644 --- a/internal/screen/screen.go +++ b/internal/screen/screen.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/micro-editor/micro/v2/internal/config" + "github.com/micro-editor/micro/v2/internal/util" "github.com/micro-editor/tcell/v2" ) @@ -182,6 +183,8 @@ func TempStart(screenWasNil bool) { func Init() error { drawChan = make(chan bool, 8) + util.SetAmbiguousWidth(config.GetGlobalOption("ambiguouswidth").(string)) + // Should we enable true color? truecolor := config.GetGlobalOption("truecolor").(string) if truecolor == "on" || (truecolor == "auto" && os.Getenv("MICRO_TRUECOLOR") == "1") { diff --git a/internal/util/util.go b/internal/util/util.go index 9838b025fb..c154ac2359 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -91,6 +91,28 @@ func init() { } Stdout = new(bytes.Buffer) + + envAmbiguousWide = runewidth.EastAsianWidth +} + +// envAmbiguousWide is the ambiguous width detected from the environment +// (the locale environment variables and RUNEWIDTH_EASTASIAN) at startup +var envAmbiguousWide bool + +// SetAmbiguousWidth sets how wide the characters with the East Asian +// Ambiguous width property (see Unicode Standard Annex #11) are considered +// to be: one column ("single"), two columns ("double") or as many columns +// as the environment implies ("auto") +func SetAmbiguousWidth(mode string) { + switch mode { + case "single": + runewidth.EastAsianWidth = false + case "double": + runewidth.EastAsianWidth = true + default: + runewidth.EastAsianWidth = envAmbiguousWide + } + runewidth.DefaultCondition.EastAsianWidth = runewidth.EastAsianWidth } // SliceEnd returns a byte slice where the index is a rune index diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 454b5c7963..7208f7fbe7 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -13,6 +13,31 @@ func TestStringWidth(t *testing.T) { assert.Equal(t, 26, n) } +func TestSetAmbiguousWidth(t *testing.T) { + defer SetAmbiguousWidth("auto") + + // U+03B1 is ambiguous width, U+3042 is wide, U+0061 is narrow + bytes := []byte("\u03b1\u3042a") + + SetAmbiguousWidth("single") + assert.Equal(t, 1, StringWidth(bytes, 1, 4)) + assert.Equal(t, 3, StringWidth(bytes, 2, 4)) + assert.Equal(t, 4, StringWidth(bytes, 3, 4)) + + SetAmbiguousWidth("double") + assert.Equal(t, 2, StringWidth(bytes, 1, 4)) + assert.Equal(t, 4, StringWidth(bytes, 2, 4)) + assert.Equal(t, 5, StringWidth(bytes, 3, 4)) + + // "auto" restores the width implied by the environment + SetAmbiguousWidth("auto") + expected := 1 + if envAmbiguousWide { + expected = 2 + } + assert.Equal(t, expected, StringWidth(bytes, 1, 4)) +} + func TestSliceVisualEnd(t *testing.T) { s := []byte("\thello") slc, n, _ := SliceVisualEnd(s, 2, 4) diff --git a/runtime/help/options.md b/runtime/help/options.md index 542089425b..62f5a90ba3 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -11,6 +11,23 @@ if you have set either of the above environment variables). Here are the available options: +* `ambiguouswidth`: how wide the characters with the East Asian Ambiguous + width property (as defined by [Unicode Standard Annex + #11](https://www.unicode.org/reports/tr11)) are considered to be. These + characters, e.g. Greek and Cyrillic letters, accented Latin letters and box + drawing characters, are drawn one column wide by most terminals but two + columns wide by some terminals in an East Asian environment. If micro's + choice does not match your terminal, text is misaligned and the cursor is + drawn in the wrong column. This setting is `global only`. + * `auto`: use two columns if the locale environment variables (`LC_ALL`, + `LC_CTYPE`, `LANG`) indicate a Chinese, Japanese or Korean locale, + otherwise one column. Can be overridden with the `RUNEWIDTH_EASTASIAN` + environment variable. + * `single`: always use one column, like `wcwidth(3)` and most terminals do. + * `double`: always use two columns. + + default value: `auto` + * `autoindent`: when creating a new line, use the same indentation as the previous line. From 5543c6f273f3ba69affbaa222b720d018c5a4496 Mon Sep 17 00:00:00 2001 From: aron-intframe Date: Thu, 3 Sep 2026 15:27:15 +0700 Subject: [PATCH 2/2] util: set only DefaultCondition when overriding ambiguous width Writing runewidth.EastAsianWidth has no effect on measurement: go-runewidth reads DefaultCondition, and syncs it from that package variable only once in its own init(). Drop the redundant write and keep the assignment that matters. --- internal/util/util.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/util/util.go b/internal/util/util.go index c154ac2359..d7d02c67f1 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -104,15 +104,18 @@ var envAmbiguousWide bool // to be: one column ("single"), two columns ("double") or as many columns // as the environment implies ("auto") func SetAmbiguousWidth(mode string) { + var wide bool switch mode { case "single": - runewidth.EastAsianWidth = false + wide = false case "double": - runewidth.EastAsianWidth = true + wide = true default: - runewidth.EastAsianWidth = envAmbiguousWide + wide = envAmbiguousWide } - runewidth.DefaultCondition.EastAsianWidth = runewidth.EastAsianWidth + // go-runewidth syncs DefaultCondition with runewidth.EastAsianWidth only + // once, in its init(), so overriding it later has to be done here + runewidth.DefaultCondition.EastAsianWidth = wide } // SliceEnd returns a byte slice where the index is a rune index