From 2cef47967e8c7d42bf8df6edad6b358b2092c47e Mon Sep 17 00:00:00 2001 From: Night Cat <107802416+MHNightCat@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:25:22 +0800 Subject: [PATCH 1/3] update overlay --- src/components/default_config.go | 2 +- src/components/model.go | 52 ++-- src/components/model_render.go | 8 +- src/components/overPlace.go | 406 ++++++++++++++++++++++++++++++ src/components/string_function.go | 44 +++- src/go.mod | 7 +- src/go.sum | 6 +- 7 files changed, 491 insertions(+), 34 deletions(-) create mode 100644 src/components/overPlace.go diff --git a/src/components/default_config.go b/src/components/default_config.go index cdfdd254..865be14f 100644 --- a/src/components/default_config.go +++ b/src/components/default_config.go @@ -68,7 +68,7 @@ metadata = false func getHelpMenuData() []helpMenuModalData { data := []helpMenuModalData{ { - subTitle: "General", + subTitle: "測試用途", }, { hotkey: hotkeys.Quit, diff --git a/src/components/model.go b/src/components/model.go index 654f17ce..e7e9264e 100644 --- a/src/components/model.go +++ b/src/components/model.go @@ -73,9 +73,9 @@ func InitialModel(dir string) model { }, helpMenu: helpMenuModal{ renderIndex: 0, - cursor: 1, - data: getHelpMenuData(), - open: false, + cursor: 1, + data: getHelpMenuData(), + open: false, }, toggleDotFile: toggleDotFileBool, } @@ -127,8 +127,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.fullWidth = msg.Width m.fileModel.maxFilePanel = (msg.Width - 20) / 24 - m.helpMenu.height = m.fullHeight -2 - m.helpMenu.width = m.fullWidth -2 + m.helpMenu.height = m.fullHeight - 2 + m.helpMenu.width = m.fullWidth - 2 if m.fullHeight > 32 { m.helpMenu.height = 30 @@ -160,7 +160,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m, cmd = mainKey(msg.String(), m, cmd) - + if m.editorMode { m.editorMode = false return m, cmd @@ -216,30 +216,36 @@ func (m model) View() string { // check is the terminal size enough if m.fullHeight < minimumHeight || m.fullWidth < minimumWidth { return terminalSizeWarnRender(m) - } else if m.typingModal.open { - return typineModalRender(m) - } else if m.warnModal.open { - return warnModalRender(m) - } else if m.helpMenu.open { - return helpMenuRender(m) - } else { - sidebar := sidebarRender(m) - filePanel := filePanelRender(m) + } + sidebar := sidebarRender(m) + + filePanel := filePanelRender(m) - mainPanel := lipgloss.JoinHorizontal(0, sidebar, filePanel) + mainPanel := lipgloss.JoinHorizontal(0, sidebar, filePanel) - processBar := processBarRender(m) + processBar := processBarRender(m) - metaData := metadataRender(m) + metaData := metadataRender(m) - clipboardBar := clipboardRender(m) + clipboardBar := clipboardRender(m) - footer := lipgloss.JoinHorizontal(0, processBar, metaData, clipboardBar) + footer := lipgloss.JoinHorizontal(0, processBar, metaData, clipboardBar) - // final render - finalRender := lipgloss.JoinVertical(0, mainPanel, footer) + // final render + finalRender := lipgloss.JoinVertical(0, mainPanel, footer) - return lipgloss.JoinVertical(lipgloss.Top, finalRender) + if m.helpMenu.open { + helpMenu := helpMenuRender(m) + return PlaceOverlay(10, 10, helpMenu, finalRender) + } + if m.typingModal.open { + typingModal := typineModalRender(m) + return PlaceOverlay(10, 10, typingModal, finalRender) + } + if m.warnModal.open { + warnModal := warnModalRender(m) + return PlaceOverlay(10, 10, warnModal, finalRender) } + return finalRender } diff --git a/src/components/model_render.go b/src/components/model_render.go index cc2ad51a..a6019df3 100644 --- a/src/components/model_render.go +++ b/src/components/model_render.go @@ -335,13 +335,13 @@ func typineModalRender(m model) string { confirm := modalConfirm.Render(" (" + hotkeys.Confirm[0] + ") New File ") cancel := modalCancel.Render(" (" + hotkeys.Cancel[0] + ") Cancel ") tip := confirm + lipgloss.NewStyle().Background(modalBGColor).Render(" ") + cancel - return fullScreenStyle(m.fullHeight, m.fullWidth).Render(modalBorderStyle(modalHeight, modalWidth).Render(fileLocation + "\n" + m.typingModal.textInput.View() + "\n\n" + tip)) + return modalBorderStyle(modalHeight, modalWidth).Render(fileLocation + "\n" + m.typingModal.textInput.View() + "\n\n" + tip) } else { fileLocation := filePanelTopDirectoryIconStyle.Render("  ") + filePanelTopPathStyle.Render(truncateTextBeginning(m.typingModal.location+"/"+m.typingModal.textInput.Value(), modalWidth-4)) + "\n" confirm := modalConfirm.Render(" (" + hotkeys.Confirm[0] + ") New Folder ") cancel := modalCancel.Render(" (" + hotkeys.Cancel[0] + ") Cancel ") tip := confirm + lipgloss.NewStyle().Background(modalBGColor).Render(" ") + cancel - return fullScreenStyle(m.fullHeight, m.fullWidth).Render(modalBorderStyle(modalHeight, modalWidth).Render(fileLocation + "\n" + m.typingModal.textInput.View() + "\n\n" + tip)) + return modalBorderStyle(modalHeight, modalWidth).Render(fileLocation + "\n" + m.typingModal.textInput.View() + "\n\n" + tip) } } @@ -351,7 +351,7 @@ func warnModalRender(m model) string { confirm := modalCancel.Render(" (" + hotkeys.Confirm[0] + ") Confirm ") cancel := modalCancel.Render(" (" + hotkeys.Cancel[0] + ") Cancel ") tip := confirm + lipgloss.NewStyle().Background(modalBGColor).Render(" ") + cancel - return fullScreenStyle(m.fullHeight, m.fullWidth).Render(modalBorderStyle(modalHeight, modalWidth).Render(title + "\n\n" + content + "\n\n" + tip)) + return modalBorderStyle(modalHeight, modalWidth).Render(title + "\n\n" + content + "\n\n" + tip) } func helpMenuRender(m model) string { @@ -425,5 +425,5 @@ func helpMenuRender(m model) string { bottomBorder := generateFooterBorder(fmt.Sprintf("%s/%s", strconv.Itoa(m.helpMenu.cursor+1 - cursorBeenTitleCount), strconv.Itoa(len(m.helpMenu.data)-totalTitleCount)), m.helpMenu.width-2) - return fullScreenStyle(m.fullHeight, m.fullWidth).Render(helpMenuModalBorderStyle(m.helpMenu.height, m.helpMenu.width, bottomBorder).Render(helpMenuContent)) + return helpMenuModalBorderStyle(m.helpMenu.height, m.helpMenu.width, bottomBorder).Render(helpMenuContent) } diff --git a/src/components/overPlace.go b/src/components/overPlace.go new file mode 100644 index 00000000..a75b9f65 --- /dev/null +++ b/src/components/overPlace.go @@ -0,0 +1,406 @@ +package components + +import ( + "bytes" + "strings" + + charmansi "github.com/charmbracelet/x/exp/term/ansi" + "github.com/mattn/go-runewidth" + ansi "github.com/muesli/reflow/ansi" + "github.com/muesli/reflow/truncate" + "github.com/muesli/termenv" +) + +// whitespace is a whitespace renderer. +type whitespace struct { + style termenv.Style + chars string +} + +type WhitespaceOption func(*whitespace) + +// Render whitespaces. +func (w whitespace) render(width int) string { + if w.chars == "" { + w.chars = " " + } + + r := []rune(w.chars) + j := 0 + b := strings.Builder{} + + // Cycle through runes and print them into the whitespace. + for i := 0; i < width; { + b.WriteRune(r[j]) + j++ + if j >= len(r) { + j = 0 + } + i += charmansi.StringWidth(string(r[j])) + } + + // Fill any extra gaps white spaces. This might be necessary if any runes + // are more than one cell wide, which could leave a one-rune gap. + short := width - charmansi.StringWidth(b.String()) + if short > 0 { + b.WriteString(strings.Repeat(" ", short)) + } + + return w.style.Styled(b.String()) +} + +// PlaceOverlay places fg on top of bg. +func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string { + fgLines, fgWidth := getLines(fg) + bgLines, bgWidth := getLines(bg) + bgHeight := len(bgLines) + fgHeight := len(fgLines) + + if fgWidth >= bgWidth && fgHeight >= bgHeight { + // FIXME: return fg or bg? + return fg + } + // TODO: allow placement outside of the bg box? + x = clamp(x, 0, bgWidth-fgWidth) + y = clamp(y, 0, bgHeight-fgHeight) + + ws := &whitespace{} + for _, opt := range opts { + opt(ws) + } + + var b strings.Builder + for i, bgLine := range bgLines { + if i > 0 { + b.WriteByte('\n') + } + if i < y || i >= y+fgHeight { + b.WriteString(bgLine) + continue + } + + pos := 0 + if x > 0 { + left := truncate.String(bgLine, uint(x)) + pos = ansi.PrintableRuneWidth(left) + b.WriteString(left) + if pos < x { + b.WriteString(ws.render(x - pos)) + pos = x + } + } + + fgLine := fgLines[i-y] + b.WriteString(fgLine) + pos += ansi.PrintableRuneWidth(fgLine) + + right := cutLeft(bgLine, pos) + bgWidth := ansi.PrintableRuneWidth(bgLine) + rightWidth := ansi.PrintableRuneWidth(right) + if rightWidth <= bgWidth-pos { + b.WriteString(ws.render(bgWidth - rightWidth - pos)) + } + + b.WriteString(right) + } + + return b.String() +} + +// cutLeft cuts printable characters from the left. +// This function is heavily based on muesli's ansi and truncate packages. +func cutLeft(s string, cutWidth int) string { + var ( + pos int + isAnsi bool + ab bytes.Buffer + b bytes.Buffer + ) + for _, c := range s { + var w int + if c == ansi.Marker || isAnsi { + isAnsi = true + ab.WriteRune(c) + if ansi.IsTerminator(c) { + isAnsi = false + if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { + ab.Reset() + } + } + } else { + w = runewidth.RuneWidth(c) + } + + if pos >= cutWidth { + if b.Len() == 0 { + if ab.Len() > 0 { + b.Write(ab.Bytes()) + } + if pos-cutWidth > 1 { + b.WriteByte(' ') + continue + } + } + b.WriteRune(c) + } + pos += w + } + return b.String() +} + +func clamp(v, lower, upper int) int { + return min(max(v, lower), upper) +} + +// Split a string into lines, additionally returning the size of the widest +// line. +func getLines(s string) (lines []string, widest int) { + lines = strings.Split(s, "\n") + + for _, l := range lines { + w := charmansi.StringWidth(l) + if widest < w { + widest = w + } + } + + return lines, widest +} + + +// import ( +// "bytes" +// "regexp" +// "strings" +// "unicode/utf8" + +// "github.com/mattn/go-runewidth" +// "github.com/muesli/reflow/ansi" +// "github.com/muesli/reflow/truncate" +// "github.com/muesli/termenv" +// "github.com/rivo/uniseg" +// ) + +// var re = regexp.MustCompile(`\x1b\[[0-9;]*[mK]`) + +// func placeOverlay(x, y int, bg, fg string) string { +// BGLines := strings.Split(bg, "\n") +// FGLines := strings.Split(fg, "\n") + +// FGNoStyleText := noStyleString(fg) +// FGNoStyleTextLines := strings.Split(FGNoStyleText, "\n") + +// BGNoStyleText := noStyleString(bg) +// BGNoStyleTextLines := strings.Split(BGNoStyleText, "\n") + +// for Y := y; Y < y+len(FGNoStyleTextLines); Y++ { +// end := x+utf8.RuneCountInString(FGNoStyleTextLines[Y - y]) + +// // BGAsciiIndexes := re.FindAllStringIndex(BGLines[Y], -1) +// // BGAsciiStringList := mapCoordsList(BGLines[Y], BGAsciiIndexes) + +// BGLines[Y] = replaceString(x,end, BGNoStyleTextLines[Y], FGLines[Y-y]) +// } + +// newBGText := strings.Join(BGLines, "\n") +// return newBGText +// } + +// func substring(start, end int, str string) string { +// runes := []rune(str) + +// substr := string(runes[start:end]) + +// return substr +// } + +// func insertString(index int, str, insertStr string) string { +// runes := []rune(str) + +// beforeIndex := string(runes[:index]) + +// afterIndex := string(runes[index:]) + +// return beforeIndex + insertStr + afterIndex +// } + +// func replaceString(start, end int, str, replaceStr string) string { +// runes := []rune(str) + +// beforeX := string(runes[:start]) + +// afterY := string(runes[end:]) + +// return beforeX + replaceStr + afterY +// } + +// func noStyleString(styledText string) string { +// re := regexp.MustCompile(`\x1b\[[0-9;]*[mK]`) + +// plainText := re.ReplaceAllString(styledText, "") + +// return plainText +// } + +// func checkIsAsciiString(locationX int, asciiStringList [][]int) bool { +// for _, loc := range asciiStringList { +// if locationX < loc[0] || locationX > loc[1] { +// return false +// } +// } +// return true +// } + +// func mapCoordsList(s string, indexes [][]int) (graphemeCoordsList [][]int) { +// for _, loc := range indexes { +// loc = mapCoords(s, loc) +// graphemeCoordsList = append(graphemeCoordsList, loc) +// } +// return +// } + +// func mapCoords(s string, byteCoords []int) (graphemeCoords []int) { +// graphemeCoords = make([]int, 2) +// gr := uniseg.NewGraphemes(s) +// graphemeIndex := -1 +// for gr.Next() { +// graphemeIndex++ +// a, b := gr.Positions() +// if a == byteCoords[0] { +// graphemeCoords[0] = graphemeIndex +// } +// if b == byteCoords[1] { +// graphemeCoords[1] = graphemeIndex + 1 +// break +// } +// } +// return +// } + +// type Renderer struct { +// output *termenv.Output +// colorProfile termenv.Profile +// hasDarkBackground bool +// } + +// type whitespace struct { +// re *Renderer +// style termenv.Style +// chars string +// } +// type WhitespaceOption func(*whitespace) + +// func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string { +// fgLines, fgWidth := getLines(fg) +// bgLines, bgWidth := getLines(bg) +// bgHeight := len(bgLines) +// fgHeight := len(fgLines) + +// if fgWidth >= bgWidth && fgHeight >= bgHeight { +// // FIXME: return fg or bg? +// return fg +// } +// // TODO: allow placement outside of the bg box? +// x = clamp(x, 0, bgWidth-fgWidth) +// y = clamp(y, 0, bgHeight-fgHeight) + +// ws := &whitespace{} +// for _, opt := range opts { +// opt(ws) +// } + +// var b strings.Builder +// for i, bgLine := range bgLines { +// if i > 0 { +// b.WriteByte('\n') +// } +// if i < y || i >= y+fgHeight { +// b.WriteString(bgLine) +// continue +// } + +// pos := 0 +// if x > 0 { +// left := truncate.String(bgLine, uint(x)) +// pos = ansi.PrintableRuneWidth(left) +// b.WriteString(left) +// if pos < x { +// b.WriteString(ws.render(x - pos)) +// pos = x +// } +// } + +// fgLine := fgLines[i-y] +// b.WriteString(fgLine) +// pos += ansi.PrintableRuneWidth(fgLine) + +// right := cutLeft(bgLine, pos) +// bgWidth := ansi.PrintableRuneWidth(bgLine) +// rightWidth := ansi.PrintableRuneWidth(right) +// if rightWidth <= bgWidth-pos { +// b.WriteString(ws.render(bgWidth - rightWidth - pos)) +// } + +// b.WriteString(right) +// } + +// return b.String() +// } + +// // cutLeft cuts printable characters from the left. +// // This function is heavily based on muesli's ansi and truncate packages. +// func cutLeft(s string, cutWidth int) string { +// var ( +// pos int +// isAnsi bool +// ab bytes.Buffer +// b bytes.Buffer +// ) +// for _, c := range s { +// var w int +// if c == ansi.Marker || isAnsi { +// isAnsi = true +// ab.WriteRune(c) +// if ansi.IsTerminator(c) { +// isAnsi = false +// if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { +// ab.Reset() +// } +// } +// } else { +// w = runewidth.RuneWidth(c) +// } + +// if pos >= cutWidth { +// if b.Len() == 0 { +// if ab.Len() > 0 { +// b.Write(ab.Bytes()) +// } +// if pos-cutWidth > 1 { +// b.WriteByte(' ') +// continue +// } +// } +// b.WriteRune(c) +// } +// pos += w +// } +// return b.String() +// } + +// func clamp(v, lower, upper int) int { +// return min(max(v, lower), upper) +// } + +// func getLines(s string) (lines []string, widest int) { +// lines = strings.Split(s, "\n") + +// for _, l := range lines { +// w := ansi.PrintableRuneWidth(l) +// if widest < w { +// widest = w +// } +// } + +// return lines, widest +// } \ No newline at end of file diff --git a/src/components/string_function.go b/src/components/string_function.go index 906da414..df9ebd6b 100644 --- a/src/components/string_function.go +++ b/src/components/string_function.go @@ -64,4 +64,46 @@ func clipboardPrettierName(name string, width int, isDir bool, isSelected bool) Render(style.icon + " ") + filePanelStyle.Render(truncateTextBeginning(name, width)) } -} \ No newline at end of file +} + +// func placeOverlay(x, y int,background, placeModal string) string { +// lines := strings.Split(placeModal, "\n") +// lines = lines +// re := regexp.MustCompile(`\x1b\[[0-9;]*[mK]`) + +// // 示例字符串 +// str := "┏A我" + +// // 使用 FindAllStringIndex 找出所有匹配的位置 +// indexes := re.FindAllStringIndex(str, -1) +// outPutLog(str) +// // 檢查是否找到匹配 +// if indexes != nil { +// for _, loc := range indexes { +// loc = mapCoords(str, loc) +// outPutLog(fmt.Sprintf("匹配的開始位置: %d, 結束位置: %d", loc[0], loc[1])) +// } +// } else { +// outPutLog("沒有找到匹配") +// } + +// return "" +// } + +// func mapCoords(s string, byteCoords []int) (graphemeCoords []int) { +// graphemeCoords = make([]int, 2) +// gr := uniseg.NewGraphemes(s) +// graphemeIndex := -1 +// for gr.Next() { +// graphemeIndex++ +// a, b := gr.Positions() +// if a == byteCoords[0] { +// graphemeCoords[0] = graphemeIndex +// } +// if b == byteCoords[1] { +// graphemeCoords[1] = graphemeIndex + 1 +// break +// } +// } +// return +// } \ No newline at end of file diff --git a/src/go.mod b/src/go.mod index 2ced9590..402d8fa4 100644 --- a/src/go.mod +++ b/src/go.mod @@ -24,6 +24,7 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -31,10 +32,10 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.15 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/reflow v0.3.0 github.com/pelletier/go-toml/v2 v2.2.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -42,7 +43,7 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index 1f39b631..b763e863 100644 --- a/src/go.sum +++ b/src/go.sum @@ -12,6 +12,8 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f h1:1BXkZqDueTOBECyDoFGRi0xMYgjJ6vvoPIkWyKOwzTc= +github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -83,8 +85,8 @@ golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= From b9c7436c9302c3fd7227e74033182da76928bdb1 Mon Sep 17 00:00:00 2001 From: MHNightCat <107802416+MHNightCat@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:26:19 +0000 Subject: [PATCH 2/3] chore: update gomod2nix --- src/gomod2nix.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gomod2nix.toml b/src/gomod2nix.toml index 5f9532cb..e76e04b9 100644 --- a/src/gomod2nix.toml +++ b/src/gomod2nix.toml @@ -22,6 +22,9 @@ schema = 3 [mod."github.com/charmbracelet/lipgloss"] version = "v0.10.0" hash = "sha256-JZD1iVeizYe0mp3qQcJbUZdYN6HP/DNC67ja79DTe6s=" + [mod."github.com/charmbracelet/x/exp/term"] + version = "v0.0.0-20240425164147-ba2a9512b05f" + hash = "sha256-7bnNJJx2DVH6gCO6oGBdctto5z56PgDYZqAxuZP35Q0=" [mod."github.com/containerd/console"] version = "v1.0.4-0.20230313162750-1ae8d489ac81" hash = "sha256-Qus81DgpWHJ6RRqeKOKcUFvzCxvPzygJqBabvBsBuHU=" @@ -101,8 +104,8 @@ schema = 3 version = "v0.5.0" hash = "sha256-EAKeODSsct5HhXPmpWJfulKSCkuUu6kkDttnjyZMNcI=" [mod."golang.org/x/sys"] - version = "v0.15.0" - hash = "sha256-n7TlABF6179RzGq3gctPDKDPRtDfnwPdjNCMm8ps2KY=" + version = "v0.19.0" + hash = "sha256-cmuL31TYLJmDm/fDnI2Sn0wB88cpdOHV1+urorsJWx4=" [mod."golang.org/x/term"] version = "v0.6.0" hash = "sha256-Ao0yXpwY8GyG+/23dVfJUYrfEfNUTES3RF45v1VhUAk=" From 36ba9578941fdc87fe402ffb9a81aa9723dd731e Mon Sep 17 00:00:00 2001 From: Night Cat <107802416+MHNightCat@users.noreply.github.com> Date: Sat, 27 Apr 2024 20:43:37 +0800 Subject: [PATCH 3/3] update all modal to pop up(still show other panel) --- src/components/default_config.go | 7 +- src/components/model.go | 15 +- src/components/model_render.go | 1 + src/components/overPlace.go | 406 ------------------------------- src/components/over_place.go | 168 +++++++++++++ 5 files changed, 187 insertions(+), 410 deletions(-) delete mode 100644 src/components/overPlace.go create mode 100644 src/components/over_place.go diff --git a/src/components/default_config.go b/src/components/default_config.go index 865be14f..b13df508 100644 --- a/src/components/default_config.go +++ b/src/components/default_config.go @@ -68,13 +68,18 @@ metadata = false func getHelpMenuData() []helpMenuModalData { data := []helpMenuModalData{ { - subTitle: "測試用途", + subTitle: "General", }, { hotkey: hotkeys.Quit, description: "Quit", hotkeyWorkType: globalType, }, + { + hotkey: hotkeys.OpenHelpMenu, + description: "Open help menu(hotkeylist)", + hotkeyWorkType: globalType, + }, { subTitle: "Panel navigation", }, diff --git a/src/components/model.go b/src/components/model.go index e7e9264e..638dccce 100644 --- a/src/components/model.go +++ b/src/components/model.go @@ -237,15 +237,24 @@ func (m model) View() string { if m.helpMenu.open { helpMenu := helpMenuRender(m) - return PlaceOverlay(10, 10, helpMenu, finalRender) + overlayX := m.fullWidth / 2 - m.helpMenu.width / 2 + overlayY := m.fullHeight / 2 - m.helpMenu.height / 2 + + return PlaceOverlay(overlayX, overlayY, helpMenu, finalRender) } + if m.typingModal.open { typingModal := typineModalRender(m) - return PlaceOverlay(10, 10, typingModal, finalRender) + overlayX := m.fullWidth / 2 - modalWidth / 2 + overlayY := m.fullHeight / 2 - modalHeight / 2 + return PlaceOverlay(overlayX, overlayY, typingModal, finalRender) } + if m.warnModal.open { warnModal := warnModalRender(m) - return PlaceOverlay(10, 10, warnModal, finalRender) + overlayX := m.fullWidth / 2 - modalWidth / 2 + overlayY := m.fullHeight / 2 - modalHeight / 2 + return PlaceOverlay(overlayX, overlayY, warnModal, finalRender) } return finalRender } diff --git a/src/components/model_render.go b/src/components/model_render.go index a6019df3..8cdc5a3a 100644 --- a/src/components/model_render.go +++ b/src/components/model_render.go @@ -345,6 +345,7 @@ func typineModalRender(m model) string { } } + func warnModalRender(m model) string { title := m.warnModal.title content := m.warnModal.content diff --git a/src/components/overPlace.go b/src/components/overPlace.go deleted file mode 100644 index a75b9f65..00000000 --- a/src/components/overPlace.go +++ /dev/null @@ -1,406 +0,0 @@ -package components - -import ( - "bytes" - "strings" - - charmansi "github.com/charmbracelet/x/exp/term/ansi" - "github.com/mattn/go-runewidth" - ansi "github.com/muesli/reflow/ansi" - "github.com/muesli/reflow/truncate" - "github.com/muesli/termenv" -) - -// whitespace is a whitespace renderer. -type whitespace struct { - style termenv.Style - chars string -} - -type WhitespaceOption func(*whitespace) - -// Render whitespaces. -func (w whitespace) render(width int) string { - if w.chars == "" { - w.chars = " " - } - - r := []rune(w.chars) - j := 0 - b := strings.Builder{} - - // Cycle through runes and print them into the whitespace. - for i := 0; i < width; { - b.WriteRune(r[j]) - j++ - if j >= len(r) { - j = 0 - } - i += charmansi.StringWidth(string(r[j])) - } - - // Fill any extra gaps white spaces. This might be necessary if any runes - // are more than one cell wide, which could leave a one-rune gap. - short := width - charmansi.StringWidth(b.String()) - if short > 0 { - b.WriteString(strings.Repeat(" ", short)) - } - - return w.style.Styled(b.String()) -} - -// PlaceOverlay places fg on top of bg. -func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string { - fgLines, fgWidth := getLines(fg) - bgLines, bgWidth := getLines(bg) - bgHeight := len(bgLines) - fgHeight := len(fgLines) - - if fgWidth >= bgWidth && fgHeight >= bgHeight { - // FIXME: return fg or bg? - return fg - } - // TODO: allow placement outside of the bg box? - x = clamp(x, 0, bgWidth-fgWidth) - y = clamp(y, 0, bgHeight-fgHeight) - - ws := &whitespace{} - for _, opt := range opts { - opt(ws) - } - - var b strings.Builder - for i, bgLine := range bgLines { - if i > 0 { - b.WriteByte('\n') - } - if i < y || i >= y+fgHeight { - b.WriteString(bgLine) - continue - } - - pos := 0 - if x > 0 { - left := truncate.String(bgLine, uint(x)) - pos = ansi.PrintableRuneWidth(left) - b.WriteString(left) - if pos < x { - b.WriteString(ws.render(x - pos)) - pos = x - } - } - - fgLine := fgLines[i-y] - b.WriteString(fgLine) - pos += ansi.PrintableRuneWidth(fgLine) - - right := cutLeft(bgLine, pos) - bgWidth := ansi.PrintableRuneWidth(bgLine) - rightWidth := ansi.PrintableRuneWidth(right) - if rightWidth <= bgWidth-pos { - b.WriteString(ws.render(bgWidth - rightWidth - pos)) - } - - b.WriteString(right) - } - - return b.String() -} - -// cutLeft cuts printable characters from the left. -// This function is heavily based on muesli's ansi and truncate packages. -func cutLeft(s string, cutWidth int) string { - var ( - pos int - isAnsi bool - ab bytes.Buffer - b bytes.Buffer - ) - for _, c := range s { - var w int - if c == ansi.Marker || isAnsi { - isAnsi = true - ab.WriteRune(c) - if ansi.IsTerminator(c) { - isAnsi = false - if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { - ab.Reset() - } - } - } else { - w = runewidth.RuneWidth(c) - } - - if pos >= cutWidth { - if b.Len() == 0 { - if ab.Len() > 0 { - b.Write(ab.Bytes()) - } - if pos-cutWidth > 1 { - b.WriteByte(' ') - continue - } - } - b.WriteRune(c) - } - pos += w - } - return b.String() -} - -func clamp(v, lower, upper int) int { - return min(max(v, lower), upper) -} - -// Split a string into lines, additionally returning the size of the widest -// line. -func getLines(s string) (lines []string, widest int) { - lines = strings.Split(s, "\n") - - for _, l := range lines { - w := charmansi.StringWidth(l) - if widest < w { - widest = w - } - } - - return lines, widest -} - - -// import ( -// "bytes" -// "regexp" -// "strings" -// "unicode/utf8" - -// "github.com/mattn/go-runewidth" -// "github.com/muesli/reflow/ansi" -// "github.com/muesli/reflow/truncate" -// "github.com/muesli/termenv" -// "github.com/rivo/uniseg" -// ) - -// var re = regexp.MustCompile(`\x1b\[[0-9;]*[mK]`) - -// func placeOverlay(x, y int, bg, fg string) string { -// BGLines := strings.Split(bg, "\n") -// FGLines := strings.Split(fg, "\n") - -// FGNoStyleText := noStyleString(fg) -// FGNoStyleTextLines := strings.Split(FGNoStyleText, "\n") - -// BGNoStyleText := noStyleString(bg) -// BGNoStyleTextLines := strings.Split(BGNoStyleText, "\n") - -// for Y := y; Y < y+len(FGNoStyleTextLines); Y++ { -// end := x+utf8.RuneCountInString(FGNoStyleTextLines[Y - y]) - -// // BGAsciiIndexes := re.FindAllStringIndex(BGLines[Y], -1) -// // BGAsciiStringList := mapCoordsList(BGLines[Y], BGAsciiIndexes) - -// BGLines[Y] = replaceString(x,end, BGNoStyleTextLines[Y], FGLines[Y-y]) -// } - -// newBGText := strings.Join(BGLines, "\n") -// return newBGText -// } - -// func substring(start, end int, str string) string { -// runes := []rune(str) - -// substr := string(runes[start:end]) - -// return substr -// } - -// func insertString(index int, str, insertStr string) string { -// runes := []rune(str) - -// beforeIndex := string(runes[:index]) - -// afterIndex := string(runes[index:]) - -// return beforeIndex + insertStr + afterIndex -// } - -// func replaceString(start, end int, str, replaceStr string) string { -// runes := []rune(str) - -// beforeX := string(runes[:start]) - -// afterY := string(runes[end:]) - -// return beforeX + replaceStr + afterY -// } - -// func noStyleString(styledText string) string { -// re := regexp.MustCompile(`\x1b\[[0-9;]*[mK]`) - -// plainText := re.ReplaceAllString(styledText, "") - -// return plainText -// } - -// func checkIsAsciiString(locationX int, asciiStringList [][]int) bool { -// for _, loc := range asciiStringList { -// if locationX < loc[0] || locationX > loc[1] { -// return false -// } -// } -// return true -// } - -// func mapCoordsList(s string, indexes [][]int) (graphemeCoordsList [][]int) { -// for _, loc := range indexes { -// loc = mapCoords(s, loc) -// graphemeCoordsList = append(graphemeCoordsList, loc) -// } -// return -// } - -// func mapCoords(s string, byteCoords []int) (graphemeCoords []int) { -// graphemeCoords = make([]int, 2) -// gr := uniseg.NewGraphemes(s) -// graphemeIndex := -1 -// for gr.Next() { -// graphemeIndex++ -// a, b := gr.Positions() -// if a == byteCoords[0] { -// graphemeCoords[0] = graphemeIndex -// } -// if b == byteCoords[1] { -// graphemeCoords[1] = graphemeIndex + 1 -// break -// } -// } -// return -// } - -// type Renderer struct { -// output *termenv.Output -// colorProfile termenv.Profile -// hasDarkBackground bool -// } - -// type whitespace struct { -// re *Renderer -// style termenv.Style -// chars string -// } -// type WhitespaceOption func(*whitespace) - -// func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string { -// fgLines, fgWidth := getLines(fg) -// bgLines, bgWidth := getLines(bg) -// bgHeight := len(bgLines) -// fgHeight := len(fgLines) - -// if fgWidth >= bgWidth && fgHeight >= bgHeight { -// // FIXME: return fg or bg? -// return fg -// } -// // TODO: allow placement outside of the bg box? -// x = clamp(x, 0, bgWidth-fgWidth) -// y = clamp(y, 0, bgHeight-fgHeight) - -// ws := &whitespace{} -// for _, opt := range opts { -// opt(ws) -// } - -// var b strings.Builder -// for i, bgLine := range bgLines { -// if i > 0 { -// b.WriteByte('\n') -// } -// if i < y || i >= y+fgHeight { -// b.WriteString(bgLine) -// continue -// } - -// pos := 0 -// if x > 0 { -// left := truncate.String(bgLine, uint(x)) -// pos = ansi.PrintableRuneWidth(left) -// b.WriteString(left) -// if pos < x { -// b.WriteString(ws.render(x - pos)) -// pos = x -// } -// } - -// fgLine := fgLines[i-y] -// b.WriteString(fgLine) -// pos += ansi.PrintableRuneWidth(fgLine) - -// right := cutLeft(bgLine, pos) -// bgWidth := ansi.PrintableRuneWidth(bgLine) -// rightWidth := ansi.PrintableRuneWidth(right) -// if rightWidth <= bgWidth-pos { -// b.WriteString(ws.render(bgWidth - rightWidth - pos)) -// } - -// b.WriteString(right) -// } - -// return b.String() -// } - -// // cutLeft cuts printable characters from the left. -// // This function is heavily based on muesli's ansi and truncate packages. -// func cutLeft(s string, cutWidth int) string { -// var ( -// pos int -// isAnsi bool -// ab bytes.Buffer -// b bytes.Buffer -// ) -// for _, c := range s { -// var w int -// if c == ansi.Marker || isAnsi { -// isAnsi = true -// ab.WriteRune(c) -// if ansi.IsTerminator(c) { -// isAnsi = false -// if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { -// ab.Reset() -// } -// } -// } else { -// w = runewidth.RuneWidth(c) -// } - -// if pos >= cutWidth { -// if b.Len() == 0 { -// if ab.Len() > 0 { -// b.Write(ab.Bytes()) -// } -// if pos-cutWidth > 1 { -// b.WriteByte(' ') -// continue -// } -// } -// b.WriteRune(c) -// } -// pos += w -// } -// return b.String() -// } - -// func clamp(v, lower, upper int) int { -// return min(max(v, lower), upper) -// } - -// func getLines(s string) (lines []string, widest int) { -// lines = strings.Split(s, "\n") - -// for _, l := range lines { -// w := ansi.PrintableRuneWidth(l) -// if widest < w { -// widest = w -// } -// } - -// return lines, widest -// } \ No newline at end of file diff --git a/src/components/over_place.go b/src/components/over_place.go new file mode 100644 index 00000000..70d96ea5 --- /dev/null +++ b/src/components/over_place.go @@ -0,0 +1,168 @@ +package components + +import ( + "bytes" + "strings" + + charmansi "github.com/charmbracelet/x/exp/term/ansi" + "github.com/mattn/go-runewidth" + ansi "github.com/muesli/reflow/ansi" + "github.com/muesli/reflow/truncate" + "github.com/muesli/termenv" +) + +// whitespace is a whitespace renderer. +type whitespace struct { + style termenv.Style + chars string +} + +type WhitespaceOption func(*whitespace) + +// Render whitespaces. +func (w whitespace) render(width int) string { + if w.chars == "" { + w.chars = " " + } + + r := []rune(w.chars) + j := 0 + b := strings.Builder{} + + // Cycle through runes and print them into the whitespace. + for i := 0; i < width; { + b.WriteRune(r[j]) + j++ + if j >= len(r) { + j = 0 + } + i += charmansi.StringWidth(string(r[j])) + } + + // Fill any extra gaps white spaces. This might be necessary if any runes + // are more than one cell wide, which could leave a one-rune gap. + short := width - charmansi.StringWidth(b.String()) + if short > 0 { + b.WriteString(strings.Repeat(" ", short)) + } + + return w.style.Styled(b.String()) +} + +// PlaceOverlay places fg on top of bg. +func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string { + fgLines, fgWidth := getLines(fg) + bgLines, bgWidth := getLines(bg) + bgHeight := len(bgLines) + fgHeight := len(fgLines) + + if fgWidth >= bgWidth && fgHeight >= bgHeight { + // FIXME: return fg or bg? + return fg + } + // TODO: allow placement outside of the bg box? + x = clamp(x, 0, bgWidth-fgWidth) + y = clamp(y, 0, bgHeight-fgHeight) + + ws := &whitespace{} + for _, opt := range opts { + opt(ws) + } + + var b strings.Builder + for i, bgLine := range bgLines { + if i > 0 { + b.WriteByte('\n') + } + if i < y || i >= y+fgHeight { + b.WriteString(bgLine) + continue + } + + pos := 0 + if x > 0 { + left := truncate.String(bgLine, uint(x)) + pos = ansi.PrintableRuneWidth(left) + b.WriteString(left) + if pos < x { + b.WriteString(ws.render(x - pos)) + pos = x + } + } + + fgLine := fgLines[i-y] + b.WriteString(fgLine) + pos += ansi.PrintableRuneWidth(fgLine) + + right := cutLeft(bgLine, pos) + bgWidth := ansi.PrintableRuneWidth(bgLine) + rightWidth := ansi.PrintableRuneWidth(right) + if rightWidth <= bgWidth-pos { + b.WriteString(ws.render(bgWidth - rightWidth - pos)) + } + + b.WriteString(right) + } + + return b.String() +} + +// cutLeft cuts printable characters from the left. +// This function is heavily based on muesli's ansi and truncate packages. +func cutLeft(s string, cutWidth int) string { + var ( + pos int + isAnsi bool + ab bytes.Buffer + b bytes.Buffer + ) + for _, c := range s { + var w int + if c == ansi.Marker || isAnsi { + isAnsi = true + ab.WriteRune(c) + if ansi.IsTerminator(c) { + isAnsi = false + if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { + ab.Reset() + } + } + } else { + w = runewidth.RuneWidth(c) + } + + if pos >= cutWidth { + if b.Len() == 0 { + if ab.Len() > 0 { + b.Write(ab.Bytes()) + } + if pos-cutWidth > 1 { + b.WriteByte(' ') + continue + } + } + b.WriteRune(c) + } + pos += w + } + return b.String() +} + +func clamp(v, lower, upper int) int { + return min(max(v, lower), upper) +} + +// Split a string into lines, additionally returning the size of the widest +// line. +func getLines(s string) (lines []string, widest int) { + lines = strings.Split(s, "\n") + + for _, l := range lines { + w := charmansi.StringWidth(l) + if widest < w { + widest = w + } + } + + return lines, widest +} \ No newline at end of file