feat: improve textbox editor behaviour
This commit is contained in:
312
tui.go
312
tui.go
@@ -148,12 +148,14 @@ type App struct {
|
||||
pendingZ bool
|
||||
searching bool
|
||||
searchQuery string
|
||||
searchEditor textEditor
|
||||
searchOrigin int
|
||||
helpVisible bool
|
||||
helpScroll int
|
||||
writeMode writeMode
|
||||
writeThreadID string
|
||||
replyDraft string
|
||||
replyEditor textEditor
|
||||
resolveTarget bool
|
||||
autoMergeTarget bool
|
||||
mergeMethod string
|
||||
@@ -197,6 +199,7 @@ type App struct {
|
||||
aiMode aiMode
|
||||
aiMenuIndex int
|
||||
aiInput string
|
||||
aiInputEditor textEditor
|
||||
aiPreview AIPreview
|
||||
aiCancel context.CancelFunc
|
||||
aiStatus AIProviderStatus
|
||||
@@ -298,6 +301,18 @@ func (m App) Init() tea.Cmd {
|
||||
return tea.Batch(commands...)
|
||||
}
|
||||
|
||||
func (m *App) resetInputEditor(editor *textEditor, text string) {
|
||||
*editor = newTextEditor(text, m.editorMode == "vim")
|
||||
editor.keys = m.keybindings
|
||||
editor.hardwareCursor = m.cursorOutput != nil
|
||||
}
|
||||
|
||||
func (m *App) resetSearchEditor(text string) {
|
||||
m.searchEditor = newTextEditor(text, false)
|
||||
m.searchEditor.keys = m.keybindings
|
||||
m.searchEditor.hardwareCursor = m.cursorOutput != nil
|
||||
}
|
||||
|
||||
func (m App) nextTick() tea.Cmd {
|
||||
return tea.Tick(m.adaptivePollInterval(time.Now()), func(t time.Time) tea.Msg { return tickMsg(t) })
|
||||
}
|
||||
@@ -416,6 +431,7 @@ func (m *App) startReply() {
|
||||
}
|
||||
m.writeMode, m.writeThreadID, m.replyDraft, m.err = writeReply, thread.ID, "", nil
|
||||
m.restoreReplyDraft(thread.ID)
|
||||
m.resetInputEditor(&m.replyEditor, m.replyDraft)
|
||||
m.folded[thread.ID] = false
|
||||
m.focusThreadDetail()
|
||||
m.scroll = m.detailMaxScroll()
|
||||
@@ -583,30 +599,28 @@ func (m App) updateWriteInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case writeReply:
|
||||
switch k {
|
||||
case "esc":
|
||||
m.writeMode, m.replyDraft, m.writeThreadID = writeNone, "", ""
|
||||
m.scroll = min(m.scroll, m.detailMaxScroll())
|
||||
if m.replyEditor.Modal && m.replyEditor.Mode != textEditorNormal {
|
||||
m.replyEditor.handleKeyAtWidth(key, true, m.threadInputWidth())
|
||||
m.replyDraft = m.replyEditor.Text
|
||||
} else {
|
||||
m.writeMode, m.replyDraft, m.replyEditor, m.writeThreadID =
|
||||
writeNone, "", textEditor{}, ""
|
||||
m.scroll = min(m.scroll, m.detailMaxScroll())
|
||||
}
|
||||
case "ctrl+s":
|
||||
if strings.TrimSpace(m.replyDraft) == "" {
|
||||
m.err = errors.New("reply cannot be empty")
|
||||
} else {
|
||||
m.writeMode, m.err = writeReplyConfirm, nil
|
||||
}
|
||||
case "enter":
|
||||
m.replyDraft += "\n"
|
||||
case "backspace":
|
||||
runes := []rune(m.replyDraft)
|
||||
if len(runes) > 0 {
|
||||
m.replyDraft = string(runes[:len(runes)-1])
|
||||
}
|
||||
m.err = nil
|
||||
default:
|
||||
if key.Type == tea.KeyRunes || key.Type == tea.KeySpace {
|
||||
m.replyDraft += textInputKeyValue(key)
|
||||
if m.replyEditor.handleKeyAtWidth(key, true, m.threadInputWidth()) {
|
||||
m.replyDraft = m.replyEditor.Text
|
||||
m.err = nil
|
||||
}
|
||||
}
|
||||
if m.writeMode == writeReply {
|
||||
m.scroll = m.detailMaxScroll()
|
||||
m.ensureThreadInputCursorVisible()
|
||||
return m, m.queueReplyDraft()
|
||||
}
|
||||
case writeReplyConfirm:
|
||||
@@ -997,7 +1011,7 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if err := m.drafts.delete(draftKey); err != nil {
|
||||
m.recordHealth("draft persistence", healthWarning, err.Error())
|
||||
}
|
||||
m.replyDraft, m.writeThreadID = "", ""
|
||||
m.replyDraft, m.replyEditor, m.writeThreadID = "", textEditor{}, ""
|
||||
m.err = nil
|
||||
m.lastRefresh = time.Now()
|
||||
return m, m.difflet.setState(diffletSuccess)
|
||||
@@ -1130,29 +1144,34 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.searching, m.searchQuery = false, ""
|
||||
m.threadIndex = clamp(m.searchOrigin, 0, len(m.details.Threads)-1)
|
||||
m.scroll = 0
|
||||
if m.searchEditor.Modal && m.searchEditor.Mode != textEditorNormal {
|
||||
m.searchEditor.handleKey(key, false)
|
||||
m.searchQuery = m.searchEditor.Text
|
||||
} else {
|
||||
m.searching, m.searchQuery, m.searchEditor = false, "", textEditor{}
|
||||
m.threadIndex = clamp(m.searchOrigin, 0, len(m.details.Threads)-1)
|
||||
m.scroll = 0
|
||||
}
|
||||
case "enter":
|
||||
m.searching = false
|
||||
case "up":
|
||||
m.moveSearch(-1)
|
||||
case "down", "tab":
|
||||
m.moveSearch(1)
|
||||
case "backspace":
|
||||
runes := []rune(m.searchQuery)
|
||||
if len(runes) > 0 {
|
||||
m.searchQuery = string(runes[:len(runes)-1])
|
||||
m.selectBestSearchMatch()
|
||||
}
|
||||
case "ctrl+u":
|
||||
m.searchQuery = ""
|
||||
m.threadIndex = clamp(m.searchOrigin, 0, len(m.details.Threads)-1)
|
||||
m.scroll = 0
|
||||
if !m.searchEditor.Modal || m.searchEditor.Mode == textEditorInsert {
|
||||
m.resetSearchEditor("")
|
||||
m.searchQuery = ""
|
||||
m.threadIndex = clamp(m.searchOrigin, 0, len(m.details.Threads)-1)
|
||||
m.scroll = 0
|
||||
}
|
||||
default:
|
||||
if key.Type == tea.KeyRunes || key.Type == tea.KeySpace {
|
||||
m.searchQuery += textInputKeyValue(key)
|
||||
m.selectBestSearchMatch()
|
||||
before := m.searchEditor.Text
|
||||
if m.searchEditor.handleKey(key, false) {
|
||||
m.searchQuery = m.searchEditor.Text
|
||||
if m.searchQuery != before {
|
||||
m.selectBestSearchMatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
@@ -1205,12 +1224,13 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case "F":
|
||||
if m.screen == threadScreen {
|
||||
m.searchQuery = ""
|
||||
m.searchQuery, m.searchEditor = "", textEditor{}
|
||||
m.threadIndex = clamp(m.threadIndex, 0, len(m.details.Threads)-1)
|
||||
}
|
||||
case "/":
|
||||
if m.screen == threadScreen {
|
||||
m.searching, m.searchQuery, m.searchOrigin = true, "", m.threadIndex
|
||||
m.resetSearchEditor("")
|
||||
m.focus, m.listHidden, m.scroll = threadListPane, false, 0
|
||||
}
|
||||
case "r":
|
||||
@@ -2168,17 +2188,20 @@ func (m App) viewWritePopup() string {
|
||||
"",
|
||||
}
|
||||
lines = append(lines, renderTextInput(
|
||||
m.replyDraft, width-2, m.cursorOutput != nil,
|
||||
m.replyEditor, width-2, m.cursorOutput != nil,
|
||||
)...)
|
||||
if m.err != nil {
|
||||
lines = append(lines, "", badStyle.Render(m.err.Error()))
|
||||
}
|
||||
lines = append(lines, "", dimStyle.Render(fmt.Sprintf(
|
||||
"%s newline • %s review • %s cancel",
|
||||
primaryKeyLabel(m.keybindings.Input.Newline),
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
)))
|
||||
if m.editorMode != "vim" {
|
||||
lines = append(lines, "", dimStyle.Render(fmt.Sprintf(
|
||||
"%s newline • %s review • %s %s",
|
||||
primaryKeyLabel(m.keybindings.Input.Newline),
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
m.inputCancelAction(),
|
||||
)))
|
||||
}
|
||||
case writeReplyConfirm:
|
||||
lines = []string{titleStyle.Render("Submit this reply to " + location + "?"), ""}
|
||||
lines = append(lines, renderCommentMarkdown(m.replyDraft, width-2)...)
|
||||
@@ -2293,9 +2316,9 @@ func (m App) helpBindings() []helpBinding {
|
||||
{keyLabel(m.keybindings.Input.Cancel), "Return to Normal mode or cancel the editor"},
|
||||
{combinedKeyLabel(m.keybindings.Navigation.PageDown, m.keybindings.Navigation.PageUp), "Move through the description by half a page"},
|
||||
{combinedKeyLabel(m.keybindings.Input.PreviousCompletion, m.keybindings.Input.NextCompletion), "Select the previous / next branch or user completion"},
|
||||
{combinedKeyLabel(m.keybindings.Input.NextField, m.keybindings.Input.Newline), "Complete the selected branch or user"},
|
||||
{keyLabel(m.keybindings.Input.Newline), "Complete the selected branch or user"},
|
||||
}
|
||||
if m.prEditEditors[prEditBodyField].Modal {
|
||||
if m.editorMode == "vim" {
|
||||
bindings = append(bindings,
|
||||
helpBinding{combinedKeyLabel(
|
||||
m.keybindings.Navigation.Left, m.keybindings.Navigation.Down,
|
||||
@@ -2312,7 +2335,7 @@ func (m App) helpBindings() []helpBinding {
|
||||
), "Move to the line start, first non-blank, or line end"},
|
||||
helpBinding{combinedKeyLabel(
|
||||
m.keybindings.Vim.GoPrefix, m.keybindings.Navigation.Last,
|
||||
), "Move to the start / end of the description"},
|
||||
), "Move to the start / end of the active field"},
|
||||
helpBinding{combinedKeyLabel(
|
||||
m.keybindings.Vim.Insert, m.keybindings.Vim.Append,
|
||||
m.keybindings.Vim.InsertLineStart, m.keybindings.Vim.AppendLineEnd,
|
||||
@@ -2320,7 +2343,7 @@ func (m App) helpBindings() []helpBinding {
|
||||
), "Enter Insert mode"},
|
||||
helpBinding{combinedKeyLabel(
|
||||
m.keybindings.Vim.OpenBelow, m.keybindings.Vim.OpenAbove,
|
||||
), "Open a line below / above"},
|
||||
), "Open a description line below / above"},
|
||||
helpBinding{combinedKeyLabel(
|
||||
m.keybindings.Vim.Visual, m.keybindings.Vim.VisualLine,
|
||||
), "Start character-wise / line-wise Visual mode"},
|
||||
@@ -2343,7 +2366,7 @@ func (m App) helpBindings() []helpBinding {
|
||||
helpBinding{combinedKeyLabel(
|
||||
m.keybindings.Navigation.Left, m.keybindings.Navigation.Down,
|
||||
m.keybindings.Navigation.Up, m.keybindings.Navigation.Right,
|
||||
), "Move the description cursor"},
|
||||
), "Move the active field cursor"},
|
||||
helpBinding{combinedKeyLabel(
|
||||
m.keybindings.Input.LineStart, m.keybindings.Input.LineEnd,
|
||||
), "Move to the start / end of the current visual line"},
|
||||
@@ -2695,6 +2718,34 @@ func (m App) viewDashboardWithLines(lines []string) string {
|
||||
return view
|
||||
}
|
||||
|
||||
func (m App) activeInputEditor() (textEditor, string, bool) {
|
||||
if m.helpVisible {
|
||||
return textEditor{}, "", false
|
||||
}
|
||||
switch {
|
||||
case m.writeMode == writeReply:
|
||||
return m.replyEditor, "REPLY", m.replyEditor.Modal
|
||||
case m.aiMode == aiDiscussion:
|
||||
return m.aiInputEditor, "AI DISCUSSION", m.aiInputEditor.Modal
|
||||
case m.writeMode == writePREdit:
|
||||
contexts := [...]string{
|
||||
"EDIT TITLE", "EDIT TARGET BRANCH", "EDIT REVIEWERS",
|
||||
"EDIT ASSIGNEES", "EDIT DESCRIPTION",
|
||||
}
|
||||
editor := m.prEditEditors[m.prEditField]
|
||||
return editor, contexts[m.prEditField], editor.Modal
|
||||
default:
|
||||
return textEditor{}, "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (m App) inputCancelAction() string {
|
||||
if m.editorMode == "vim" {
|
||||
return "normal/cancel"
|
||||
}
|
||||
return "cancel"
|
||||
}
|
||||
|
||||
func (m App) dashboardDisplayLines() []string {
|
||||
mascot := m.difflet.frameLines()
|
||||
if len(mascot) == diffletHeight && !m.diffletHiddenForCurrentView() {
|
||||
@@ -3488,18 +3539,27 @@ func (m App) viewThreads() string {
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
)
|
||||
} else if m.writeMode == writeReply {
|
||||
help = fmt.Sprintf(
|
||||
"reply inline • %s newline • %s review • %s cancel",
|
||||
primaryKeyLabel(m.keybindings.Input.Newline),
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
)
|
||||
if m.editorMode == "vim" {
|
||||
help = fmt.Sprintf(
|
||||
"%s review • %s normal/cancel",
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
)
|
||||
} else {
|
||||
help = fmt.Sprintf(
|
||||
"reply inline • arrows move • %s newline • %s review • %s cancel",
|
||||
primaryKeyLabel(m.keybindings.Input.Newline),
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
)
|
||||
}
|
||||
} else if m.aiMode == aiDiscussion {
|
||||
help = fmt.Sprintf(
|
||||
"local AI discussion • %s newline • %s prepare • %s cancel",
|
||||
"local AI discussion • arrows move • %s newline • %s prepare • %s %s",
|
||||
primaryKeyLabel(m.keybindings.Input.Newline),
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
m.inputCancelAction(),
|
||||
)
|
||||
}
|
||||
m.positionThreadInputHardwareCursor()
|
||||
@@ -3520,9 +3580,8 @@ func (m App) threadList(width, height int) string {
|
||||
lines := []string{titleStyle.Render(fmt.Sprintf("Threads (%d/%d)", len(matches), len(m.details.Threads)))}
|
||||
if m.searching {
|
||||
queryWidth := max(1, innerWidth-len("Filter: ")-1)
|
||||
query := ansi.Truncate(m.searchQuery, queryWidth, "…")
|
||||
lines = append(lines, titleStyle.Render("Filter: ")+query+
|
||||
inputCursorFallback(m.cursorOutput != nil))
|
||||
query := renderSingleLineInput(m.searchEditor, queryWidth, m.cursorOutput != nil)
|
||||
lines = append(lines, titleStyle.Render("Filter: ")+query)
|
||||
}
|
||||
if m.loading && len(m.details.Threads) == 0 {
|
||||
lines = append(lines, "Loading…")
|
||||
@@ -3839,7 +3898,9 @@ func (m App) inlineReplyLines(width int) []detailLine {
|
||||
}
|
||||
textWidth := max(1, width-5)
|
||||
lineIndex := 0
|
||||
for _, part := range renderTextInput(m.replyDraft, textWidth, m.cursorOutput != nil) {
|
||||
for _, part := range renderTextInput(
|
||||
m.replyEditor, textWidth, m.cursorOutput != nil,
|
||||
) {
|
||||
lines = append(lines, detailLine{
|
||||
rail: rail, anchor: fmt.Sprintf("reply:body:%d", lineIndex), text: part,
|
||||
})
|
||||
@@ -3848,48 +3909,72 @@ func (m App) inlineReplyLines(width int) []detailLine {
|
||||
if m.err != nil {
|
||||
lines = append(lines, detailLine{rail: rail, text: badStyle.Render(m.err.Error())})
|
||||
}
|
||||
lines = append(lines, detailLine{
|
||||
rail: rail,
|
||||
text: dimStyle.Render(fmt.Sprintf(
|
||||
"%s newline • %s review • %s cancel",
|
||||
primaryKeyLabel(m.keybindings.Input.Newline),
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
)),
|
||||
})
|
||||
if m.editorMode != "vim" {
|
||||
lines = append(lines, detailLine{
|
||||
rail: rail,
|
||||
text: dimStyle.Render(fmt.Sprintf(
|
||||
"%s newline • %s review • %s %s",
|
||||
primaryKeyLabel(m.keybindings.Input.Newline),
|
||||
primaryKeyLabel(m.keybindings.Input.Submit),
|
||||
primaryKeyLabel(m.keybindings.Input.Cancel),
|
||||
m.inputCancelAction(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func inputCursorFallback(hardwareCursor bool) string {
|
||||
if hardwareCursor {
|
||||
return ""
|
||||
func renderTextInput(editor textEditor, width int, hardwareCursor bool) []string {
|
||||
editor.hardwareCursor = hardwareCursor
|
||||
rendered := renderTextEditor(editor, width, true)
|
||||
lines := make([]string, 0, len(rendered))
|
||||
for _, line := range rendered {
|
||||
lines = append(lines, line.text)
|
||||
}
|
||||
return "\x1b[4m \x1b[24m"
|
||||
return lines
|
||||
}
|
||||
|
||||
func renderTextInput(value string, width int, hardwareCursor bool) []string {
|
||||
const cursorSentinel = "\ue000"
|
||||
if hardwareCursor {
|
||||
value += cursorSentinel
|
||||
} else {
|
||||
value += inputCursorFallback(false)
|
||||
func renderSingleLineInput(editor textEditor, width int, hardwareCursor bool) string {
|
||||
editor.hardwareCursor = hardwareCursor
|
||||
rendered := renderTextEditor(editor, width, true)
|
||||
return rendered[editorCursorVisualLine(editor, width)].text
|
||||
}
|
||||
|
||||
func (m App) threadInputWidth() int {
|
||||
width, _ := m.detailPaneSize()
|
||||
return max(1, width-5)
|
||||
}
|
||||
|
||||
func (m *App) ensureThreadInputCursorVisible() {
|
||||
if m.writeMode != writeReply && m.aiMode != aiDiscussion {
|
||||
return
|
||||
}
|
||||
var lines []string
|
||||
for _, sourceLine := range strings.Split(value, "\n") {
|
||||
wrapped := ansi.Hardwrap(ansi.Wordwrap(sourceLine, width, ""), width, false)
|
||||
if hardwareCursor {
|
||||
wrapped = strings.TrimSuffix(wrapped, cursorSentinel)
|
||||
width, height := m.detailPaneSize()
|
||||
editor, prefix := m.replyEditor, "reply:body:"
|
||||
if m.aiMode == aiDiscussion {
|
||||
editor, prefix = m.aiInputEditor, "ai-discussion:body:"
|
||||
}
|
||||
visualLine := editorCursorVisualLine(editor, m.threadInputWidth())
|
||||
wantedAnchor := fmt.Sprintf("%s%d", prefix, visualLine)
|
||||
lines := m.renderedDetailLines(width)
|
||||
cursorLine := -1
|
||||
for index := range lines {
|
||||
if lines[index].anchor == wantedAnchor {
|
||||
cursorLine = index
|
||||
break
|
||||
}
|
||||
lines = append(lines, strings.Split(wrapped, "\n")...)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func textInputKeyValue(key tea.KeyMsg) string {
|
||||
if key.Type == tea.KeySpace {
|
||||
return " "
|
||||
if cursorLine < 0 {
|
||||
return
|
||||
}
|
||||
return string(key.Runes)
|
||||
viewportHeight := max(1, height-2)
|
||||
switch {
|
||||
case cursorLine < m.scroll:
|
||||
m.scroll = cursorLine
|
||||
case cursorLine >= m.scroll+viewportHeight:
|
||||
m.scroll = cursorLine - viewportHeight + 1
|
||||
}
|
||||
m.scroll = clamp(m.scroll, 0, max(0, len(lines)-viewportHeight))
|
||||
}
|
||||
|
||||
func (m App) positionThreadInputHardwareCursor() {
|
||||
@@ -3906,10 +3991,13 @@ func (m App) positionThreadInputHardwareCursor() {
|
||||
paneWidth = m.threadListWidth()
|
||||
}
|
||||
queryWidth := max(1, max(1, paneWidth-2)-len("Filter: ")-1)
|
||||
query := ansi.Truncate(m.searchQuery, queryWidth, "…")
|
||||
if m.searchEditor.Mode != textEditorInsert {
|
||||
return
|
||||
}
|
||||
_, column := editorCursorVisualPosition(m.searchEditor, queryWidth)
|
||||
m.cursorOutput.SetCursor(
|
||||
true,
|
||||
2+ansi.StringWidth("Filter: ")+ansi.StringWidth(query),
|
||||
2+ansi.StringWidth("Filter: ")+column,
|
||||
m.contentTop+topLines+3,
|
||||
)
|
||||
return
|
||||
@@ -3923,10 +4011,20 @@ func (m App) positionThreadInputHardwareCursor() {
|
||||
if m.aiMode == aiDiscussion {
|
||||
prefix = "ai-discussion:body:"
|
||||
}
|
||||
editor := m.replyEditor
|
||||
if m.aiMode == aiDiscussion {
|
||||
editor = m.aiInputEditor
|
||||
}
|
||||
if editor.Mode != textEditorInsert {
|
||||
return
|
||||
}
|
||||
visualLine, column := editorCursorVisualPosition(editor, m.threadInputWidth())
|
||||
cursorLine := -1
|
||||
wantedAnchor := fmt.Sprintf("%s%d", prefix, visualLine)
|
||||
for index := range lines {
|
||||
if strings.HasPrefix(lines[index].anchor, prefix) {
|
||||
if lines[index].anchor == wantedAnchor {
|
||||
cursorLine = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if cursorLine < 0 {
|
||||
@@ -3945,7 +4043,7 @@ func (m App) positionThreadInputHardwareCursor() {
|
||||
line := lines[cursorLine]
|
||||
m.cursorOutput.SetCursor(
|
||||
true,
|
||||
paneStart+2+ansi.StringWidth(line.rail)+ansi.StringWidth(line.fixed+line.text),
|
||||
paneStart+2+ansi.StringWidth(line.rail+line.fixed)+column,
|
||||
m.contentTop+topLines+2+screenLine,
|
||||
)
|
||||
}
|
||||
@@ -4226,21 +4324,47 @@ func (m App) frame(lines []string, help string) string {
|
||||
if status == "" && !m.lastRefresh.IsZero() {
|
||||
status = dimStyle.Render("updated " + m.lastRefresh.Format("15:04:05"))
|
||||
}
|
||||
footer := truncate(help, m.width)
|
||||
footerWidth := m.width
|
||||
if status != "" {
|
||||
footer = truncate(help, max(0, m.width-lipgloss.Width(status)-2)) + " " + status
|
||||
footerWidth = max(0, m.width-lipgloss.Width(status)-2)
|
||||
}
|
||||
footer := m.renderFooter(help, footerWidth)
|
||||
if status != "" {
|
||||
footer += " " + status
|
||||
}
|
||||
bodyLines := strings.Split(body, "\n")
|
||||
if len(bodyLines) > max(0, m.height-1) {
|
||||
bodyLines = bodyLines[:max(0, m.height-1)]
|
||||
bodyHeight := max(0, m.height-1)
|
||||
if len(bodyLines) > bodyHeight {
|
||||
bodyLines = bodyLines[:bodyHeight]
|
||||
}
|
||||
for i := range bodyLines {
|
||||
bodyLines[i] = ansi.Truncate(bodyLines[i], m.width, "")
|
||||
}
|
||||
bodyLines = append(bodyLines, ansi.Truncate(dimStyle.Render(footer), m.width, ""))
|
||||
for len(bodyLines) < bodyHeight {
|
||||
bodyLines = append(bodyLines, "")
|
||||
}
|
||||
bodyLines = append(bodyLines, ansi.Truncate(footer, m.width, ""))
|
||||
return lipgloss.NewStyle().Width(m.width).Height(m.height).Render(strings.Join(bodyLines, "\n"))
|
||||
}
|
||||
|
||||
func (m App) renderFooter(help string, width int) string {
|
||||
if width <= 0 {
|
||||
return ""
|
||||
}
|
||||
editor, context, ok := m.activeInputEditor()
|
||||
if !ok {
|
||||
return dimStyle.Render(truncate(help, width))
|
||||
}
|
||||
mode := activeStyle.Render(" " + editor.modeLabel() + " ")
|
||||
contextLabel := titleStyle.Render(context)
|
||||
prefix := mode + " " + contextLabel
|
||||
remaining := width - lipgloss.Width(prefix)
|
||||
if remaining <= 2 {
|
||||
return ansi.Truncate(prefix, width, "")
|
||||
}
|
||||
return prefix + " " + dimStyle.Render(truncate(help, remaining-2))
|
||||
}
|
||||
|
||||
func (m App) restingDiffletState() diffletState {
|
||||
if m.err != nil {
|
||||
return diffletSad
|
||||
|
||||
Reference in New Issue
Block a user