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