255 lines
No EOL
7.7 KiB
Go
Executable file
255 lines
No EOL
7.7 KiB
Go
Executable file
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// === Help экран ===
|
||
|
||
var helpSections = []helpSection{
|
||
{
|
||
title: "РЕЖИМЫ",
|
||
items: []helpItem{
|
||
{"i / a / A", "INSERT: вставить / после курсора / в конец строки"},
|
||
{"I / o / O", "INSERT: в начало / новая строка ниже / выше"},
|
||
{"Esc", "вернуться в NORMAL"},
|
||
{":", "режим команд"},
|
||
{"/", "режим поиска"},
|
||
},
|
||
},
|
||
{
|
||
title: "НАВИГАЦИЯ (NORMAL)",
|
||
items: []helpItem{
|
||
{"h j k l", "← ↓ ↑ →"},
|
||
{"w / b / e", "слово вперёд / назад / конец слова"},
|
||
{"0 / ^ / $", "начало / первый символ / конец строки"},
|
||
{"gg / G", "начало / конец файла"},
|
||
{"5G / 42G", "перейти на строку 5 / 42"},
|
||
{"Ctrl+D / Ctrl+U", "страница вниз / вверх"},
|
||
},
|
||
},
|
||
{
|
||
title: "НАВИГАЦИЯ (INSERT)",
|
||
items: []helpItem{
|
||
{"↑ ↓ ← →", "перемещение курсора"},
|
||
{"Home / End", "начало / конец строки"},
|
||
{"Delete", "удалить символ под курсором"},
|
||
},
|
||
},
|
||
{
|
||
title: "ПОИСК И ЗАМЕНА",
|
||
items: []helpItem{
|
||
{"/pattern", "поиск по файлу"},
|
||
{"n / N", "следующее / предыдущее совпадение"},
|
||
{"f{c} / F{c}", "символ c вправо / влево в строке"},
|
||
{"t{c} / T{c}", "до символа c вправо / влево"},
|
||
{"; / ,", "повторить f/t / в обратном направлении"},
|
||
{"%", "парная скобка () [] {}"},
|
||
{":s/old/new/", "заменить первое вхождение"},
|
||
{":s/old/new/g", "заменить все вхождения в файле"},
|
||
},
|
||
},
|
||
{
|
||
title: "РЕДАКТИРОВАНИЕ",
|
||
items: []helpItem{
|
||
{"x", "удалить символ под курсором"},
|
||
{"dd / 3dd", "удалить строку / 3 строки"},
|
||
{"dw / de / db", "удалить до слова / конца слова / назад"},
|
||
{"yy", "скопировать строку"},
|
||
{"p / P", "вставить ниже / выше"},
|
||
{"u", "отменить (undo)"},
|
||
{"Ctrl+R", "повторить (redo)"},
|
||
},
|
||
},
|
||
{
|
||
title: "ВЫДЕЛЕНИЕ (VISUAL LINE)",
|
||
items: []helpItem{
|
||
{"V", "войти в режим выделения строк"},
|
||
{"j / k", "расширить выделение вниз / вверх"},
|
||
{"gg / G", "выделить до начала / конца файла"},
|
||
{"d / x", "удалить выделенные строки"},
|
||
{"y", "скопировать выделенные строки"},
|
||
{"Esc / V", "выйти без действия"},
|
||
},
|
||
},
|
||
{
|
||
title: "ФАЙЛ",
|
||
items: []helpItem{
|
||
{":w", "сохранить"},
|
||
{":w path", "сохранить копию в path"},
|
||
{":saveas path", "сохранить как (переключиться на path)"},
|
||
{":q / :q!", "выйти / выйти без сохранения"},
|
||
{":wq / :x", "сохранить и выйти"},
|
||
{":e path", "открыть файл"},
|
||
{":e", "перезагрузить текущий файл"},
|
||
{":new", "новый пустой буфер"},
|
||
{":new path", "создать / открыть файл"},
|
||
{":N", "перейти на строку N"},
|
||
},
|
||
},
|
||
{
|
||
title: "ПАНЕЛИ",
|
||
items: []helpItem{
|
||
{"< / >", "уменьшить / увеличить ширину preview"},
|
||
{"Пробел", "показать / скрыть preview (только .md)"},
|
||
{":previewwidth N", "установить ширину preview"},
|
||
},
|
||
},
|
||
{
|
||
title: "ЭКСПОРТ И ЗАГРУЗКА",
|
||
items: []helpItem{
|
||
{":export pdf", "экспорт в PDF (рядом с файлом)"},
|
||
{":export pdf path", "экспорт в указанный путь"},
|
||
{":fetch URL", "скачать статью и открыть"},
|
||
{"grimoir URL", "запустить с URL (скачать сразу)"},
|
||
},
|
||
},
|
||
{
|
||
title: "НАСТРОЙКИ",
|
||
items: []helpItem{
|
||
{":set", "показать все настройки"},
|
||
{":set number / nonumber", "номера строк вкл / выкл"},
|
||
{":set tab_width=4", "ширина таба"},
|
||
{":set autosave=60", "автосейв каждые N секунд (0=выкл)"},
|
||
{":config show", "откуда загружен конфиг"},
|
||
{":config paths", "пути поиска конфига"},
|
||
{":config write", "записать дефолтный конфиг"},
|
||
{":config reload", "перезагрузить конфиг"},
|
||
},
|
||
},
|
||
{
|
||
title: "HELP",
|
||
items: []helpItem{
|
||
{":help / :h / ?", "этот экран"},
|
||
{"q / Esc", "закрыть help"},
|
||
},
|
||
},
|
||
}
|
||
|
||
type helpSection struct {
|
||
title string
|
||
items []helpItem
|
||
}
|
||
|
||
type helpItem struct {
|
||
key string
|
||
desc string
|
||
}
|
||
|
||
// renderHelp рендерит help экран
|
||
func renderHelp(m *EditorModel) string {
|
||
var out strings.Builder
|
||
|
||
// Стили
|
||
headerStyle := lipgloss.NewStyle().
|
||
Background(lipgloss.Color("4")).
|
||
Foreground(lipgloss.Color("15")).
|
||
Bold(true).
|
||
Width(m.width).
|
||
Padding(0, 2)
|
||
|
||
sectionStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("11")).
|
||
Bold(true)
|
||
|
||
keyStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("81")).
|
||
Width(22)
|
||
|
||
descStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("252"))
|
||
|
||
dimStyle := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("8"))
|
||
|
||
footerStyle := lipgloss.NewStyle().
|
||
Background(lipgloss.Color("236")).
|
||
Foreground(lipgloss.Color("8")).
|
||
Width(m.width).
|
||
Padding(0, 2)
|
||
|
||
// Заголовок
|
||
out.WriteString(headerStyle.Render(" grimoir — справка по командам"))
|
||
out.WriteString("\n")
|
||
|
||
// Считаем сколько строк доступно
|
||
available := m.height - 3 // заголовок + footer + отступ
|
||
lineCount := 0
|
||
|
||
// Две колонки
|
||
colWidth := m.width / 2
|
||
leftCol := &strings.Builder{}
|
||
rightCol := &strings.Builder{}
|
||
|
||
mid := len(helpSections) / 2
|
||
if len(helpSections)%2 != 0 {
|
||
mid++
|
||
}
|
||
|
||
renderColumn := func(sections []helpSection, col *strings.Builder) {
|
||
for _, sec := range sections {
|
||
col.WriteString(sectionStyle.Render(" "+sec.title) + "\n")
|
||
for _, item := range sec.items {
|
||
key := keyStyle.Render(" " + item.key)
|
||
desc := descStyle.Render(item.desc)
|
||
col.WriteString(key + dimStyle.Render(" ") + desc + "\n")
|
||
}
|
||
col.WriteString("\n")
|
||
}
|
||
}
|
||
|
||
renderColumn(helpSections[:mid], leftCol)
|
||
renderColumn(helpSections[mid:], rightCol)
|
||
|
||
// Объединяем колонки построчно
|
||
leftLines := strings.Split(leftCol.String(), "\n")
|
||
rightLines := strings.Split(rightCol.String(), "\n")
|
||
|
||
maxLines := len(leftLines)
|
||
if len(rightLines) > maxLines {
|
||
maxLines = len(rightLines)
|
||
}
|
||
|
||
for i := 0; i < maxLines && lineCount < available; i++ {
|
||
left := ""
|
||
right := ""
|
||
if i < len(leftLines) {
|
||
left = leftLines[i]
|
||
}
|
||
if i < len(rightLines) {
|
||
right = rightLines[i]
|
||
}
|
||
|
||
// Обрезаем левую колонку до colWidth видимых символов
|
||
leftVis := visibleLen(left)
|
||
if leftVis < colWidth {
|
||
left += strings.Repeat(" ", colWidth-leftVis)
|
||
} else if leftVis > colWidth {
|
||
left = truncateANSI(left, colWidth)
|
||
}
|
||
|
||
out.WriteString(left)
|
||
out.WriteString(right)
|
||
out.WriteString("\n")
|
||
lineCount++
|
||
}
|
||
|
||
// Заполняем оставшееся место
|
||
for lineCount < available {
|
||
out.WriteString("\n")
|
||
lineCount++
|
||
}
|
||
|
||
// Footer
|
||
out.WriteString(footerStyle.Render(fmt.Sprintf(
|
||
" q/Esc — закрыть │ %s │ %d строк в файле",
|
||
grimVersion, len(m.lines),
|
||
)))
|
||
|
||
return out.String()
|
||
}
|
||
|
||
// visibleLen определена в render.go |