mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-12-28 04:19:41 -08:00
refactor: escape color sequences on zsh
This commit is contained in:
parent
7d8892020e
commit
f478255bbf
|
@ -1,63 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gookit/color"
|
|
||||||
)
|
|
||||||
|
|
||||||
//ColorWriter writes colorized strings
|
|
||||||
type ColorWriter struct {
|
|
||||||
Buffer *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
//Transparent implies a transparent color
|
|
||||||
Transparent string = "transparent"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (w *ColorWriter) writeColoredText(background string, foreground string, text string) {
|
|
||||||
var coloredText string
|
|
||||||
if foreground == Transparent {
|
|
||||||
style := color.HEX(background, false)
|
|
||||||
colorCodes := style.Code()
|
|
||||||
// this takes the colors and inverts them so the foreground becomes transparent
|
|
||||||
coloredText = fmt.Sprintf("\x1b[%s;49m\x1b[7m%s\x1b[m\x1b[0m", colorCodes, text)
|
|
||||||
} else if background == "" || background == Transparent {
|
|
||||||
style := color.HEX(foreground)
|
|
||||||
coloredText = style.Sprint(text)
|
|
||||||
} else {
|
|
||||||
style := color.HEXStyle(foreground, background)
|
|
||||||
coloredText = style.Sprint(text)
|
|
||||||
}
|
|
||||||
w.Buffer.WriteString(coloredText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *ColorWriter) writeAndRemoveText(background string, foreground string, text string, textToRemove string, parentText string) string {
|
|
||||||
w.writeColoredText(background, foreground, text)
|
|
||||||
return strings.Replace(parentText, textToRemove, "", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *ColorWriter) write(background string, foreground string, text string) {
|
|
||||||
r := regexp.MustCompile(`<\s*(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})>(?P<text>.*?)<\s*/\s*>`)
|
|
||||||
match := r.FindAllStringSubmatch(text, -1)
|
|
||||||
for i := range match {
|
|
||||||
// get the text before the color override and write that first
|
|
||||||
textBeforeColorOverride := strings.Split(text, match[i][0])[0]
|
|
||||||
text = w.writeAndRemoveText(background, foreground, textBeforeColorOverride, textBeforeColorOverride, text)
|
|
||||||
text = w.writeAndRemoveText(background, match[i][1], match[i][2], match[i][0], text)
|
|
||||||
}
|
|
||||||
// color the remaining part of text with background and foreground
|
|
||||||
w.writeColoredText(background, foreground, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *ColorWriter) string() string {
|
|
||||||
return w.Buffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *ColorWriter) reset() {
|
|
||||||
w.Buffer.Reset()
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWriteAndRemoveText(t *testing.T) {
|
|
||||||
writer := &ColorWriter{
|
|
||||||
Buffer: new(bytes.Buffer),
|
|
||||||
}
|
|
||||||
inputText := "This is white, <#ff5733>this is orange</>, white again"
|
|
||||||
text := writer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText)
|
|
||||||
assert.Equal(t, "<#ff5733>this is orange</>, white again", text)
|
|
||||||
assert.NotContains(t, writer.string(), "<#ff5733>")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteAndRemoveTextColored(t *testing.T) {
|
|
||||||
writer := &ColorWriter{
|
|
||||||
Buffer: new(bytes.Buffer),
|
|
||||||
}
|
|
||||||
inputText := "This is white, <#ff5733>this is orange</>, white again"
|
|
||||||
text := writer.writeAndRemoveText("#193549", "#ff5733", "this is orange", "<#ff5733>this is orange</>", inputText)
|
|
||||||
assert.Equal(t, "This is white, , white again", text)
|
|
||||||
assert.NotContains(t, writer.string(), "<#ff5733>")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteColorOverride(t *testing.T) {
|
|
||||||
writer := &ColorWriter{
|
|
||||||
Buffer: new(bytes.Buffer),
|
|
||||||
}
|
|
||||||
text := "This is white, <#ff5733>this is orange</>, white again"
|
|
||||||
writer.write("#193549", "#ff5733", text)
|
|
||||||
assert.NotContains(t, writer.string(), "<#ff5733>")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteColorTransparent(t *testing.T) {
|
|
||||||
writer := &ColorWriter{
|
|
||||||
Buffer: new(bytes.Buffer),
|
|
||||||
}
|
|
||||||
text := "This is white"
|
|
||||||
writer.writeColoredText("#193549", Transparent, text)
|
|
||||||
t.Log(writer.string())
|
|
||||||
}
|
|
32
engine.go
32
engine.go
|
@ -2,15 +2,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"golang.org/x/text/unicode/norm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type engine struct {
|
type engine struct {
|
||||||
settings *Settings
|
settings *Settings
|
||||||
env environmentInfo
|
env environmentInfo
|
||||||
renderer *ColorWriter
|
renderer *Renderer
|
||||||
activeBlock *Block
|
activeBlock *Block
|
||||||
activeSegment *Segment
|
activeSegment *Segment
|
||||||
previousActiveSegment *Segment
|
previousActiveSegment *Segment
|
||||||
|
@ -107,37 +104,22 @@ func (e *engine) renderBlockSegments(block *Block) string {
|
||||||
return e.renderer.string()
|
return e.renderer.string()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *engine) lenWithoutANSI(str string) int {
|
|
||||||
ansi := "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
|
|
||||||
re := regexp.MustCompile(ansi)
|
|
||||||
stripped := re.ReplaceAllString(str, "")
|
|
||||||
var i norm.Iter
|
|
||||||
i.InitString(norm.NFD, stripped)
|
|
||||||
var count int
|
|
||||||
for !i.Done() {
|
|
||||||
i.Next()
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *engine) render() {
|
func (e *engine) render() {
|
||||||
for _, block := range e.settings.Blocks {
|
for _, block := range e.settings.Blocks {
|
||||||
// if line break, append a line break
|
// if line break, append a line break
|
||||||
if block.Type == LineBreak {
|
if block.Type == LineBreak {
|
||||||
fmt.Printf("\x1b[%dC ", 1000)
|
fmt.Print(e.renderer.lineBreak())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if block.LineOffset < 0 {
|
if block.LineOffset != 0 {
|
||||||
fmt.Printf("\x1b[%dF", -block.LineOffset)
|
fmt.Print(e.renderer.changeLine(block.LineOffset))
|
||||||
} else if block.LineOffset > 0 {
|
|
||||||
fmt.Printf("\x1b[%dB", block.LineOffset)
|
|
||||||
}
|
}
|
||||||
switch block.Alignment {
|
switch block.Alignment {
|
||||||
case Right:
|
case Right:
|
||||||
fmt.Printf("\x1b[%dC", 1000)
|
fmt.Print(e.renderer.carriageReturn())
|
||||||
blockText := e.renderBlockSegments(block)
|
blockText := e.renderBlockSegments(block)
|
||||||
fmt.Printf("\x1b[%dD", e.lenWithoutANSI(blockText)+e.settings.RightSegmentOffset)
|
cursorMove := e.renderer.setCursorForRightWrite(blockText, e.settings.RightSegmentOffset)
|
||||||
|
fmt.Print(cursorMove)
|
||||||
fmt.Print(blockText)
|
fmt.Print(blockText)
|
||||||
default:
|
default:
|
||||||
fmt.Print(e.renderBlockSegments(block))
|
fmt.Print(e.renderBlockSegments(block))
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLenWithoutANSI(t *testing.T) {
|
|
||||||
block := &Block{
|
|
||||||
Type: Prompt,
|
|
||||||
Alignment: Right,
|
|
||||||
Segments: []*Segment{
|
|
||||||
{
|
|
||||||
Type: Time,
|
|
||||||
Style: Plain,
|
|
||||||
Background: "#B8B80A",
|
|
||||||
Foreground: "#ffffff",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
engine := &engine{
|
|
||||||
renderer: &ColorWriter{
|
|
||||||
Buffer: new(bytes.Buffer),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
blockText := engine.renderBlockSegments(block)
|
|
||||||
strippedLength := engine.lenWithoutANSI(blockText)
|
|
||||||
assert.Equal(t, 10, strippedLength)
|
|
||||||
}
|
|
12
main.go
12
main.go
|
@ -38,12 +38,18 @@ func main() {
|
||||||
fmt.Println(string(theme))
|
fmt.Println(string(theme))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
colorWriter := &Renderer{
|
||||||
|
Buffer: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
var shell string
|
||||||
|
if parentProcess, err := env.getParentProcess(); err != nil {
|
||||||
|
shell = parentProcess.Executable()
|
||||||
|
}
|
||||||
|
colorWriter.init(shell)
|
||||||
engine := &engine{
|
engine := &engine{
|
||||||
settings: settings,
|
settings: settings,
|
||||||
env: env,
|
env: env,
|
||||||
renderer: &ColorWriter{
|
renderer: colorWriter,
|
||||||
Buffer: new(bytes.Buffer),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
engine.render()
|
engine.render()
|
||||||
}
|
}
|
||||||
|
|
151
renderer.go
Executable file
151
renderer.go
Executable file
|
@ -0,0 +1,151 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gookit/color"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type formats struct {
|
||||||
|
single string
|
||||||
|
full string
|
||||||
|
transparent string
|
||||||
|
linebreak string
|
||||||
|
linechange string
|
||||||
|
left string
|
||||||
|
right string
|
||||||
|
rANSI string
|
||||||
|
}
|
||||||
|
|
||||||
|
//Shell indicates the shell we're currently in
|
||||||
|
type Shell string
|
||||||
|
|
||||||
|
//Renderer writes colorized strings
|
||||||
|
type Renderer struct {
|
||||||
|
Buffer *bytes.Buffer
|
||||||
|
formats *formats
|
||||||
|
shell Shell
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
//Transparent implies a transparent color
|
||||||
|
Transparent string = "transparent"
|
||||||
|
zsh Shell = "zsh"
|
||||||
|
universal Shell = "any"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Renderer) init(shell string) {
|
||||||
|
r.formats = &formats{
|
||||||
|
rANSI: "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))",
|
||||||
|
}
|
||||||
|
switch shell {
|
||||||
|
case "zsh":
|
||||||
|
r.formats.single = "%%{\x1b[%sm%%}%s%%{\x1b[0m%%}"
|
||||||
|
r.formats.full = "%%{\x1b[%sm\x1b[%sm%%}%s%%{\x1b[0m%%}"
|
||||||
|
r.formats.transparent = "%%{\x1b[%s;49m\x1b[7m%%}%s%%{\x1b[m\x1b[0m%%}"
|
||||||
|
r.formats.linebreak = "\n"
|
||||||
|
r.formats.linechange = "%%{\x1b[%d%s%%}"
|
||||||
|
r.formats.left = "%%{\x1b[%d%%}"
|
||||||
|
r.formats.right = "%%{\x1b[%dD%%}"
|
||||||
|
r.shell = zsh
|
||||||
|
default:
|
||||||
|
r.formats.single = "\x1b[%sm%s\x1b[0m"
|
||||||
|
r.formats.full = "\x1b[%sm\x1b[%sm%s\x1b[0m"
|
||||||
|
r.formats.transparent = "\x1b[%s;49m\x1b[7m%s\x1b[m\x1b[0m"
|
||||||
|
r.formats.linebreak = "\x1b[1000C "
|
||||||
|
r.formats.linechange = "\x1b[%d%s"
|
||||||
|
r.formats.left = "\x1b[%dC"
|
||||||
|
r.formats.right = "\x1b[%dD"
|
||||||
|
r.shell = universal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) getAnsiFromHex(hexColor string, isBackground bool) string {
|
||||||
|
style := color.HEX(hexColor, isBackground)
|
||||||
|
return style.Code()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) writeColoredText(background string, foreground string, text string) {
|
||||||
|
var coloredText string
|
||||||
|
if foreground == Transparent && background != "" {
|
||||||
|
ansiColor := r.getAnsiFromHex(background, false)
|
||||||
|
coloredText = fmt.Sprintf(r.formats.transparent, ansiColor, text)
|
||||||
|
} else if background == "" || background == Transparent {
|
||||||
|
ansiColor := r.getAnsiFromHex(foreground, false)
|
||||||
|
coloredText = fmt.Sprintf(r.formats.single, ansiColor, text)
|
||||||
|
} else if foreground != "" && background != "" {
|
||||||
|
bgAnsiColor := r.getAnsiFromHex(background, true)
|
||||||
|
fgAnsiColor := r.getAnsiFromHex(foreground, false)
|
||||||
|
coloredText = fmt.Sprintf(r.formats.full, bgAnsiColor, fgAnsiColor, text)
|
||||||
|
}
|
||||||
|
r.Buffer.WriteString(coloredText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) writeAndRemoveText(background string, foreground string, text string, textToRemove string, parentText string) string {
|
||||||
|
r.writeColoredText(background, foreground, text)
|
||||||
|
return strings.Replace(parentText, textToRemove, "", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) write(background string, foreground string, text string) {
|
||||||
|
rex := regexp.MustCompile(`<\s*(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})>(?P<text>.*?)<\s*/\s*>`)
|
||||||
|
match := rex.FindAllStringSubmatch(text, -1)
|
||||||
|
for i := range match {
|
||||||
|
// get the text before the color override and write that first
|
||||||
|
textBeforeColorOverride := strings.Split(text, match[i][0])[0]
|
||||||
|
text = r.writeAndRemoveText(background, foreground, textBeforeColorOverride, textBeforeColorOverride, text)
|
||||||
|
text = r.writeAndRemoveText(background, match[i][1], match[i][2], match[i][0], text)
|
||||||
|
}
|
||||||
|
// color the remaining part of text with background and foreground
|
||||||
|
r.writeColoredText(background, foreground, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) lenWithoutANSI(str string) int {
|
||||||
|
re := regexp.MustCompile(r.formats.rANSI)
|
||||||
|
stripped := re.ReplaceAllString(str, "")
|
||||||
|
if r.shell == zsh {
|
||||||
|
stripped = strings.Replace(stripped, "%{", "", -1)
|
||||||
|
stripped = strings.Replace(stripped, "%}", "", -1)
|
||||||
|
}
|
||||||
|
var i norm.Iter
|
||||||
|
i.InitString(norm.NFD, stripped)
|
||||||
|
var count int
|
||||||
|
for !i.Done() {
|
||||||
|
i.Next()
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) lineBreak() string {
|
||||||
|
return r.formats.linebreak
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) carriageReturn() string {
|
||||||
|
return fmt.Sprintf(r.formats.left, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) setCursorForRightWrite(text string, offset int) string {
|
||||||
|
strippedLen := r.lenWithoutANSI(text) + offset
|
||||||
|
return fmt.Sprintf(r.formats.right, strippedLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) changeLine(numberOfLines int) string {
|
||||||
|
position := "B"
|
||||||
|
if numberOfLines < 0 {
|
||||||
|
position = "F"
|
||||||
|
numberOfLines = -numberOfLines
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(r.formats.linechange, numberOfLines, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) string() string {
|
||||||
|
return r.Buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) reset() {
|
||||||
|
r.Buffer.Reset()
|
||||||
|
}
|
70
renderer_test.go
Executable file
70
renderer_test.go
Executable file
|
@ -0,0 +1,70 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteAndRemoveText(t *testing.T) {
|
||||||
|
renderer := &Renderer{
|
||||||
|
Buffer: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
renderer.init("pwsh")
|
||||||
|
inputText := "This is white, <#ff5733>this is orange</>, white again"
|
||||||
|
text := renderer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText)
|
||||||
|
assert.Equal(t, "<#ff5733>this is orange</>, white again", text)
|
||||||
|
assert.NotContains(t, renderer.string(), "<#ff5733>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteAndRemoveTextColored(t *testing.T) {
|
||||||
|
renderer := &Renderer{
|
||||||
|
Buffer: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
renderer.init("pwsh")
|
||||||
|
inputText := "This is white, <#ff5733>this is orange</>, white again"
|
||||||
|
text := renderer.writeAndRemoveText("#193549", "#ff5733", "this is orange", "<#ff5733>this is orange</>", inputText)
|
||||||
|
assert.Equal(t, "This is white, , white again", text)
|
||||||
|
assert.NotContains(t, renderer.string(), "<#ff5733>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteColorOverride(t *testing.T) {
|
||||||
|
renderer := &Renderer{
|
||||||
|
Buffer: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
renderer.init("pwsh")
|
||||||
|
text := "This is white, <#ff5733>this is orange</>, white again"
|
||||||
|
renderer.write("#193549", "#ff5733", text)
|
||||||
|
assert.NotContains(t, renderer.string(), "<#ff5733>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteColorTransparent(t *testing.T) {
|
||||||
|
renderer := &Renderer{
|
||||||
|
Buffer: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
renderer.init("pwsh")
|
||||||
|
text := "This is white"
|
||||||
|
renderer.writeColoredText("#193549", Transparent, text)
|
||||||
|
t.Log(renderer.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLenWithoutANSI(t *testing.T) {
|
||||||
|
text := "\x1b[44mhello\x1b[0m"
|
||||||
|
renderer := &Renderer{
|
||||||
|
Buffer: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
renderer.init("pwsh")
|
||||||
|
strippedLength := renderer.lenWithoutANSI(text)
|
||||||
|
assert.Equal(t, 5, strippedLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLenWithoutANSIZsh(t *testing.T) {
|
||||||
|
text := "%{\x1b[44m%}hello%{\x1b[0m%}"
|
||||||
|
renderer := &Renderer{
|
||||||
|
Buffer: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
renderer.init("zsh")
|
||||||
|
strippedLength := renderer.lenWithoutANSI(text)
|
||||||
|
assert.Equal(t, 5, strippedLength)
|
||||||
|
}
|
|
@ -9,14 +9,14 @@ type command struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
//Shell to execute command in
|
//ExecutableShell to execute command in
|
||||||
Shell Property = "shell"
|
ExecutableShell Property = "shell"
|
||||||
//Command to execute
|
//Command to execute
|
||||||
Command Property = "command"
|
Command Property = "command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *command) enabled() bool {
|
func (c *command) enabled() bool {
|
||||||
shell := c.props.getString(Shell, "bash")
|
shell := c.props.getString(ExecutableShell, "bash")
|
||||||
command := c.props.getString(Command, "echo no command specified")
|
command := c.props.getString(Command, "echo no command specified")
|
||||||
if strings.Contains(command, "||") {
|
if strings.Contains(command, "||") {
|
||||||
commands := strings.Split(command, "||")
|
commands := strings.Split(command, "||")
|
||||||
|
|
Loading…
Reference in a new issue