Set default cmd charset to UTF-8 on Windows

Kesa...大约 3 分钟WindowsGolang

1. Issue Description

Recently, I started learning Bubbleteaopen in new window. When I reimplemented its example: Composable viewsopen in new window, I encountered a problem, the code did not work as expected.

Expected Behavior

image-20231203000429195
image-20231203000429195

Actual Behavior

image-20231202231658793
image-20231202231658793

2. What led to this?

In the example, it uses lipglossopen in new window to render the content displayed in the terminal (code: composable-views: Line46open in new window):

// set render style
modelStyle = lipgloss.NewStyle().
			Width(15).
			Height(5).
			Align(lipgloss.Center, lipgloss.Center).
			BorderStyle(lipgloss.HiddenBorder())
...
// render content
		s += lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), modelStyle.Render(m.spinner.View()))

After debugging, I discovered that the issue was due to the character set of my terminal being set to 936 (GBK). For Windows code page table see wikiopen in new window.

In style.Render()open in new window, it uses function borders.renderHorizontalEdgeopen in new window to render horizon border:

// github.com/charmbracelet/lipgloss/blob/master/style.go

// Render applies the defined style formatting to a given string.
func (s Style) Render(strs ...string) string {
	...
    if !inline {
		str = s.applyBorder(str)
		str = s.applyMargins(str, inline)
	}
    ...
}

func (s Style) applyBorder(str string) string {
	...
    	// Render top
	if hasTop {
		top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
		top = s.styleBorder(top, topFG, topBG)
		out.WriteString(top)
		out.WriteRune('\n')
	}
    ...
}

// github.com/charmbracelet/lipgloss/blob/master/borders.go

// Render the horizontal (top or bottom) portion of a border.
func renderHorizontalEdge(left, middle, right string, width int) string {
    ...
	leftWidth := ansi.PrintableRuneWidth(left)
	rightWidth := ansi.PrintableRuneWidth(right)

	runes := []rune(middle)
	j := 0

	out := strings.Builder{}
	out.WriteString(left)
	for i := leftWidth + rightWidth; i < width+rightWidth; {
		out.WriteRune(runes[j])
		j++
		if j >= len(runes) {
			j = 0
		}
		i += ansi.PrintableRuneWidth(string(runes[j]))
	}
	out.WriteString(right)

	return out.String()
}

As code above, ansi.PrintableRuneWidth()open in new window is used to get the width of a rune.

The length of in GBK is 2, so the function borders.renderHorizontalEdgeopen in new window will print the wrong number of (horizon border).

// github.com/muesli/reflow/blob/master/ansi/buffer.go

// PrintableRuneWidth returns the cell width of the given string.
func PrintableRuneWidth(s string) int {
	var n int
	var ansi bool

	for _, c := range s {
		if c == Marker {
			// ANSI escape sequence
			ansi = true
		} else if ansi {
			if IsTerminator(c) {
				// ANSI sequence terminated
				ansi = false
			}
		} else {
			n += runewidth.RuneWidth(c)
		}
	}

	return n
}

runewidth.RuneWidth()open in new window:

// github.com/mattn/go-runewidth/blob/master/runewidth.go

// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func RuneWidth(r rune) int {
	return DefaultCondition.RuneWidth(r)
}

// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
	if r < 0 || r > 0x10FFFF {
		return 0
	}
	if len(c.combinedLut) > 0 {
		return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
	}
	// optimized version, verified by TestRuneWidthChecksums()
	if !c.EastAsianWidth {
		switch {
		case r < 0x20:
			return 0
		case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
			return 0
		case r < 0x300:
			return 1
		case inTable(r, narrow):
			return 1
		case inTables(r, nonprint, combining):
			return 0
		case inTable(r, doublewidth):
			return 2
		default:
			return 1
		}
	} else {
		switch {
		case inTables(r, nonprint, combining):
			return 0
		case inTable(r, narrow):
			return 1
		case inTables(r, ambiguous, doublewidth):
			return 2
		case !c.StrictEmojiNeutral && inTables(r, ambiguous, emoji, narrow):
			return 2
		default:
			return 1
		}
	}
}

| in GBK will match inTble(r, narrow) in the else block.

The table narrow is generated by generateopen in new window according to https://home.unicode.org/open in new window.

3. Demo to explain how it happened

package main

import (
	"fmt"
	"github.com/charmbracelet/lipgloss"
)

func main() {
	var style = lipgloss.NewStyle().
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("63"))

	fmt.Print(style.Render("hello"))
}
// output
┌───┐
│hello│
└───┘

Top border render steps:

  1. Get the horizon length: 7 = 5 + 2 = len(“hello”) + number of vertical border |
  2. Get the middle horizon borders (its length is 2 in GBK): only 2 (7-2 % 2), expect 5 (7-2 % 1)

4. How to solve

Get code page of cmd

Use chcp to get the current code page of cmd:

> chcp
936

Change code page to UTF-8 Temporarily

> chcp 65001

Set default cmd code page

  1. Start -> Run -> regedit
  2. Go to [HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\Autorun]
  3. Change the value to @chcp 65001>nul

If Autorun is not present, you can add a New String.

This approach will auto-execute @chcp 65001>nul when cmd starts.

Reference

  1. https://superuser.com/questions/269818/change-default-code-page-of-windows-console-to-utf-8/269857#269857open in new window
  2. https://en.wikipedia.org/wiki/Windows_code_pageopen in new window
  3. https://github.com/charmbracelet/bubbletea/blob/master/examples/composable-views/main.go#L46open in new window
  4. https://home.unicode.org/open in new window
  5. https://github.com/charmbracelet/lipglossopen in new window
  6. https://github.com/mattn/go-runewidth/open in new window
  7. https://github.com/muesli/reflowopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2