package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"math"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/awesome-gocui/gocui"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/encoding/unicode"
"gopkg.in/yaml.v3"
)
var programVersion string = "0.8.1"
// Структура конфигурации
type Config struct {
Hotkeys Hotkeys `yaml:"hotkeys"`
Settings Settings `yaml:"settings"`
}
// Структура доступных сочетаний клавиш для переопределения (#23)
type Hotkeys struct {
Help string `yaml:"help"`
Up string `yaml:"up"`
QuickUp string `yaml:"quickUp"`
VeryQuickUp string `yaml:"veryQuickUp"`
SwitchFilterMode string `yaml:"switchFilterMode"`
Down string `yaml:"down"`
QuickDown string `yaml:"quickDown"`
VeryQuickDown string `yaml:"veryQuickDown"`
BackSwitchFilterMode string `yaml:"backSwitchFilterMode"`
Left string `yaml:"left"`
Right string `yaml:"right"`
SwitchWindow string `yaml:"switchWindow"`
BackSwitchWindows string `yaml:"backSwitchWindows"`
LoadJournal string `yaml:"loadJournal"`
GoToFilter string `yaml:"goToFilter"`
GoToEnd string `yaml:"goToEnd"`
GoToTop string `yaml:"goToTop"`
TailModeMore string `yaml:"tailModeMore"`
TailModeLess string `yaml:"tailModeLess"`
UpdateIntervalMore string `yaml:"updateIntervalMore"`
UpdateIntervalLess string `yaml:"updateIntervalLess"`
AutoUpdateJournal string `yaml:"autoUpdateJournal"`
UpdateJournal string `yaml:"updateJournal"`
UpdateLists string `yaml:"updateLists"`
ColorDisable string `yaml:"colorDisable"`
TailspinEnable string `yaml:"tailspinEnable"`
SwitchDockerMode string `yaml:"switchDockerMode"`
SwitchStreamMode string `yaml:"switchStreamMode"`
TimestampShow string `yaml:"timestampShow"`
Exit string `yaml:"exit"`
}
// Структура доступных параметров для переопределения значений по умолчанию при запуске (#27)
type Settings struct {
TailMode string `yaml:"tailMode"`
UpdateInterval string `yaml:"updateInterval"`
DisableAutoUpdate string `yaml:"disableAutoUpdate"`
DisableColor string `yaml:"disableColor"`
DisableMouse string `yaml:"disableMouse"`
DisableTimestamp string `yaml:"disableTimestamp"`
OnlyStream string `yaml:"onlyStream"`
DisableFastMode string `yaml:"disableFastMode"`
}
// Структура хранения информации о журналах
type Journal struct {
name string // название журнала (имя службы) или дата загрузки
boot_id string // id загрузки системы
}
type Logfile struct {
name string
path string
}
type DockerContainers struct {
name string
id string
namespace string
}
// Структура для парсинга логов из docker cli
type dockerLogLines struct {
isError bool
timestamp time.Time
content string
}
// Основная структура приложения (графический интерфейс и данные журналов)
type App struct {
gui *gocui.Gui // графический интерфейс (gocui)
sshMode bool // использовать вызов команд (exec.Command) через ssh
sshOptions []string // опции для ssh подключения
fastMode bool // загрузка журналов в горутине (beta mode)
testMode bool // исключаем вызовы к gocui при тестирование функций
tailSpinMode bool // режим покраски через tailspin
tailSpinBinName string // название исполняемого файла (tailspin/tspin)
colorMode bool // отключение/включение покраски ключевых слов
mouseSupport bool // отключение/включение поддержки мыши
dockerStreamLogs bool // принудительное чтение журналов контейнеров Docker из потоков (по умолчанию, чтение происходит из файловой системы, если есть доступ)
dockerStreamLogsStr string // отображаемый режим чтения журнала Docker (в зависимости от прав доступа и флага)
dockerStreamMode string // переменная для хранения режима чтения потоков (all, stdout или stderr)
getOS string // название ОС
getArch string // архитектура процессора
hostName string // текущее имя хоста для покраски в логах
userName string // текущее имя пользователя
systemDisk string // порядковая буква системного диска для Windows
userNameArray []string // список всех пользователей
rootDirArray []string // список всех корневых каталогов
selectUnits string // название журнала (UNIT/USER_UNIT/kernel/audit)
selectPath string // путь к логам (/var/log/)
selectContainerizationSystem string // название системы контейнеризации (docker/compose/podman/kubernetes)
selectFilterMode string // режим фильтрации (default/fuzzy/regex)
logViewCount string // количество логов для просмотра
logUpdateSeconds int // период фонового обновления журнала
secondsChan chan int // канал для изменения интервала обновления в горутине
journals []Journal // список (массив/срез) журналов для отображения
maxVisibleServices int // максимальное количество видимых элементов в окне списка служб
startServices int // индекс первого видимого элемента
selectedJournal int // индекс выбранного журнала
logfiles []Logfile
maxVisibleFiles int
startFiles int
selectedFile int
dockerContainers []DockerContainers
maxVisibleDockerContainers int
startDockerContainers int
selectedDockerContainer int
// Фильтрация по времени
timestampFilterView bool // отображение окон
sinceTimestampFilterMode bool // использовать режим фильтрации для since
untilTimestampFilterMode bool // использовать режим фильтрации для until
sinceFilterText string // начало отрезка времени
untilFilterText string // конец отрезка времени
// Текст для фильтрации список журналов
filterListText string
// Массивы для хранения списка журналов без фильтрации
journalsNotFilter []Journal
logfilesNotFilter []Logfile
dockerContainersNotFilter []DockerContainers
// Переменные для отслеживания изменений размера окна
windowWidth int
windowHeight int
filterText string // текст для фильтрации записей журнала
currentLogLines []string // набор строк (срез) для хранения журнала без фильтрации
filteredLogLines []string // набор строк (срез) для хранения журнала после фильтра
logScrollPos int // позиция прокрутки для отображаемых строк журнала
lastFilterText string // фиксируем содержимое последнего ввода текста для фильтрации
autoScroll bool // используется для автоматического скроллинга вниз при обновлении (если это не ручной скроллинг)
disableAutoScroll bool // отключение автоматического обновления вывода
lastUpdateLine string // фиксируем предпоследнюю строку для делимитра
updateTime string // фиксируем время загрузки журнала для делимитра
lastDateUpdateFile time.Time // последняя дата изменения файла
lastSizeFile int64 // размер файла
updateFile bool // проверка для обновления вывода в горутине (отключение только если нет изменений в файле и для Windows Event)
lastWindow string // фиксируем последний используемый источник для вывода логов
lastSelected string // фиксируем название последнего выбранного журнала или контейнера
// Переменные для хранения значений автообновления вывода при смене окна
lastSelectUnits string
lastBootId string
lastLogPath string
lastContainerizationSystem string
lastContainerId string
// Цвета окон по умолчанию (изменяется в зависимости от доступности журналов)
journalListFrameColor gocui.Attribute
fileSystemFrameColor gocui.Attribute
dockerFrameColor gocui.Attribute
// Фиксируем последнее время загрузки и покраски журнала
debugStartTime time.Time
debugLoadTime string
debugColorTime string
// Отключение привязки горячих клавиш на время загрузки списка
keybindingsEnabled bool
// Отключение встроенных временных меток (timestamp) для логов Docker
timestampDocker bool
streamTypeDocker bool
// Регулярные выражения для покраски строк
trimHttpRegex *regexp.Regexp
trimHttpsRegex *regexp.Regexp
trimPrefixPathRegex *regexp.Regexp
trimPostfixPathRegex *regexp.Regexp
hexByteRegex *regexp.Regexp
dateTimeRegex *regexp.Regexp
integersInputRegex *regexp.Regexp
syslogUnitRegex *regexp.Regexp
lastCurrentView string // фиксируем последнее используемое окно для Esc после /
backCurrentView bool // отключаем/ключаем возврат
uniquePrefixColorMap map[string]string // карта для хранения уникального цвета для каждого контейнера в стеках compose
}
func showHelp() {
fmt.Println("lazyjournal - A TUI for reading logs from journald, auditd, file system, Docker containers, Podman and Kubernetes pods.")
fmt.Println("Source code: https://github.com/Lifailon/lazyjournal")
fmt.Println("If you have problems with the application, please open issue: https://github.com/Lifailon/lazyjournal/issues")
fmt.Println("")
fmt.Println(" Flags:")
fmt.Println(" --help, -h Show help")
fmt.Println(" --version, -v Show version")
fmt.Println(" --config, -g Show configuration of hotkeys and settings (check values)")
fmt.Println(" --audit, -a Show audit information")
fmt.Println(" --tail, -t Change the number of log lines to output (range: 200-200000, default: 50000)")
fmt.Println(" --update, -u Change the auto refresh interval of the log output (range: 2-10, default: 5)")
fmt.Println(" --disable-autoupdate, -e Disable streaming of new events (log is loaded once without automatic update)")
fmt.Println(" --disable-color, -d Disable output coloring")
fmt.Println(" --disable-mouse, -m Disable mouse control support")
fmt.Println(" --disable-timestamp, -p Disable timestamp for docker logs")
fmt.Println(" --only-stream, -o Force reading of docker container logs in stream mode (by default from the file system)")
fmt.Println(" --command-color, -c ANSI coloring in command line mode")
fmt.Println(" --command-fuzzy, -f Filtering using fuzzy search in command line mode")
fmt.Println(" --command-regex, -r Filtering using regular expression (regexp) in command line mode")
fmt.Println(" --ssh, -s Connect to remote host (use standard ssh options, separated by spaces in quotes)")
fmt.Println(" Example: lazyjournal --ssh \"lifailon@192.168.3.101 -p 22\"")
fmt.Println()
}
// Confi (#23)
func showConfig() {
// Читаем конфигурацию (извлекаем путь и ошибки)
configPath, err := config.getConfig()
fmt.Println("path:", configPath)
fmt.Println("---")
// Проверяем конфигурацию на ошибки
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Выводим содержимое конфигурации
// fmt.Println(string(configData))
// Выводим полученные значения из конфигурации (форматированный вывод) с проверкой на пустые значения
fmt.Println("hotkeys:")
fmt.Printf(" help: %s\n", config.Hotkeys.Help)
fmt.Printf(" up: %s\n", config.Hotkeys.Up)
fmt.Printf(" quickUp: %s\n", config.Hotkeys.QuickUp)
fmt.Printf(" veryQuickUp: %s\n", config.Hotkeys.VeryQuickUp)
fmt.Printf(" switchFilterMode: %s\n", config.Hotkeys.SwitchFilterMode)
fmt.Printf(" down: %s\n", config.Hotkeys.Down)
fmt.Printf(" quickDown: %s\n", config.Hotkeys.QuickDown)
fmt.Printf(" veryQuickDown: %s\n", config.Hotkeys.VeryQuickDown)
fmt.Printf(" backSwitchFilterMode: %s\n", config.Hotkeys.BackSwitchFilterMode)
fmt.Printf(" left: %s\n", config.Hotkeys.Left)
fmt.Printf(" right: %s\n", config.Hotkeys.Right)
fmt.Printf(" switchWindow: %s\n", config.Hotkeys.SwitchWindow)
fmt.Printf(" backSwitchWindows: %s\n", config.Hotkeys.BackSwitchWindows)
fmt.Printf(" loadJournal: %s\n", config.Hotkeys.LoadJournal)
fmt.Printf(" goToFilter: %s\n", config.Hotkeys.GoToFilter)
fmt.Printf(" goToEnd: %s\n", config.Hotkeys.GoToEnd)
fmt.Printf(" goToTop: %s\n", config.Hotkeys.GoToTop)
fmt.Printf(" tailModeMore: %s\n", config.Hotkeys.TailModeMore)
fmt.Printf(" tailModeLess: %s\n", config.Hotkeys.TailModeLess)
fmt.Printf(" updateIntervalMore: %s\n", config.Hotkeys.UpdateIntervalMore)
fmt.Printf(" updateIntervalLess: %s\n", config.Hotkeys.UpdateIntervalLess)
fmt.Printf(" autoUpdateJournal: %s\n", config.Hotkeys.AutoUpdateJournal)
fmt.Printf(" updateJournal: %s\n", config.Hotkeys.UpdateJournal)
fmt.Printf(" updateLists: %s\n", config.Hotkeys.UpdateLists)
fmt.Printf(" colorDisable: %s\n", config.Hotkeys.ColorDisable)
fmt.Printf(" tailspinEnable: %s\n", config.Hotkeys.TailspinEnable)
fmt.Printf(" switchDockerMode: %s\n", config.Hotkeys.SwitchDockerMode)
fmt.Printf(" switchStreamMode: %s\n", config.Hotkeys.SwitchStreamMode)
fmt.Printf(" timestampShow: %s\n", config.Hotkeys.TimestampShow)
fmt.Printf(" exit: %s\n", config.Hotkeys.Exit)
fmt.Println("settings:")
fmt.Printf(" tailMode: %s\n", config.Settings.TailMode)
fmt.Printf(" updateInterval: %s\n", config.Settings.UpdateInterval)
fmt.Printf(" disableColor: %s\n", config.Settings.DisableColor)
fmt.Printf(" disableAutoUpdate: %s\n", config.Settings.DisableAutoUpdate)
fmt.Printf(" disableMouse: %s\n", config.Settings.DisableMouse)
fmt.Printf(" disableTimestamp: %s\n", config.Settings.DisableTimestamp)
fmt.Printf(" onlyStream: %s\n", config.Settings.OnlyStream)
fmt.Printf(" disableFastMode: %s\n", config.Settings.DisableFastMode)
fmt.Println()
}
// Audit (#18) for homebrew
func (app *App) showAudit() {
var auditText []string
app.testMode = true
app.getOS = runtime.GOOS
auditText = append(auditText,
"system:",
" date: "+time.Now().Format("02.01.2006 15:04:05"),
" go: "+strings.ReplaceAll(runtime.Version(), "go", ""),
)
data, err := os.ReadFile("/etc/os-release")
// Если ошибка при чтении файла, то возвращаем только название ОС
if err != nil {
auditText = append(auditText, " os: "+app.getOS)
} else {
var name, version string
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "NAME=") {
name = strings.Trim(line[5:], "\"")
}
if strings.HasPrefix(line, "VERSION=") {
version = strings.Trim(line[8:], "\"")
}
}
auditText = append(auditText, " os: "+app.getOS+" "+name+" "+version)
}
auditText = append(auditText, " arch: "+app.getArch)
currentUser, _ := user.Current()
app.userName = currentUser.Username
if strings.Contains(app.userName, "\\") {
app.userName = strings.Split(app.userName, "\\")[1]
}
auditText = append(auditText, " username: "+app.userName)
if app.getOS != "windows" {
auditText = append(auditText, " privilege: "+(map[bool]string{true: "root", false: "user"})[os.Geteuid() == 0])
}
execPath, err := os.Executable()
if err == nil {
if strings.Contains(execPath, "go-build") {
auditText = append(auditText, " execType: source code")
} else {
auditText = append(auditText, " execType: binary file")
}
}
auditText = append(auditText, " execPath: "+execPath)
if app.getOS == "windows" {
// Windows Event
app.loadWinEvents()
auditText = append(auditText,
"winEvent:",
" logs: ",
" - count: "+strconv.Itoa(len(app.journals)),
)
// Filesystem
if app.userName != "runneradmin" {
app.systemDisk = os.Getenv("SystemDrive")
if len(app.systemDisk) >= 1 {
app.systemDisk = string(app.systemDisk[0])
} else {
app.systemDisk = "C"
}
auditText = append(auditText,
"fileSystem:",
" systemDisk: "+app.systemDisk,
" files:",
)
paths := []struct {
fullPath string
path string
}{
{"Program Files", "ProgramFiles"},
{"Program Files (x86)", "ProgramFiles86"},
{"ProgramData", "ProgramData"},
{"/AppData/Local", "AppDataLocal"},
{"/AppData/Roaming", "AppDataRoaming"},
}
// Создаем группу для ожидания выполнения всех горутин
var wg sync.WaitGroup
// Мьютекс для безопасного доступа к переменной auditText
var mu sync.Mutex
for _, path := range paths {
// Увеличиваем счетчик горутин
wg.Add(1)
go func(path struct{ fullPath, path string }) {
// Отнимаем счетчик горутин при завершении выполнения горутины
defer wg.Done()
var fullPath string
if strings.HasPrefix(path.fullPath, "Program") {
fullPath = "\"" + app.systemDisk + ":/" + path.fullPath + "\""
} else {
fullPath = "\"" + app.systemDisk + ":/Users/" + app.userName + path.fullPath + "\""
}
app.loadWinFiles(path.path)
lenLogFiles := strconv.Itoa(len(app.logfiles))
// Блокируем доступ на завись в переменную auditText
mu.Lock()
auditText = append(auditText,
" - path: "+fullPath,
" count: "+lenLogFiles,
)
// Разблокировать мьютекс
mu.Unlock()
}(path)
}
// Ожидаем завершения всех горутин
wg.Wait()
}
} else {
// systemd/journald
auditText = append(auditText,
"systemd:",
" journald:",
)
csCheck := exec.Command("journalctl", "--version")
_, err := csCheck.Output()
if err == nil {
auditText = append(auditText,
" - installed: true",
" journals:",
)
journalList := []struct {
name string
journalName string
}{
{"Unit list", "services"},
{"System journals", "UNIT"},
{"User journals", "USER_UNIT"},
{"Kernel boot", "kernel"},
}
for _, journal := range journalList {
app.loadServices(journal.journalName)
lenJournals := strconv.Itoa(len(app.journals))
auditText = append(auditText,
" - name: "+journal.name,
" count: "+lenJournals,
)
}
} else {
auditText = append(auditText, " - installed: false")
}
// Filesystem
auditText = append(auditText,
"fileSystem:",
" files:",
)
paths := []struct {
name string
path string
}{
{"System var logs", "/var/log/"},
{"Optional package logs", "/opt/"},
{"Users home logs", "/home/"},
{"Process descriptor logs", "descriptor"},
}
for _, path := range paths {
app.loadFiles(path.path)
lenLogFiles := strconv.Itoa(len(app.logfiles))
auditText = append(auditText,
" - name: "+path.name,
" path: "+path.path,
" count: "+lenLogFiles,
)
}
}
auditText = append(auditText,
"containerization: ",
" system: ",
)
containerizationSystems := []string{
"docker",
"docker compose",
"podman",
"kubernetes (kubectl)",
}
for _, cs := range containerizationSystems {
auditText = append(auditText, " - name: "+cs)
switch cs {
case "docker compose":
cs = "compose"
csCheck := exec.Command("docker", "compose", "version")
output, err := csCheck.Output()
if err == nil {
auditText = append(auditText, " installed: true")
csVersion := strings.TrimSpace(string(output))
csVersion = strings.Split(csVersion, "version v")[1]
auditText = append(auditText, " version: "+csVersion)
cmd := exec.Command(
"docker", "compose", "ls", "-a",
)
_, err := cmd.Output()
if err == nil {
app.loadDockerContainer(cs)
auditText = append(auditText, " stacks: "+strconv.Itoa(len(app.dockerContainers)))
} else {
auditText = append(auditText, " stacks: 0")
}
} else {
auditText = append(auditText, " installed: false")
}
case "kubernetes (kubectl)":
cs = "kubectl"
csCheck := exec.Command(cs, "version")
output, _ := csCheck.Output()
// По умолчанию у version код возврата всегда 1, по этому проверяем вывод
if strings.Contains(string(output), "Version:") {
auditText = append(auditText, " installed: true")
// Преобразуем байты в строку и обрезаем пробелы
csVersion := strings.TrimSpace(string(output))
// Удаляем текст до номера версии
csVersion = strings.Split(csVersion, "Version: v")[1]
// Забираем первую строку
csVersion = strings.Split(csVersion, "\n")[0]
auditText = append(auditText, " version: "+csVersion)
cmd := exec.Command(
cs, "get", "pods", "-A",
"-o", "jsonpath={range .items[*]}{.metadata.uid} {.metadata.name} {.status.phase}{'\\n'}{end}",
)
_, err := cmd.Output()
if err == nil {
app.loadDockerContainer(cs)
auditText = append(auditText, " pods: "+strconv.Itoa(len(app.dockerContainers)))
} else {
auditText = append(auditText, " pods: 0")
}
} else {
auditText = append(auditText, " installed: false")
}
default:
csCheck := exec.Command(cs, "--version")
output, err := csCheck.Output()
if err == nil {
auditText = append(auditText, " installed: true")
csVersion := strings.TrimSpace(string(output))
csVersion = strings.Split(csVersion, "version ")[1]
csVersion = strings.Split(csVersion, ", ")[0]
auditText = append(auditText, " version: "+csVersion)
cmd := exec.Command(
cs, "ps", "-a",
"--format", "{{.ID}} {{.Names}} {{.State}}",
)
_, err := cmd.Output()
if err == nil {
app.loadDockerContainer(cs)
auditText = append(auditText, " containers: "+strconv.Itoa(len(app.dockerContainers)))
} else {
auditText = append(auditText, " containers: 0")
}
} else {
auditText = append(auditText, " installed: false")
}
}
}
for _, line := range auditText {
fmt.Println(line)
}
}
// Объявляем конфигурацию
var config Config
// Читаем конфигурацию
func (config *Config) getConfig() (string, error) {
// Читаем файл конфигурации из текущего каталога
currentDir, err := os.Getwd()
if err != nil {
return "", err
}
configPath := filepath.Join(currentDir, "config.yml")
configData, err := os.ReadFile(configPath)
if err != nil {
// Из каталога с исполняемым файлом
execDir, err := os.Executable()
if err != nil {
return "", err
}
configPath = filepath.Join(execDir, "config.yml")
configData, err = os.ReadFile(configPath)
if err != nil {
// Из каталога ~/.config/lazyjournal/
homePath, _ := os.UserHomeDir()
configPath = filepath.Join(homePath, ".config", "lazyjournal", "config.yml")
configData, err = os.ReadFile(configPath)
if err != nil {
return configPath, ErrConfigNotFound
}
}
}
// Парсим yaml конфигурации
err = yaml.Unmarshal(configData, &config)
if err != nil {
return configPath, fmt.Errorf("%w: %w", ErrYamlSyntax, err)
}
return configPath, err
}
// Предварительная компиляция регулярных выражений для покраски вывода и их доступности в тестах
var (
// Исключаем все до http:// (включительно) в начале строки
trimHttpRegex = regexp.MustCompile(`^.*http://|([^a-zA-Z0-9:/._?&=+-].*)$`)
// И после любого символа, который не может содержать в себе url
trimHttpsRegex = regexp.MustCompile(`^.*https://|([^a-zA-Z0-9:/._?&=+-].*)$`)
// Иключаем все до первого символа слэша (не включительно)
trimPrefixPathRegex = regexp.MustCompile(`^[^/]+`)
// Исключаем все после первого символа, который не должен (но может) содержаться в пути
trimPostfixPathRegex = regexp.MustCompile(`[=:'"(){}\[\]]+.*$`)
// Байты или числа в шестнадцатеричном формате: 0x2 || 0xc0000001
hexByteRegex = regexp.MustCompile(`\b0x[0-9A-Fa-f]+\b`)
// DateTime: YYYY-MM-DDTHH:MM:SS.MS+HH:MM || YYYY-MM-DDTHH:MM:SS.MSZ
dateTimeRegex = regexp.MustCompile(`\b(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:\d{2}|Z))\b`)
// Integers: Int only + Time + MAC address + Percentage (int%) + Date2 (20/03/2025)
integersInputRegex = regexp.MustCompile(`^[^a-zA-Z]*\d+[^a-zA-Z]*$`)
// Syslog UNIT
syslogUnitRegex = regexp.MustCompile(`^[a-zA-Z-_.]+\[\d+\]:$`)
// Замена пробелов на T для фильтрации по дате+время
reSpace = regexp.MustCompile(`\s+`)
// Проверка формата времени (короткий формат)
filterTimeRegex = regexp.MustCompile(`^[+-]\d+[smhd]$`)
)
// Ошибки
var (
ErrConfigNotFound = errors.New("configuration file not found")
ErrYamlSyntax = errors.New("error yaml syntax in config file")
ErrSSHConnection = errors.New("error connecting on SSH to")
ErrInvalidStat = errors.New("invalid stat output")
)
var (
uniquePrefixColorArr = []string{
"\033[32m", // Зеленый
"\033[33m", // Желтый
"\033[34m", // Синий
"\033[35m", // Пурпурный
"\033[36m", // Голубой
}
)
// Определяем название удаленной системы
func remoteGetOS(sshOptions []string) (string, error) {
cmd := exec.Command("ssh", append(sshOptions, "uname", "-s")...)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("%w: %s", ErrSSHConnection, sshOptions[0])
} else {
return strings.ToLower(string(output)), nil
}
}
var g *gocui.Gui
func runGoCui(mock bool) {
// Инициализация значений по умолчанию + компиляция регулярных выражений для покраски
app := &App{
sshMode: false,
fastMode: true,
testMode: false,
tailSpinMode: false,
colorMode: true,
mouseSupport: true,
dockerStreamLogs: false,
dockerStreamMode: "all",
startServices: 0, // начальная позиция списка юнитов
selectedJournal: 0, // начальный индекс выбранного журнала
startFiles: 0,
selectedFile: 0,
startDockerContainers: 0,
selectedDockerContainer: 0,
debugLoadTime: "0s",
debugColorTime: "0s",
selectUnits: "services", // "UNIT" || "USER_UNIT" || "kernel" || "audit"
selectPath: "/var/log/", // "/opt/", "/home/" или "/Users/" (для macOS) + /root/ || "descriptor"
selectContainerizationSystem: "docker", // "compose" || "podman" || "kubernetes"
selectFilterMode: "default", // "fuzzy" || "regex" || "timestamp"
timestampFilterView: false,
sinceTimestampFilterMode: false,
untilTimestampFilterMode: false,
journalListFrameColor: gocui.ColorDefault,
fileSystemFrameColor: gocui.ColorDefault,
dockerFrameColor: gocui.ColorDefault,
autoScroll: true,
trimHttpRegex: trimHttpRegex,
trimHttpsRegex: trimHttpsRegex,
trimPrefixPathRegex: trimPrefixPathRegex,
trimPostfixPathRegex: trimPostfixPathRegex,
hexByteRegex: hexByteRegex,
dateTimeRegex: dateTimeRegex,
integersInputRegex: integersInputRegex,
syslogUnitRegex: syslogUnitRegex,
keybindingsEnabled: true,
timestampDocker: true,
streamTypeDocker: true,
lastCurrentView: "services",
backCurrentView: false,
uniquePrefixColorMap: make(map[string]string),
}
// Определяем используемую ОС (linux/darwin/*bsd/windows) и архитектуру
app.getOS = runtime.GOOS
app.getArch = runtime.GOARCH
// Аргументы
help := flag.Bool("help", false, "Show help")
flag.BoolVar(help, "h", false, "Show help")
version := flag.Bool("version", false, "Show version")
flag.BoolVar(version, "v", false, "Show version")
configFlag := flag.Bool("config", false, "Show configuration of hotkeys and settings (check values)")
flag.BoolVar(configFlag, "g", false, "Show configuration of hotkeys and settings (check values)")
audit := flag.Bool("audit", false, "Show audit information")
flag.BoolVar(audit, "a", false, "Show audit information")
tailFlag := flag.String("tail", "50000", "Change the number of log lines to output (range: 200-200000, default: 50000)")
flag.StringVar(tailFlag, "t", "50000", "Change the number of log lines to output (range: 200-200000, default: 50000)")
updateFlag := flag.Int("update", 5, "Change the auto refresh interval of the log output (range: 2-10, default: 5)")
flag.IntVar(updateFlag, "u", 5, "Change the auto refresh interval of the log output (range: 2-10, default: 5)")
disableScroll := flag.Bool("disable-autoupdate", false, "Disable streaming of new events (log is loaded once without automatic update)")
flag.BoolVar(disableScroll, "e", false, "Disable streaming of new events (log is loaded once without automatic update)")
disableColor := flag.Bool("disable-color", false, "Disable output coloring")
flag.BoolVar(disableColor, "d", false, "Disable output coloring")
disableMouse := flag.Bool("disable-mouse", false, "Disable mouse control support")
flag.BoolVar(disableMouse, "m", false, "Disable mouse control support")
disableTimeStamp := flag.Bool("disable-timestamp", false, "Disable timestamp for docker logs")
flag.BoolVar(disableTimeStamp, "p", false, "Disable timestamp for docker logs")
dockerStreamFlag := flag.Bool("only-stream", false, "Force reading of docker container logs in stream mode (by default from the file system)")
flag.BoolVar(dockerStreamFlag, "o", false, "Force reading of docker container logs in stream mode (by default from the file system)")
commandColor := flag.Bool("command-color", false, "ANSI coloring in command line mode")
flag.BoolVar(commandColor, "c", false, "ANSI coloring in command line mode")
commandFuzzy := flag.String("command-fuzzy", "", "Filtering using fuzzy search in command line mode")
flag.StringVar(commandFuzzy, "f", "", "Filtering using fuzzy search in command line mode")
commandRegex := flag.String("command-regex", "", "Filtering using regular expression (regexp) in command line mode")
flag.StringVar(commandRegex, "r", "", "Filtering using regular expression (regexp) in command line mode")
sshModeFlag := flag.String("ssh", "", "Connect to remote host (use standard SSH options, separated by spaces in quotes)")
flag.StringVar(sshModeFlag, "s", "", "Connect to remote host (use standard SSH options, separated by spaces in quotes)")
// Обработка аргументов
flag.Parse()
if *help {
showHelp()
os.Exit(0)
}
if *version {
fmt.Println(programVersion)
os.Exit(0)
}
if *configFlag {
showConfig()
os.Exit(0)
}
if *audit {
app.showAudit()
os.Exit(0)
}
// Проверяем и извлекаем значения настроек для флагов из конфигурации
_, errConfig := config.getConfig()
if errConfig != nil {
fmt.Println(errConfig)
}
if config.Settings.TailMode != "" && *tailFlag == "50000" {
tailFlag = &config.Settings.TailMode
}
if config.Settings.UpdateInterval != "" && *updateFlag == 5 {
updateIntervalInt, err := strconv.Atoi(config.Settings.UpdateInterval)
if err == nil {
updateFlag = &updateIntervalInt
}
}
if config.Settings.DisableAutoUpdate != "" && !*disableScroll {
if strings.EqualFold(config.Settings.DisableAutoUpdate, "true") {
trueFlag := true
disableScroll = &trueFlag
}
}
if config.Settings.DisableColor != "" && !*disableColor {
if strings.EqualFold(config.Settings.DisableColor, "true") {
trueFlag := true
disableColor = &trueFlag
}
}
if config.Settings.DisableMouse != "" && !*disableMouse {
if strings.EqualFold(config.Settings.DisableMouse, "true") {
trueFlag := true
disableMouse = &trueFlag
}
}
if config.Settings.DisableTimestamp != "" && !*disableTimeStamp {
if strings.EqualFold(config.Settings.DisableTimestamp, "true") {
trueFlag := true
disableTimeStamp = &trueFlag
}
}
if config.Settings.OnlyStream != "" && !*dockerStreamFlag {
if strings.EqualFold(config.Settings.OnlyStream, "true") {
trueFlag := true
dockerStreamFlag = &trueFlag
}
}
if config.Settings.DisableFastMode != "" {
if strings.EqualFold(config.Settings.DisableFastMode, "true") {
app.fastMode = false
}
}
// Обработка остальных флагов с учетом полученных данных из конфигурации
if *tailFlag == "200" || *tailFlag == "500" || *tailFlag == "1000" ||
*tailFlag == "5000" || *tailFlag == "10000" || *tailFlag == "20000" ||
*tailFlag == "30000" || *tailFlag == "50000" || *tailFlag == "100000" ||
*tailFlag == "150000" || *tailFlag == "200000" {
app.logViewCount = *tailFlag
} else {
// Если ошибка в конфигурации, задаем значение по умолчанию
if config.Settings.TailMode != "" && *tailFlag == "" {
app.logViewCount = "50000"
} else {
// Если ошибка в флаге, возвращяем ошибку
fmt.Println("Available values: 200, 500, 1000, 5000, 10000, 20000, 30000 50000, 100000, 150000, 200000 (default: 50000 lines)")
os.Exit(1)
}
}
if *updateFlag >= 2 && *updateFlag <= 10 {
app.logUpdateSeconds = *updateFlag
} else {
if config.Settings.UpdateInterval != "" && *updateFlag == 0 {
app.logUpdateSeconds = 5
} else {
fmt.Println("Valid range: 2-10 (default: 5 seconds)")
os.Exit(1)
}
}
if *disableScroll {
app.disableAutoScroll = true
app.autoScroll = false
}
if *disableColor {
app.colorMode = false
}
if *disableMouse {
app.mouseSupport = false
}
if *disableTimeStamp {
app.timestampDocker = false
}
if *dockerStreamFlag {
app.dockerStreamLogs = true
app.dockerStreamLogsStr = "stream"
} else {
// Проверяем доступность директории на чтение
dir := "/var/lib/docker/containers"
f, err := os.Open(dir)
if err != nil {
app.dockerStreamLogsStr = "stream"
} else {
// Пробуем прочитать имя первого элемента (проверить список файлов/директорий)
_, err = f.Readdirnames(1)
f.Close()
if err != nil {
app.dockerStreamLogsStr = "stream"
} else {
app.dockerStreamLogsStr = "json"
}
}
}
// Определяем переменные и массивы для покраски вывода
// Текущее имя хоста
app.hostName, _ = os.Hostname()
// Удаляем доменную часть, если она есть
if strings.Contains(app.hostName, ".") {
app.hostName = strings.Split(app.hostName, ".")[0]
}
// Текущее имя пользователя
currentUser, _ := user.Current()
app.userName = currentUser.Username
// Удаляем доменную часть, если она есть
if strings.Contains(app.userName, "\\") {
app.userName = strings.Split(app.userName, "\\")[1]
}
// Определяем букву системного диска с установленной ОС Windows
app.systemDisk = os.Getenv("SystemDrive")
if len(app.systemDisk) >= 1 {
app.systemDisk = string(app.systemDisk[0])
} else {
app.systemDisk = "C"
}
// Имена пользователей
passwd, _ := os.Open("/etc/passwd")
scanner := bufio.NewScanner(passwd)
for scanner.Scan() {
line := scanner.Text()
userName := strings.Split(line, ":")
if len(userName) > 0 {
app.userNameArray = append(app.userNameArray, userName[0])
}
}
// Список корневых каталогов (ls -d /*/) с приставкой "/"
files, _ := os.ReadDir("/")
for _, file := range files {
if file.IsDir() {
app.rootDirArray = append(app.rootDirArray, "/"+file.Name())
}
}
// Обработка покраски вывода в режиме командной строки
if *commandColor {
app.commandLineColor()
os.Exit(0)
}
// Обработка фильтрации с неточным поиском в режиме командной строки
if *commandFuzzy != "" {
app.commandLineFuzzy(*commandFuzzy)
os.Exit(0)
}
// Обработка фильтрации с поддержкой регулярных выражений в режиме командной строки
if *commandRegex != "" {
filter := strings.ToLower(*commandRegex)
// Добавляем флаг для нечувствительности к регистру по умолчанию
filter = "(?i)" + filter
// Компилируем и проверяем регулярное выражение
regex, err := regexp.Compile(filter)
if err != nil {
fmt.Println("Regular expression syntax error")
os.Exit(1)
}
app.commandLineRegex(regex)
os.Exit(0)
}
// Включаем режим ssh и заполняем параметры (включая sudo и другие стандартные опции ssh подключения, например, порт)
if *sshModeFlag != "" {
app.sshMode = true
options := strings.Split(*sshModeFlag, " ")
app.sshOptions = append(app.sshOptions, options...)
getOS, err := remoteGetOS(app.sshOptions)
if err != nil {
fmt.Println(err)
os.Exit(1)
} else {
app.getOS = getOS
}
}
// Создаем GUI
var err error
if mock {
g, err = gocui.NewGui(gocui.OutputSimulator, true) // 1-й параметр для режима работы терминала (tcell) и 2-й параметр для форка
} else {
g, err = gocui.NewGui(gocui.OutputNormal, true)
}
if err != nil {
log.Panicln(err)
}
// Закрываем GUI после завершения
defer g.Close()
app.gui = g
// Функция, которая будет вызываться при обновлении интерфейса
g.SetManagerFunc(app.layout)
// Включить поддержку мыши
if app.mouseSupport {
g.Mouse = true
}
// Цветовая схема GUI
g.FgColor = gocui.ColorDefault // поля всех окон и цвет текста
g.BgColor = gocui.ColorDefault // фон
// Привязка клавиш для работы с интерфейсом из функции setupKeybindings()
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
// Выполняем layout для инициализации интерфейса
if err := app.layout(g); err != nil {
log.Panicln(err)
}
// Фиксируем текущее количество видимых строк в терминале (-1 заголовок)
if v, err := g.View("services"); err == nil {
_, viewHeight := v.Size()
app.maxVisibleServices = viewHeight
}
// Загрузка списка служб или событий Windows
if app.getOS == "windows" {
v, err := g.View("services")
if err != nil {
log.Panicln(err)
}
v.Title = " < Windows Event Logs (0) > "
// Загружаем список событий Windows в горутине
go func() {
app.loadWinEvents()
}()
} else {
app.loadServices(app.selectUnits)
}
// Filesystem
if v, err := g.View("varLogs"); err == nil {
_, viewHeight := v.Size()
app.maxVisibleFiles = viewHeight
}
// Определяем ОС и загружаем файловые журналы
if app.getOS == "windows" {
selectedVarLog, err := g.View("varLogs")
if err != nil {
log.Panicln(err)
}
g.Update(func(g *gocui.Gui) error {
selectedVarLog.Clear()
fmt.Fprintln(selectedVarLog, "Searching log files...")
selectedVarLog.Highlight = false
return nil
})
selectedVarLog.Title = " < Program Files (0) > "
app.selectPath = "ProgramFiles"
// Загружаем список файлов Windows в горутине
go func() {
app.loadWinFiles(app.selectPath)
}()
} else {
app.loadFiles(app.selectPath)
}
// Docker
if v, err := g.View("docker"); err == nil {
_, viewHeight := v.Size()
app.maxVisibleDockerContainers = viewHeight
}
app.loadDockerContainer(app.selectContainerizationSystem)
// Устанавливаем фокус на окно с журналами по умолчанию
if _, err := g.SetCurrentView("filterList"); err != nil {
return
}
// Горутина для автоматического обновления вывода журнала каждые n (logUpdateSeconds) секунд
app.secondsChan = make(chan int, app.logUpdateSeconds)
go func() {
app.updateLogBackground(app.secondsChan, false)
}()
// Горутина для отслеживания изменений размера окна и его перерисовки
go func() {
app.updateWindowSize(1)
}()
// Запус GUI
if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) {
log.Panicln(err)
}
}
func main() {
runGoCui(false)
}
// Структура интерфейса окон GUI
func (app *App) layout(g *gocui.Gui) error {
maxX, maxY := g.Size() // получаем текущий размер интерфейса терминала (ширина, высота)
leftPanelWidth := maxX / 4 // ширина левой колонки
inputHeight := 3 // высота поля ввода для фильтрации список
availableHeight := maxY - inputHeight // общая высота всех трех окон слева
panelHeight := availableHeight / 3 // высота каждого окна
// Поле ввода для фильтрации списков
if v, err := g.SetView("filterList", 0, 0, leftPanelWidth-1, inputHeight-1, 0); err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
v.Title = "Filtering lists"
v.Editable = true
v.Wrap = true
v.FrameColor = gocui.ColorGreen // Цвет границ окна
v.TitleColor = gocui.ColorGreen // Цвет заголовка
v.Editor = app.createFilterEditor("lists")
}
// Окно для отображения списка доступных журналов (UNIT)
// Размеры окна: заголовок, отступ слева, отступ сверху, ширина, высота, 5-й параметр из форка для продолжение окна (2)
if v, err := g.SetView("services", 0, inputHeight, leftPanelWidth-1, inputHeight+panelHeight-1, 0); err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
v.Title = " < Unit list (0) > " // заголовок окна
v.Highlight = true // выделение активного элемента в списке
v.Wrap = false // отключаем перенос строк
v.Autoscroll = true // включаем автопрокрутку
// Цветовая схема из форка awesome-gocui/gocui
v.SelBgColor = gocui.ColorGreen // Цвет фона при выборе в списке
v.SelFgColor = gocui.ColorBlack // Цвет текста
app.updateServicesList() // выводим список журналов в это окно
}
// Окно для списка логов из файловой системы
if v, err := g.SetView("varLogs", 0, inputHeight+panelHeight, leftPanelWidth-1, inputHeight+2*panelHeight-1, 0); err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
v.Title = " < System var logs (0) > "
v.Highlight = true
v.Wrap = false
v.Autoscroll = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
app.updateLogsList()
}
// Окно для списка контейнеров Docker и Podman
if v, err := g.SetView("docker", 0, inputHeight+2*panelHeight, leftPanelWidth-1, maxY-1, 0); err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
v.Title = " < Docker containers (0) > "
v.Highlight = true
v.Wrap = false
v.Autoscroll = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
}
// Окно ввода текста для фильтрации
if v, err := g.SetView("filter", leftPanelWidth+1, 0, maxX-1, 2, 0); err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
v.Title = "Filter (Default)"
v.Editable = true // включить окно редактируемым для ввода текста
v.Editor = app.createFilterEditor("logs") // редактор для обработки ввода
v.Wrap = true
}
// Интерфейс скролла в окне вывода лога (maxX-3 ширина окна - отступ слева)
if v, err := g.SetView("scrollLogs", maxX-3, 3, maxX-1, maxY-1, 0); err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
v.Wrap = true
v.Autoscroll = false
// Цвет текста (зеленый)
v.FgColor = gocui.ColorGreen
// Заполняем окно стрелками
_, viewHeight := v.Size()
fmt.Fprintln(v, "▲")
for i := 1; i < viewHeight-1; i++ {
fmt.Fprintln(v, " ")
}
fmt.Fprintln(v, "▼")
}
// Окно для вывода записей выбранного журнала (maxX-2 для отступа скролла и 8 для продолжения углов)
if v, err := g.SetView("logs", leftPanelWidth+1, 3, maxX-1-2, maxY-1, 8); err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
v.Title = "Logs"
v.Wrap = true
v.Autoscroll = false
v.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
// Включение курсора в режиме фильтра и отключение в остальных окнах
currentView := g.CurrentView()
if currentView != nil && (currentView.Name() == "filter" || currentView.Name() == "filterList" || currentView.Name() == "sinceFilter" || currentView.Name() == "untilFilter") {
g.Cursor = true
} else {
g.Cursor = false
}
return nil
}
// ---------------------------------------- journalctl/Windows Event Logs ----------------------------------------
// Функция для удаления ANSI-символов покраски
func removeANSI(input string) string {
ansiEscapeRegex := regexp.MustCompile(`\033\[[0-9;]*m`)
return ansiEscapeRegex.ReplaceAllString(input, "")
}
// Функция для извлечения даты из строки для списка загрузок ядра
func parseDateFromName(name string) time.Time {
cleanName := removeANSI(name)
dateFormat := "02.01.2006 15:04:05"
// Извлекаем дату, начиная с 22-го символа (после дефиса)
parsedDate, _ := time.Parse(dateFormat, cleanName[22:])
return parsedDate
}
// Функция для загрузки списка журналов служб или загрузок системы из journald с помощью journalctl
func (app *App) loadServices(journalName string) {
app.journals = nil
// Проверка, что в системе установлен/поддерживается утилита journalctl
var checkJournald *exec.Cmd
if app.sshMode {
checkJournald = exec.Command("ssh", append(app.sshOptions, "journalctl", "--version")...)
} else {
checkJournald = exec.Command("journalctl", "--version")
}
// Проверяем на ошибки (очищаем список служб, отключаем курсор и выводим ошибку)
_, err := checkJournald.Output()
if err != nil && !app.testMode {
vError, _ := app.gui.View("services")
vError.Clear()
app.journalListFrameColor = gocui.ColorRed
vError.FrameColor = app.journalListFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31msystemd-journald not supported\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error: systemd-journald not supported")
}
switch {
// Services list from systemd
case journalName == "services":
// Получаем список всех юнитов в системе через systemctl в формате JSON
var unitsList *exec.Cmd
if app.sshMode {
unitsList = exec.Command("ssh", append(app.sshOptions, "systemctl", "list-units", "--all", "--plain", "--no-legend", "--no-pager", "--output=json")...) // "--type=service"
} else {
unitsList = exec.Command("systemctl", "list-units", "--all", "--plain", "--no-legend", "--no-pager", "--output=json") // "--type=service"
}
output, err := unitsList.Output()
if !app.testMode {
if err != nil {
vError, _ := app.gui.View("services")
vError.Clear()
app.journalListFrameColor = gocui.ColorRed
vError.FrameColor = app.journalListFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mAccess denied in systemd via systemctl\033[0m")
return
}
v, _ := app.gui.View("services")
app.journalListFrameColor = gocui.ColorDefault
if v.FrameColor != gocui.ColorDefault {
v.FrameColor = gocui.ColorGreen
}
v.Highlight = true
}
if err != nil && app.testMode {
log.Print("Error: access denied in systemd via systemctl")
}
// Чтение данных в формате JSON
var units []map[string]interface{}
err = json.Unmarshal(output, &units)
// Если ошибка JSON, создаем массив вручную
if err != nil {
lines := strings.Split(string(output), "\n")
for _, line := range lines {
// Разбиваем строку на поля (эквивалентно: awk '{print $1,$3,$4}')
fields := strings.Fields(line)
// Пропускаем строки с недостаточным количеством полей
if len(fields) < 3 {
continue
}
// Заполняем временный массив из строки
unit := map[string]interface{}{
"unit": fields[0],
"active": fields[2],
"sub": fields[3],
}
// Добавляем временный массив строки в основной массив
units = append(units, unit)
}
}
serviceMap := make(map[string]bool)
// Обработка записей
for _, unit := range units {
// Извлечение данных в формате JSON и проверка статуса для покраски
unitName, _ := unit["unit"].(string)
active, _ := unit["active"].(string)
if active == "active" {
active = "\033[32m" + active + "\033[0m"
} else {
active = "\033[31m" + active + "\033[0m"
}
sub, _ := unit["sub"].(string)
if sub == "exited" || sub == "dead" {
sub = "\033[31m" + sub + "\033[0m"
} else {
sub = "\033[32m" + sub + "\033[0m"
}
name := unitName + " (" + active + "/" + sub + ")"
bootID := unitName
// Уникальный ключ для проверки
uniqueKey := name + ":" + bootID
if !serviceMap[uniqueKey] {
serviceMap[uniqueKey] = true
// Добавление записи в массив
app.journals = append(app.journals, Journal{
name: name,
boot_id: bootID,
})
}
}
// Audit rules keys from auditd
case journalName == "auditd":
// Получаем список правил
var auditRulesList *exec.Cmd
if app.sshMode {
auditRulesList = exec.Command("ssh", append(app.sshOptions, "auditctl", "-l")...)
} else {
auditRulesList = exec.Command("auditctl", "-l")
}
output, err := auditRulesList.Output()
// Проверяем, что auditd установлен и на ошибку доступа
if !app.testMode {
if err != nil {
var errorText string
if err.Error() == "exit status 4" {
errorText = "Access denied in auditd via auditctl (root only)"
} else {
errorText = "Auditd not installed"
}
vError, _ := app.gui.View("services")
vError.Clear()
app.journalListFrameColor = gocui.ColorRed
vError.FrameColor = app.journalListFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31m"+errorText+"\033[0m")
return
}
v, _ := app.gui.View("services")
app.journalListFrameColor = gocui.ColorDefault
if v.FrameColor != gocui.ColorDefault {
v.FrameColor = gocui.ColorGreen
}
v.Highlight = true
}
if err != nil && app.testMode {
if strings.Contains(err.Error(), "root to run") {
log.Print("Access denied in auditd via auditctl (root only)")
} else {
log.Print("Auditd not installed")
}
}
// Заполняем список всех уникальный ключей
keysMap := make(map[string]bool)
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
rule := scanner.Text()
if strings.Contains(rule, "-k ") {
// Разбиваем строку правила на 2 части (split) до ключа
rulePart := strings.Split(rule, "-k ")
if len(rulePart) > 1 {
// Разбиваем на слова (fields) из второй части правила после ключа и извлекаем первое слово
keyPart := strings.Fields(rulePart[1])[0]
if !keysMap[keyPart] {
keysMap[keyPart] = true
app.journals = append(app.journals, Journal{
name: keyPart,
boot_id: keyPart,
})
}
}
}
}
// Boots list from journald
case journalName == "kernel":
// Получаем список загрузок системы
var bootCmd *exec.Cmd
if app.sshMode {
bootCmd = exec.Command("ssh", append(app.sshOptions, "journalctl", "--list-boots", "-o", "json")...)
} else {
bootCmd = exec.Command("journalctl", "--list-boots", "-o", "json")
}
bootOutput, err := bootCmd.Output()
if !app.testMode {
if err != nil {
vError, _ := app.gui.View("services")
vError.Clear()
app.journalListFrameColor = gocui.ColorRed
vError.FrameColor = app.journalListFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mError getting boot information from journald\033[0m")
return
} else {
vError, _ := app.gui.View("services")
app.journalListFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
}
if err != nil && app.testMode {
log.Print("Error: getting boot information from journald")
}
// Структура для парсинга JSON
type BootInfo struct {
BootID string `json:"boot_id"`
FirstEntry int64 `json:"first_entry"`
LastEntry int64 `json:"last_entry"`
}
var bootRecords []BootInfo
err = json.Unmarshal(bootOutput, &bootRecords)
// Если JSON невалидный или режим тестирования (Ubuntu 20.04 не поддерживает вывод в формате json)
if err != nil || app.testMode {
// Парсим вывод построчно
lines := strings.Split(string(bootOutput), "\n")
for _, line := range lines {
// Разбиваем строку на массив
wordsArray := strings.Fields(line)
// 0 d914ebeb67c6428a87f9cfe3861c295d Mon 2024-11-25 12:15:07 MSK—Mon 2024-11-25 18:34:53 MSK
if len(wordsArray) >= 8 {
bootId := wordsArray[1]
// Забираем дату, проверяем и изменяем формат
var parseDate []string
var bootDate string
parseDate = strings.Split(wordsArray[3], "-")
if len(parseDate) == 3 {
bootDate = fmt.Sprintf("%s.%s.%s", parseDate[2], parseDate[1], parseDate[0])
} else {
continue
}
var stopDate string
parseDate = strings.Split(wordsArray[6], "-")
if len(parseDate) == 3 {
stopDate = fmt.Sprintf("%s.%s.%s", parseDate[2], parseDate[1], parseDate[0])
} else {
continue
}
// Заполняем массив
bootDateTime := bootDate + " " + wordsArray[4]
stopDateTime := stopDate + " " + wordsArray[7]
app.journals = append(app.journals, Journal{
name: fmt.Sprintf("\033[34m%s\033[0m - \033[34m%s\033[0m", bootDateTime, stopDateTime),
boot_id: bootId,
})
}
}
}
if err == nil {
// Очищаем массив, если он был заполнен в режиме тестирования
app.journals = []Journal{}
// Добавляем информацию о загрузках в app.journals
for _, bootRecord := range bootRecords {
// Преобразуем наносекунды в секунды
firstEntryTime := time.Unix(bootRecord.FirstEntry/1000000, bootRecord.FirstEntry%1000000)
lastEntryTime := time.Unix(bootRecord.LastEntry/1000000, bootRecord.LastEntry%1000000)
// Форматируем строку в формате "DD.MM.YYYY HH:MM:SS"
const dateFormat = "02.01.2006 15:04:05"
name := fmt.Sprintf("\033[34m%s\033[0m - \033[34m%s\033[0m", firstEntryTime.Format(dateFormat), lastEntryTime.Format(dateFormat))
// Добавляем в массив
app.journals = append(app.journals, Journal{
name: name,
boot_id: bootRecord.BootID,
})
}
}
// Сортируем по второй дате
sort.Slice(app.journals, func(i, j int) bool {
date1 := parseDateFromName(app.journals[i].name)
date2 := parseDateFromName(app.journals[j].name)
// Сравниваем по второй дате в обратном порядке (After для сортировки по убыванию)
return date1.After(date2)
})
// Journals list from journald
default:
var cmd *exec.Cmd
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "journalctl", "--no-pager", "-F", journalName)...)
} else {
cmd = exec.Command("journalctl", "--no-pager", "-F", journalName)
}
output, err := cmd.Output()
if !app.testMode {
if err != nil {
vError, _ := app.gui.View("services")
vError.Clear()
app.journalListFrameColor = gocui.ColorRed
vError.FrameColor = app.journalListFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mError getting services from journald via journalctl\033[0m")
return
} else {
vError, _ := app.gui.View("services")
app.journalListFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
}
if err != nil && app.testMode {
log.Print("Error: getting services from journald via journalctl")
}
// Создаем массив (хеш-таблица с доступом по ключу) для уникальных имен служб
serviceMap := make(map[string]bool)
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
serviceName := strings.TrimSpace(scanner.Text())
if serviceName != "" && !serviceMap[serviceName] {
serviceMap[serviceName] = true
app.journals = append(app.journals, Journal{
name: serviceName,
boot_id: "",
})
}
}
// Сортируем список служб по алфавиту
sort.Slice(app.journals, func(i, j int) bool {
return app.journals[i].name < app.journals[j].name
})
}
if !app.testMode {
// Сохраняем неотфильтрованный список
app.journalsNotFilter = app.journals
// Применяем фильтр при загрузки и обновляем список служб в интерфейсе через updateServicesList() внутри функции
app.applyFilterList()
}
}
// Функция для загрузки списка всех журналов событий Windows через PowerShell
func (app *App) loadWinEvents() {
app.debugStartTime = time.Now()
app.journals = nil
// Получаем список, игнорируем ошибки, фильтруем пустые журналы, забираем нужные параметры, сортируем и выводим в формате JSON
cmd := exec.Command("powershell", "-Command",
"Get-WinEvent -ListLog * -ErrorAction Ignore | "+
"Where-Object RecordCount -ne 0 | "+
"Where-Object RecordCount -ne $null | "+
"Select-Object LogName,RecordCount | "+
"Sort-Object -Descending RecordCount | "+
"ConvertTo-Json")
eventsJson, _ := cmd.Output()
var events []map[string]interface{}
_ = json.Unmarshal(eventsJson, &events)
for _, event := range events {
// Извлечение названия журнала и количество записей
LogName, _ := event["LogName"].(string)
RecordCount, _ := event["RecordCount"].(float64)
RecordCountInt := int(RecordCount)
RecordCountString := strconv.Itoa(RecordCountInt)
// Удаляем приставку
LogView := strings.ReplaceAll(LogName, "Microsoft-Windows-", "")
// Разбивает строку на 2 части для покраски
LogViewSplit := strings.SplitN(LogView, "/", 2)
if len(LogViewSplit) == 2 {
LogView = "\033[33m" + LogViewSplit[0] + "\033[0m" + ": " + "\033[36m" + LogViewSplit[1] + "\033[0m"
} else {
LogView = "\033[36m" + LogView + "\033[0m"
}
LogView = LogView + " (" + RecordCountString + ")"
app.journals = append(app.journals, Journal{
name: LogView,
boot_id: LogName,
})
}
if !app.testMode {
app.journalsNotFilter = app.journals
app.applyFilterList()
}
}
// Функция для обновления окна со списком служб
func (app *App) updateServicesList() {
// Выбираем окно для заполнения в зависимости от используемого журнала
v, err := app.gui.View("services")
if err != nil {
return
}
// Очищаем окно
v.Clear()
// Вычисляем конечную позицию видимой области (стартовая позиция + максимальное количество видимых строк)
visibleEnd := app.startServices + app.maxVisibleServices
if visibleEnd > len(app.journals) {
visibleEnd = len(app.journals)
}
// Отображаем только элементы в пределах видимой области
for i := app.startServices; i < visibleEnd; i++ {
fmt.Fprintln(v, app.journals[i].name)
}
}
// Функция для перемещения по списку журналов вниз
func (app *App) nextService(v *gocui.View, step int) error {
// Обновляем текущее количество видимых строк в терминале (-1 заголовок)
_, viewHeight := v.Size()
app.maxVisibleServices = viewHeight
// Если список журналов пустой, ничего не делаем
if len(app.journals) == 0 {
return nil
}
// Переходим к следующему, если текущий выбранный журнал не последний
if app.selectedJournal < len(app.journals)-1 {
// Увеличиваем индекс выбранного журнала
app.selectedJournal += step
// Проверяем, чтобы не выйти за пределы списка
if app.selectedJournal >= len(app.journals) {
app.selectedJournal = len(app.journals) - 1
}
// Проверяем, вышли ли за пределы видимой области (увеличиваем стартовую позицию видимости, только если дошли до 0 + maxVisibleServices)
if app.selectedJournal >= app.startServices+app.maxVisibleServices {
// Сдвигаем видимую область вниз
app.startServices += step
// Проверяем, чтобы не выйти за пределы списка
if app.startServices > len(app.journals)-app.maxVisibleServices {
app.startServices = len(app.journals) - app.maxVisibleServices
}
// Обновляем отображение списка служб
app.updateServicesList()
}
// Если сдвинули видимую область, корректируем индекс для смещения курсора в интерфейсе
if app.selectedJournal < app.startServices+app.maxVisibleServices {
// Выбираем журнал по скорректированному индексу
return app.selectServiceByIndex(app.selectedJournal - app.startServices)
}
}
return nil
}
// Функция для перемещения по списку журналов вверх
func (app *App) prevService(v *gocui.View, step int) error {
_, viewHeight := v.Size()
app.maxVisibleServices = viewHeight
if len(app.journals) == 0 {
return nil
}
// Переходим к предыдущему, если текущий выбранный журнал не первый
if app.selectedJournal > 0 {
app.selectedJournal -= step
// Если ушли в минус (за начало журнала), приводим к нулю
if app.selectedJournal < 0 {
app.selectedJournal = 0
}
// Проверяем, вышли ли за пределы видимой области
if app.selectedJournal < app.startServices {
app.startServices -= step
if app.startServices < 0 {
app.startServices = 0
}
app.updateServicesList()
}
if app.selectedJournal >= app.startServices {
return app.selectServiceByIndex(app.selectedJournal - app.startServices)
}
}
return nil
}
// Функция для визуального выбора журнала по индексу (смещение курсора выделения)
func (app *App) selectServiceByIndex(index int) error {
// Получаем доступ к представлению списка служб
v, err := app.gui.View("services")
if err != nil {
return err
}
// Обновляем счетчик в заголовке
re := regexp.MustCompile(`\s\(.+\) >`)
updateTitle := " (0) >"
if len(app.journals) != 0 {
updateTitle = " (" + strconv.Itoa(app.selectedJournal+1) + "/" + strconv.Itoa(len(app.journals)) + ") >"
}
v.Title = re.ReplaceAllString(v.Title, updateTitle)
// Устанавливаем курсор на нужный индекс (строку)
// Первый столбец (0), индекс строки
if err := v.SetCursor(0, index); err != nil {
return nil
}
return nil
}
// Функция для выбора журнала в списке сервисов по нажатию Enter
func (app *App) selectService(g *gocui.Gui, v *gocui.View) error {
// Проверка, что есть доступ к представлению и список журналов не пустой
if v == nil || len(app.journals) == 0 {
return nil
}
// Получаем текущую позицию курсора
_, cy := v.Cursor()
// Читаем строку, на которой находится курсор
line, err := v.Line(cy)
if err != nil {
return err
}
// Загружаем журналы выбранной службы, обрезая пробелы в названии
if app.fastMode {
go func() {
app.loadJournalLogs(strings.TrimSpace(line), true)
}()
} else {
app.loadJournalLogs(strings.TrimSpace(line), true)
}
// Включаем загрузку журнала (только при ручном выборе для Windows)
app.updateFile = true
// Фиксируем для ручного или автоматического обновления вывода журнала
app.lastWindow = "services"
app.lastSelected = strings.TrimSpace(line)
return nil
}
// Функция для загрузки записей журнала выбранной службы через journalctl
// Второй параметр для обнолвения позиции делимитра нового вывода лога а также сброса автоскролл
func (app *App) loadJournalLogs(serviceName string, newUpdate bool) {
// Сбрасываем последнюю используемую систему контейнеризации (ошибка при покраске после compose)
app.lastContainerizationSystem = ""
if serviceName == "" {
return
}
app.debugStartTime = time.Now()
var output []byte
var err error
selectUnits := app.selectUnits
if newUpdate {
app.lastSelectUnits = app.selectUnits
} else {
selectUnits = app.lastSelectUnits
}
switch {
// Читаем журналы Windows
case app.getOS == "windows":
if !app.updateFile {
return
}
// Отключаем чтение в горутине
app.updateFile = false
// Извлекаем полное имя события
var eventName string
for _, journal := range app.journals {
journalBootName := removeANSI(journal.name)
if journalBootName == serviceName {
eventName = journal.boot_id
break
}
}
output = app.loadWinEventLog(eventName)
if len(output) == 0 && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
return
}
if len(output) == 0 && app.testMode {
app.currentLogLines = []string{}
return
}
// Читаем лог выбранного по ключу журнала аудита
case selectUnits == "auditd":
if newUpdate {
app.lastBootId = serviceName
} else {
serviceName = app.lastBootId
}
var cmd *exec.Cmd
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "ausearch", "-k", serviceName, "--format", "interpret")...)
} else {
cmd = exec.Command("ausearch", "-k", serviceName, "--format", "interpret")
}
output, err = cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, "\033[31mError getting auditd logs:", err, "\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error: getting auditd logs. ", err)
}
// Читаем лог ядра загрузки системы
case selectUnits == "kernel":
// Извлекаем id журнала из названия
var boot_id string
for _, journal := range app.journals {
journalBootName := removeANSI(journal.name)
if journalBootName == serviceName {
boot_id = journal.boot_id
break
}
}
// Сохраняем название для обновления вывода журнала при фильтрации списков
if newUpdate {
app.lastBootId = boot_id
} else {
boot_id = app.lastBootId
}
var cmd *exec.Cmd
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "journalctl", "-k", "-b", boot_id, "--no-pager", "-n", app.logViewCount)...)
} else {
cmd = exec.Command("journalctl", "-k", "-b", boot_id, "--no-pager", "-n", app.logViewCount)
}
output, err = cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, "\033[31mError getting kernal logs:", err, "\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error: getting kernal logs. ", err)
}
// Для юнитов systemd и других журналов по названию (--unit=UNIT)
default:
if selectUnits == "services" {
// Удаляем статусы с покраской из навзания
var ansiEscape = regexp.MustCompile(`\s\(.+\)`)
serviceName = ansiEscape.ReplaceAllString(serviceName, "")
}
var cmd *exec.Cmd
if app.sshMode {
switch {
case app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command("ssh", append(app.sshOptions, "journalctl", "-u", serviceName, "--no-pager", "--since", app.sinceFilterText, "--until", app.untilFilterText, "-n", app.logViewCount)...)
case app.sinceTimestampFilterMode && !app.untilTimestampFilterMode:
cmd = exec.Command("ssh", append(app.sshOptions, "journalctl", "-u", serviceName, "--no-pager", "--since", app.sinceFilterText, "-n", app.logViewCount)...)
case !app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command("ssh", append(app.sshOptions, "journalctl", "-u", serviceName, "--no-pager", "--until", app.untilFilterText, "-n", app.logViewCount)...)
default:
cmd = exec.Command("ssh", append(app.sshOptions, "journalctl", "-u", serviceName, "--no-pager", "-n", app.logViewCount)...)
}
} else {
switch {
case app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command("journalctl", "-u", serviceName, "--no-pager", "--since", app.sinceFilterText, "--until", app.untilFilterText, "-n", app.logViewCount)
case app.sinceTimestampFilterMode && !app.untilTimestampFilterMode:
cmd = exec.Command("journalctl", "-u", serviceName, "--no-pager", "--since", app.sinceFilterText, "-n", app.logViewCount)
case !app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command("journalctl", "-u", serviceName, "--no-pager", "--until", app.untilFilterText, "-n", app.logViewCount)
default:
cmd = exec.Command("journalctl", "-u", serviceName, "--no-pager", "-n", app.logViewCount)
}
}
output, err = cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, "\033[31mError getting journald logs:", err, "\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error: getting journald logs. ", err)
}
}
// Сохраняем строки журнала в массив
app.currentLogLines = strings.Split(string(output), "\n")
if !app.testMode {
app.updateDelimiter(newUpdate)
// Очищаем поле ввода для фильтрации, что бы не применять фильтрацию к новому журналу
// app.filterText = ""
// Применяем текущий фильтр к записям для обновления вывода
app.applyFilter(false)
}
}
// Функция для чтения и парсинга содержимого события Windows через wevtutil
func (app *App) loadWinEventLog(eventName string) (output []byte) {
app.lastContainerizationSystem = ""
if eventName == "" {
return []byte("")
}
cmd := exec.Command("powershell", "-Command",
"wevtutil qe "+eventName+" /f:text -l:en /c:"+app.logViewCount+
" /q:'*[System[TimeCreated[timediff(@SystemTime) <= 2592000000]]]'")
eventData, _ := cmd.Output()
// Декодирование вывода из Windows-1251 в UTF-8
decoder := charmap.Windows1251.NewDecoder()
decodeEventData, decodeErr := decoder.Bytes(eventData)
if decodeErr == nil {
eventData = decodeEventData
}
// Разбиваем вывод на массив
eventStrings := strings.Split(string(eventData), "Event[")
var eventMessage []string
for _, eventString := range eventStrings {
var dateTime, eventID, level, description string
// Разбиваем элемент массива на строки
lines := strings.Split(eventString, "\n")
// Флаг для обработки последней строки Description с содержимым Message
isDescription := false
for _, line := range lines {
// Удаляем проблемы во всех строках
trimmedLine := strings.TrimSpace(line)
switch {
// Обновляем формат даты
case strings.HasPrefix(trimmedLine, "Date:"):
dateTime = strings.ReplaceAll(trimmedLine, "Date: ", "")
dateTimeParse := strings.Split(dateTime, "T")
dateParse := strings.Split(dateTimeParse[0], "-")
timeParse := strings.Split(dateTimeParse[1], ".")
dateTime = fmt.Sprintf("%s.%s.%s %s", dateParse[2], dateParse[1], dateParse[0], timeParse[0])
case strings.HasPrefix(trimmedLine, "Event ID:"):
eventID = strings.ReplaceAll(trimmedLine, "Event ID: ", "")
case strings.HasPrefix(trimmedLine, "Level:"):
level = strings.ReplaceAll(trimmedLine, "Level: ", "")
case strings.HasPrefix(trimmedLine, "Description:"):
// Фиксируем и пропускаем Description
isDescription = true
case isDescription:
// Добавляем до конца текущего массива все не пустые строки
if trimmedLine != "" {
description += "\n" + trimmedLine
}
}
}
if dateTime != "" && eventID != "" && level != "" && description != "" {
eventMessage = append(eventMessage, fmt.Sprintf("%s %s (%s): %s", dateTime, level, eventID, strings.TrimSpace(description)))
}
}
fullMessage := strings.Join(eventMessage, "\n")
return []byte(fullMessage)
}
// ---------------------------------------- Filesystem ----------------------------------------
// Базовая структура os.Stat
type fileInfo struct {
name string
size int64
modTime time.Time
}
// Дочерние методы os.Stat
func (fi *fileInfo) Name() string { return fi.name }
func (fi *fileInfo) Size() int64 { return fi.size }
func (fi *fileInfo) ModTime() time.Time { return fi.modTime }
func (fi *fileInfo) Mode() os.FileMode { return 0o644 } // default rights
func (fi *fileInfo) IsDir() bool { return false } // only file
func (fi *fileInfo) Sys() any { return nil }
// Имитация метода os.Stat через exec.Command
func (app *App) statFile(path string) (os.FileInfo, error) {
if app.sshMode {
// Аргументы для команды stats. Ключи для перехода по символическим ссылкам
// для получения информации о целевых файлах (для проверки доступа) и форматирования вывода
statArgs := app.sshOptions
statArgs = append(statArgs, "stat", "-L", "-c", "'%n|%s|%Y'", path)
cmd := exec.Command("ssh", statArgs...)
output, err := cmd.Output()
if err != nil {
return nil, err
}
// Парсим вывод stat (пример вывода: /var/log/syslog|8744995|1756116219)
line := strings.TrimSpace(string(output))
parts := strings.Split(line, "|")
if len(parts) != 3 {
return nil, fmt.Errorf("%w: %s", ErrInvalidStat, line)
}
// Преобразуем размер и время в int
size, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return nil, err
}
modTimeUnix, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return nil, err
}
modTime := time.Unix(modTimeUnix, 0)
// Создаем кастомный FileInfo
return &fileInfo{
name: parts[0],
size: size,
modTime: modTime,
}, nil
} else {
// В локальном режиме возвращяем стандартный os.Stat
return os.Stat(path)
}
}
// Получение массива статистики по всем файлам
func (app *App) statFiles(paths []string) (map[string]os.FileInfo, error) {
if len(paths) == 0 {
return make(map[string]os.FileInfo), nil
}
args := make([]string, len(app.sshOptions))
copy(args, app.sshOptions)
args = append(args, "stat", "-L", "-c", "'%n|%s|%Y'")
args = append(args, paths...)
cmd := exec.Command("ssh", args...)
output, _ := cmd.Output()
results := make(map[string]os.FileInfo)
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
parts := strings.Split(line, "|")
if len(parts) != 3 {
return nil, fmt.Errorf("%w: %s", ErrInvalidStat, line)
}
size, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return nil, err
}
modTimeUnix, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return nil, err
}
modTime := time.Unix(modTimeUnix, 0)
results[parts[0]] = &fileInfo{
name: parts[0],
size: size,
modTime: modTime,
}
}
return results, nil
}
func (app *App) loadFiles(logPath string) {
app.logfiles = nil // сбрасываем (очищаем) массив перед загрузкой новых журналов
var output []byte
switch logPath {
case "descriptor":
var cmd *exec.Cmd
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "lsof", "-Fn")...)
} else {
cmd = exec.Command("lsof", "-Fn")
}
// Подавить вывод ошибок при отсутствиее прав доступа (opendir: Permission denied)
cmd.Stderr = nil
output, _ = cmd.Output()
// Разбиваем вывод на строки
files := strings.Split(strings.TrimSpace(string(output)), "\n")
// Если список файлов пустой, возвращаем ошибку Permission denied
if !app.testMode {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
vError, _ := app.gui.View("varLogs")
vError.Clear()
// Меняем цвет окна на красный
app.fileSystemFrameColor = gocui.ColorRed
vError.FrameColor = app.fileSystemFrameColor
// Отключаем курсор и выводим сообщение об ошибке
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mPermission denied (files not found)\033[0m")
return
} else {
vError, _ := app.gui.View("varLogs")
app.fileSystemFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
} else {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
log.Print("Error: permission denied (files not found from descriptor)")
}
}
// Очищаем массив перед добавлением отфильтрованных файлов
output = []byte{}
// Фильтруем строки, которые заканчиваются на ".log" и удаляем префикс (имя файла)
for _, file := range files {
if strings.HasSuffix(file, ".log") {
file = strings.TrimPrefix(file, "n")
output = append(output, []byte(file+"\n")...)
}
}
case "/var/log/":
var cmd *exec.Cmd
// Загрузка системных журналов для macOS
if app.getOS == "darwin" {
args := []string{
logPath, "/Library/Logs",
"-type", "f",
"-name", "*.asl", "-o",
"-name", "*.log", "-o",
"-name", "*log*", "-o",
"-name", "*.[0-9]*", "-o",
"-name", "*.[0-9].*", "-o",
"-name", "*.pcap", "-o",
"-name", "*.pcap.gz", "-o",
"-name", "*.pcapng", "-o",
"-name", "*.pcapng.gz",
}
if app.sshMode {
sshArgs := app.sshOptions
sshArgs = append(sshArgs, "find")
sshArgs = append(sshArgs, args...)
cmd = exec.Command("ssh", sshArgs...)
} else {
cmd = exec.Command("find", args...)
}
} else {
// Загрузка системных журналов для Linux: все файлы, которые содержат log в расширение или названии (архивы включительно), а также расширение с цифрой (архивные) и pcap/pcapng
args := []string{
logPath,
"-type", "f",
"-name", "*.log", "-o",
"-name", "*log*", "-o",
"-name", "*.[0-9]*", "-o",
"-name", "*.[0-9].*", "-o",
"-name", "*.pcap", "-o",
"-name", "*.pcap", "-o",
"-name", "*.pcap.gz", "-o",
"-name", "*.pcapng", "-o",
"-name", "*.pcapng.gz",
}
if app.sshMode {
sshArgs := app.sshOptions
sshArgs = append(sshArgs, "find")
sshArgs = append(sshArgs, args...)
cmd = exec.Command("ssh", sshArgs...)
} else {
cmd = exec.Command("find", args...)
}
}
output, _ = cmd.Output()
// Преобразуем вывод команды в строку и делим на массив строк
files := strings.Split(strings.TrimSpace(string(output)), "\n")
// Если список файлов пустой, возвращаем ошибку Permission denied
if !app.testMode {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
vError, _ := app.gui.View("varLogs")
vError.Clear()
// Меняем цвет окна на красный
app.fileSystemFrameColor = gocui.ColorRed
vError.FrameColor = app.fileSystemFrameColor
// Отключаем курсор и выводим сообщение об ошибке
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mPermission denied (files not found)\033[0m")
return
} else {
vError, _ := app.gui.View("varLogs")
app.fileSystemFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
} else {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
log.Print("Error: files not found in /var/log")
}
}
// Добавляем пути по умолчанию для /var/log
logPaths := []string{
// Ядро
"/var/log/dmesg\n",
// Информация о входах и выходах пользователей, перезагрузках и остановках системы
"/var/log/wtmp\n",
// Информация о неудачных попытках входа в систему (например, неправильные пароли)
"/var/log/btmp\n",
// Информация о текущих пользователях, их сеансах и входах в систему
"/var/run/utmp\n",
"/run/utmp\n",
// macOS/BSD/RHEL
"/var/log/secure\n",
"/var/log/messages\n",
"/var/log/daemon\n",
"/var/log/lpd-errs\n",
"/var/log/security.out\n",
"/var/log/daily.out\n",
// Службы
"/var/log/cron\n",
"/var/log/ftpd\n",
"/var/log/ntpd\n",
"/var/log/named\n",
"/var/log/dhcpd\n",
}
for _, path := range logPaths {
output = append([]byte(path), output...)
}
case "/opt/":
var cmd *exec.Cmd
if app.sshMode {
cmd = exec.Command(
"ssh", append(app.sshOptions,
"find", logPath,
"-type", "f",
"-name", "*.log", "-o",
"-name", "*.log.*",
)...,
)
} else {
cmd = exec.Command(
"find", logPath,
"-type", "f",
"-name", "*.log", "-o",
"-name", "*.log.*",
)
}
output, _ = cmd.Output()
files := strings.Split(strings.TrimSpace(string(output)), "\n")
if !app.testMode {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
vError, _ := app.gui.View("varLogs")
vError.Clear()
// Меняем цвет окна на красный
app.fileSystemFrameColor = gocui.ColorRed
vError.FrameColor = app.fileSystemFrameColor
// Отключаем курсор и выводим сообщение об ошибке
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mFiles not found\033[0m")
return
} else {
vError, _ := app.gui.View("varLogs")
app.fileSystemFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
} else {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
log.Print("Error: files not found in /opt/")
}
}
default:
// Домашние каталоги пользователей: /home/ для Linux и /Users/ для macOS
if app.getOS == "darwin" {
logPath = "/Users/"
}
// Ищем файлы с помощью системной утилиты find
var cmd *exec.Cmd
args := []string{
logPath,
"-type", "d",
"-name", "Library", "-o",
"-name", "Pictures", "-o",
"-name", "Movies", "-o",
"-name", "Music", "-o",
"-name", ".Trash", "-o",
"-name", ".cache",
"-prune", "-o",
"-type", "f",
"-name", "*.log", "-o",
"-name", "*.asl", "-o",
"-name", "*.pcap", "-o",
"-name", "*.pcap.gz", "-o",
"-name", "*.pcapng", "-o",
"-name", "*.pcapng.gz",
}
if app.sshMode {
sshArgs := app.sshOptions
sshArgs = append(sshArgs, "find")
sshArgs = append(sshArgs, args...)
cmd = exec.Command("ssh", sshArgs...)
} else {
cmd = exec.Command("find", args...)
}
output, _ = cmd.Output()
files := strings.Split(strings.TrimSpace(string(output)), "\n")
if !app.testMode {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
vError, _ := app.gui.View("varLogs")
vError.Clear()
vError.Highlight = false
fmt.Fprintln(vError, "\033[32mFiles not found\033[0m")
return
} else {
vError, _ := app.gui.View("varLogs")
app.fileSystemFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
} else {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
log.Print("Error: files not found in home directories")
}
}
// Получаем содержимое файлов из домашнего каталога пользователя root
var cmdRootDir *exec.Cmd
args = []string{
"/root/",
"-type", "f",
"-name", "*.log", "-o",
"-name", "*.pcap", "-o",
"-name", "*.pcap.gz", "-o",
"-name", "*.pcapng", "-o",
"-name", "*.pcapng.gz",
}
if app.sshMode {
sshArgs := app.sshOptions
sshArgs = append(sshArgs, "find")
sshArgs = append(sshArgs, args...)
cmdRootDir = exec.Command("ssh", sshArgs...)
} else {
cmdRootDir = exec.Command("find", args...)
}
outputRootDir, err := cmdRootDir.Output()
// Добавляем содержимое директории /root/ в общий массив, если есть доступ
if err == nil {
output = append(output, outputRootDir...)
}
if app.fileSystemFrameColor == gocui.ColorRed && !app.testMode {
vError, _ := app.gui.View("varLogs")
app.fileSystemFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
}
// Формируем массив путей
logFullPaths := strings.Split(strings.TrimSpace(string(output)), "\n")
// Получаем статистику по всем файлам одним вызовом в режиме ssh
var statFiles map[string]os.FileInfo
if app.sshMode {
statFiles, _ = app.statFiles(logFullPaths)
}
// Карта уникальных путей
serviceMap := make(map[string]bool)
// Основной цикл
for _, logFullPath := range logFullPaths {
// Удаляем префикс пути и расширение файла в конце
logName := logFullPath
if logPath != "descriptor" {
logName = strings.TrimPrefix(logFullPath, logPath)
}
logName = strings.TrimSuffix(logName, ".log")
logName = strings.TrimSuffix(logName, ".asl")
logName = strings.TrimSuffix(logName, ".gz")
logName = strings.TrimSuffix(logName, ".xz")
logName = strings.TrimSuffix(logName, ".bz2")
logName = strings.ReplaceAll(logName, "/", " ")
logName = strings.ReplaceAll(logName, ".log.", ".")
logName = strings.TrimPrefix(logName, " ")
if logPath == "/home/" || logPath == "/Users/" {
// Разбиваем строку на слова
words := strings.Fields(logName)
// Берем первое и последнее слово
firstWord := words[0]
lastWord := words[len(words)-1]
logName = "\x1b[0;33m" + firstWord + "\033[0m" + ": " + lastWord
}
// Получаем информацию о файле
var fileInfo os.FileInfo
var exists bool
var err error
if app.sshMode {
// Извлекаем статистику из массива
fileInfo, exists = statFiles[logFullPath]
// Пропускаем файл, если он не найден в результатах
if !exists {
continue
}
} else {
// Запрашиваем статистику для каждого файла в локальном режиме
fileInfo, err = os.Stat(logFullPath)
if err != nil {
// Пропускаем файл, если к нему нет доступа (актуально для статических файлов из переменной logPath)
continue
}
}
// Проверяем, что файл не пустой
if fileInfo.Size() == 0 {
// Пропускаем пустой файл
continue
}
// Получаем дату изменения
modTime := fileInfo.ModTime()
// Форматирование даты в формат DD.MM.YYYY HH:MM
formattedDate := modTime.Format("02.01.2006 15:04")
// Проверяем, что полного пути до файла еще нет в списке
if logName != "" && !serviceMap[logFullPath] {
// Добавляем путь в массив для проверки уникальных путей
serviceMap[logFullPath] = true
// Получаем имя процесса для файла дескриптора
if logPath == "descriptor" {
var cmd *exec.Cmd
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "lsof", "-Fc", logFullPath)...)
} else {
cmd = exec.Command("lsof", "-Fc", logFullPath)
}
cmd.Stderr = nil
outputLsof, _ := cmd.Output()
processLines := strings.Split(strings.TrimSpace(string(outputLsof)), "\n")
// Ищем строку, которая содержит имя процесса (только первый процесс)
for _, line := range processLines {
if strings.HasPrefix(line, "c") {
// Удаляем префикс
processName := line[1:]
logName = "\x1b[0;33m" + processName + "\033[0m" + ": " + logName
break
}
}
}
// Выделение цветом подов и контейнеров k3s из файловой системы
if strings.HasPrefix(logName, "pods") {
logName = strings.Replace(logName, "pods", "\033[33mpod\033[0m", 1)
}
if strings.HasPrefix(logName, "containers") {
logName = strings.Replace(logName, "containers", "\033[32mcontainer\033[0m", 1)
}
// Добавляем в список
app.logfiles = append(app.logfiles, Logfile{
name: "[" + "\033[34m" + formattedDate + "\033[0m" + "] " + logName,
path: logFullPath,
})
}
}
// Сортируем по дате
sort.Slice(app.logfiles, func(i, j int) bool {
// Извлечение дат из имени
layout := "02.01.2006 15:04"
dateI, _ := time.Parse(layout, extractDate(app.logfiles[i].name))
dateJ, _ := time.Parse(layout, extractDate(app.logfiles[j].name))
// return dateI.Before(dateJ)
// Сортировка в обратном порядке
return dateI.After(dateJ)
})
if !app.testMode {
app.logfilesNotFilter = app.logfiles
app.applyFilterList()
}
}
func (app *App) loadWinFiles(logPath string) {
app.logfiles = nil
// Определяем путь по параметру
switch logPath {
case "ProgramFiles":
logPath = app.systemDisk + ":\\Program Files"
case "ProgramFiles86":
logPath = app.systemDisk + ":\\Program Files (x86)"
case "ProgramData":
logPath = app.systemDisk + ":\\ProgramData"
case "AppDataLocal":
logPath = app.systemDisk + ":\\Users\\" + app.userName + "\\AppData\\Local"
case "AppDataRoaming":
logPath = app.systemDisk + ":\\Users\\" + app.userName + "\\AppData\\Roaming"
}
// Ищем файлы с помощью WalkDir
var files []string
// Доступ к срезу files из нескольких горутин
var mu sync.Mutex
// Группа ожидания для отслеживания завершения всех горутин
var wg sync.WaitGroup
// Получаем список корневых директорий
rootDirs, _ := os.ReadDir(logPath)
for _, rootDir := range rootDirs {
// Проверяем, является ли текущий элемент директорие
if rootDir.IsDir() {
// Увеличиваем счетчик ожидаемых горутин
wg.Add(1)
go func(dir string) {
// Уменьшаем счетчик горутин после завершения текущей
defer wg.Done()
// Рекурсивно обходим все файлы и подкаталоги в текущей директории
err := filepath.WalkDir(filepath.Join(logPath, dir), func(path string, d os.DirEntry, err error) error {
if err != nil {
// Игнорируем ошибки, чтобы не прерывать поиск
return nil
}
// Проверяем, что текущий элемент не является директорией и имеет расширение .log
if !d.IsDir() && strings.HasSuffix(strings.ToLower(d.Name()), ".log") {
// Получаем относительный путь (без корневого пути logPath)
relPath, _ := filepath.Rel(logPath, path)
// Используем мьютекс для добавления файла в срез
mu.Lock()
files = append(files, relPath)
mu.Unlock()
}
return nil
})
if err != nil {
return
}
}(
// Передаем имя текущей директории в горутину
rootDir.Name(),
)
}
}
// Ждем завершения всех запущенных горутин
wg.Wait()
// Объединяем все пути в одну строку, разделенную символом новой строки
output := strings.Join(files, "\n")
if !app.testMode {
// Если список файлов пустой, возвращаем ошибку
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
vError, _ := app.gui.View("varLogs")
vError.Clear()
app.fileSystemFrameColor = gocui.ColorRed
vError.FrameColor = app.fileSystemFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mPermission denied (files not found)\033[0m")
return
} else {
vError, _ := app.gui.View("varLogs")
app.fileSystemFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
} else {
if len(files) == 0 || (len(files) == 1 && files[0] == "") {
log.Print("Error: files not found in ", logPath)
}
}
serviceMap := make(map[string]bool)
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
// Формируем полный путь к файлу
logFullPath := logPath + "\\" + scanner.Text()
// Формируем имя файла для списка
logName := scanner.Text()
logName = strings.TrimSuffix(logName, ".log")
logName = strings.ReplaceAll(logName, "\\", " ")
// Получаем информацию о файле
fileInfo, err := os.Stat(logFullPath)
// Пропускаем файлы, к которым нет доступа
if err != nil {
continue
}
// Пропускаем пустые файлы
if fileInfo.Size() == 0 {
continue
}
// Получаем дату изменения
modTime := fileInfo.ModTime()
// Форматирование даты в формат DD.MM.YYYY HH:MM
formattedDate := modTime.Format("02.01.2006 15:04")
// Проверяем, что полного пути до файла еще нет в списке
if logName != "" && !serviceMap[logFullPath] {
// Добавляем путь в массив для проверки уникальных путей
serviceMap[logFullPath] = true
// Добавляем в список
app.logfiles = append(app.logfiles, Logfile{
name: "[" + "\033[34m" + formattedDate + "\033[0m" + "] " + logName,
path: logFullPath,
})
}
}
// Сортируем по дате
sort.Slice(app.logfiles, func(i, j int) bool {
layout := "02.01.2006 15:04"
dateI, _ := time.Parse(layout, extractDate(app.logfiles[i].name))
dateJ, _ := time.Parse(layout, extractDate(app.logfiles[j].name))
return dateI.After(dateJ)
})
if !app.testMode {
app.logfilesNotFilter = app.logfiles
app.applyFilterList()
}
}
// Функция для извлечения первой втречающейся даты в формате DD.MM.YYYY HH:MM
func extractDate(name string) string {
re := regexp.MustCompile(`\d{2}\.\d{2}\.\d{4}\s\d{2}:\d{2}`)
return re.FindString(name)
}
func (app *App) updateLogsList() {
v, err := app.gui.View("varLogs")
if err != nil {
return
}
v.Clear()
visibleEnd := app.startFiles + app.maxVisibleFiles
if visibleEnd > len(app.logfiles) {
visibleEnd = len(app.logfiles)
}
for i := app.startFiles; i < visibleEnd; i++ {
fmt.Fprintln(v, app.logfiles[i].name)
}
}
func (app *App) nextFileName(v *gocui.View, step int) error {
_, viewHeight := v.Size()
app.maxVisibleFiles = viewHeight
if len(app.logfiles) == 0 {
return nil
}
if app.selectedFile < len(app.logfiles)-1 {
app.selectedFile += step
if app.selectedFile >= len(app.logfiles) {
app.selectedFile = len(app.logfiles) - 1
}
if app.selectedFile >= app.startFiles+app.maxVisibleFiles {
app.startFiles += step
if app.startFiles > len(app.logfiles)-app.maxVisibleFiles {
app.startFiles = len(app.logfiles) - app.maxVisibleFiles
}
app.updateLogsList()
}
if app.selectedFile < app.startFiles+app.maxVisibleFiles {
return app.selectFileByIndex(app.selectedFile - app.startFiles)
}
}
return nil
}
func (app *App) prevFileName(v *gocui.View, step int) error {
_, viewHeight := v.Size()
app.maxVisibleFiles = viewHeight
if len(app.logfiles) == 0 {
return nil
}
if app.selectedFile > 0 {
app.selectedFile -= step
if app.selectedFile < 0 {
app.selectedFile = 0
}
if app.selectedFile < app.startFiles {
app.startFiles -= step
if app.startFiles < 0 {
app.startFiles = 0
}
app.updateLogsList()
}
if app.selectedFile >= app.startFiles {
return app.selectFileByIndex(app.selectedFile - app.startFiles)
}
}
return nil
}
func (app *App) selectFileByIndex(index int) error {
v, err := app.gui.View("varLogs")
if err != nil {
return err
}
// Обновляем счетчик в заголовке
re := regexp.MustCompile(`\s\(.+\) >`)
updateTitle := " (0) >"
if len(app.logfiles) != 0 {
updateTitle = " (" + strconv.Itoa(app.selectedFile+1) + "/" + strconv.Itoa(len(app.logfiles)) + ") >"
}
v.Title = re.ReplaceAllString(v.Title, updateTitle)
if err := v.SetCursor(0, index); err != nil {
return nil
}
return nil
}
func (app *App) selectFile(g *gocui.Gui, v *gocui.View) error {
if v == nil || len(app.logfiles) == 0 {
return nil
}
_, cy := v.Cursor()
line, err := v.Line(cy)
if err != nil {
return err
}
if app.fastMode {
go func() {
app.loadFileLogs(strings.TrimSpace(line), true)
}()
} else {
app.loadFileLogs(strings.TrimSpace(line), true)
}
app.lastWindow = "varLogs"
app.lastSelected = strings.TrimSpace(line)
return nil
}
// Функция для чтения файла
func (app *App) loadFileLogs(logName string, newUpdate bool) {
app.lastContainerizationSystem = ""
if logName == "" {
return
}
app.debugStartTime = time.Now()
// В параметре logName имя файла при выборе возвращяется без символов покраски
// Получаем путь из массива по имени
var logFullPath string
var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`)
for _, logfile := range app.logfiles {
// Удаляем покраску из имени файла в сохраненном массиве
logFileName := ansiEscape.ReplaceAllString(logfile.name, "")
// Ищем переданное в функцию имя файла и извлекаем путь
if logFileName == logName {
logFullPath = logfile.path
break
}
}
if newUpdate {
app.lastLogPath = logFullPath
// Фиксируем новую дату изменения и размер для выбранного файла
fileInfo, err := app.statFile(logFullPath)
if err != nil {
return
}
fileModTime := fileInfo.ModTime()
fileSize := fileInfo.Size()
app.lastDateUpdateFile = fileModTime
app.lastSizeFile = fileSize
app.updateFile = true
} else {
logFullPath = app.lastLogPath
// Проверяем дату изменения
fileInfo, err := app.statFile(logFullPath)
if err != nil {
return
}
fileModTime := fileInfo.ModTime()
fileSize := fileInfo.Size()
// Обновлять файл в горутине, только если есть изменения (проверяем дату модификации и размер)
if fileModTime != app.lastDateUpdateFile || fileSize != app.lastSizeFile {
app.lastDateUpdateFile = fileModTime
app.lastSizeFile = fileSize
app.updateFile = true
} else {
app.updateFile = false
}
}
// Читаем файл, толькое если были изменения
if app.updateFile {
// Читаем логи в системе Windows
if app.getOS == "windows" {
decodedOutput, stringErrors := app.loadWinFileLog(logFullPath)
if stringErrors != "nil" && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, "\033[31mError", stringErrors, "\033[0m")
return
}
if stringErrors != "nil" && app.testMode {
log.Print("Error: ", stringErrors)
}
app.currentLogLines = strings.Split(string(decodedOutput), "\n")
} else {
var cmd *exec.Cmd
// Читаем логи в системах UNIX (Linux/Darwin/*BSD)
switch {
// Читаем файлы в формате ASL (Apple System Log)
case strings.HasSuffix(logFullPath, "asl"):
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "syslog", "-f", logFullPath)...)
} else {
cmd = exec.Command("syslog", "-f", logFullPath)
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using syslog tool in ASL (Apple System Log) format.\n", err, "\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error: reading log using syslog tool in ASL (Apple System Log) format. ", err)
}
app.currentLogLines = strings.Split(string(output), "\n")
// Читаем журналы Packet Capture в формате pcap/pcapng
case strings.HasSuffix(logFullPath, "pcap") || strings.HasSuffix(logFullPath, "pcapng"):
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "tcpdump", "-n", "-r", logFullPath)...)
} else {
cmd = exec.Command("tcpdump", "-n", "-r", logFullPath)
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using tcpdump tool.\n", err, "\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error: reading log using tcpdump tool. ", err)
}
app.currentLogLines = strings.Split(string(output), "\n")
// Packet Filter (PF) Firewall OpenBSD
case strings.HasSuffix(logFullPath, "pflog"):
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "tcpdump", "-e", "-n", "-r", logFullPath)...)
} else {
cmd = exec.Command("tcpdump", "-e", "-n", "-r", logFullPath)
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using tcpdump tool.\n", err, "\033[0m")
return
}
app.currentLogLines = strings.Split(string(output), "\n")
// Читаем архивные логи в формате pcap/pcapng (macOS)
case strings.HasSuffix(logFullPath, "pcap.gz") || strings.HasSuffix(logFullPath, "pcapng.gz"):
var unpacker string = "gzip"
// Создаем временный файл
tmpFile, err := os.CreateTemp("", "temp-*.pcap")
if err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError create temp file.\n", err, "\033[0m")
return
}
// Удаляем временный файл после обработки
defer os.Remove(tmpFile.Name())
var cmdUnzip *exec.Cmd
if app.sshMode {
cmdUnzip = exec.Command("ssh", append(app.sshOptions, unpacker, "-dc", logFullPath)...)
} else {
cmdUnzip = exec.Command(unpacker, "-dc", logFullPath)
}
cmdUnzip.Stdout = tmpFile
if err := cmdUnzip.Start(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError starting", unpacker, "tool.\n", err, "\033[0m")
return
}
if err := cmdUnzip.Wait(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError decompressing file with", unpacker, "tool.\n", err, "\033[0m")
return
}
// Закрываем временный файл, чтобы tcpdump мог его открыть
if err := tmpFile.Close(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError closing temp file.\n", err, "\033[0m")
return
}
// Создаем команду для tcpdump
var cmdTcpdump *exec.Cmd
if app.sshMode {
cmdTcpdump = exec.Command("ssh", append(app.sshOptions, "tcpdump", "-n", "-r", tmpFile.Name())...)
} else {
cmdTcpdump = exec.Command("tcpdump", "-n", "-r", tmpFile.Name())
}
tcpdumpOut, err := cmdTcpdump.StdoutPipe()
if err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError creating stdout pipe for tcpdump.\n", err, "\033[0m")
return
}
// Запускаем tcpdump
if err := cmdTcpdump.Start(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError starting tcpdump.\n", err, "\033[0m")
return
}
// Читаем вывод tcpdump построчно
scanner := bufio.NewScanner(tcpdumpOut)
var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError reading output from tcpdump.\n", err, "\033[0m")
return
}
// Ожидаем завершения tcpdump
if err := cmdTcpdump.Wait(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError finishing tcpdump.\n", err, "\033[0m")
return
}
app.currentLogLines = lines
// Читаем архивные логи (unpack + stdout) в формате: gz/xz/bz2
case strings.HasSuffix(logFullPath, ".gz") || strings.HasSuffix(logFullPath, ".xz") || strings.HasSuffix(logFullPath, ".bz2"):
var unpacker string
switch {
case strings.HasSuffix(logFullPath, ".gz"):
unpacker = "gzip"
case strings.HasSuffix(logFullPath, ".xz"):
unpacker = "xz"
case strings.HasSuffix(logFullPath, ".bz2"):
unpacker = "bzip2"
}
var cmdUnzip *exec.Cmd
var cmdTail *exec.Cmd
if app.sshMode {
cmdUnzip = exec.Command("ssh", append(app.sshOptions, unpacker, "-dc", logFullPath)...)
cmdTail = exec.Command("ssh", append(app.sshOptions, "tail", "-n", app.logViewCount)...)
} else {
cmdUnzip = exec.Command(unpacker, "-dc", logFullPath)
cmdTail = exec.Command("tail", "-n", app.logViewCount)
}
pipe, err := cmdUnzip.StdoutPipe()
if err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError creating pipe for", unpacker, "tool.\n", err, "\033[0m")
return
}
// Стандартный вывод программы передаем в stdin tail
cmdTail.Stdin = pipe
out, err := cmdTail.StdoutPipe()
if err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError creating stdout pipe for tail.\n", err, "\033[0m")
return
}
// Запуск команд
if err := cmdUnzip.Start(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError starting", unpacker, "tool.\n", err, "\033[0m")
return
}
if err := cmdTail.Start(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError starting tail from", unpacker, "stdout.\n", err, "\033[0m")
return
}
// Чтение вывода
output, err := io.ReadAll(out)
if err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError reading output from tail.\n", err, "\033[0m")
return
}
// Ожидание завершения команд
if err := cmdUnzip.Wait(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError reading archive log using", unpacker, "tool.\n", err, "\033[0m")
return
}
if err := cmdTail.Wait(); err != nil && !app.testMode {
vError, _ := app.gui.View("logs")
vError.Clear()
fmt.Fprintln(vError, " \033[31mError reading log using tail tool.\n", err, "\033[0m")
return
}
// Выводим содержимое
app.currentLogLines = strings.Split(string(output), "\n")
// Читаем бинарные файлы с помощью last для wtmp, а также utmp (OpenBSD) и utx.log (FreeBSD)
case strings.Contains(logFullPath, "wtmp") || strings.Contains(logFullPath, "utmp") || strings.Contains(logFullPath, "utx.log"):
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "last", "-f", logFullPath)...)
} else {
cmd = exec.Command("last", "-f", logFullPath)
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using last tool.\n", err, "\033[0m")
return
}
// Разбиваем вывод на строки
lines := strings.Split(string(output), "\n")
var filteredLines []string
// Фильтруем строки, исключая последнюю строку и пустые строки
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if trimmedLine != "" && !strings.Contains(trimmedLine, "begins") {
filteredLines = append(filteredLines, trimmedLine)
}
}
// Переворачиваем порядок строк
for i, j := 0, len(filteredLines)-1; i < j; i, j = i+1, j-1 {
filteredLines[i], filteredLines[j] = filteredLines[j], filteredLines[i]
}
app.currentLogLines = filteredLines
// lastb for btmp
case strings.Contains(logFullPath, "btmp"):
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "lastb", "-f", logFullPath)...)
} else {
cmd = exec.Command("lastb", "-f", logFullPath)
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using lastb tool.\n", err, "\033[0m")
return
}
lines := strings.Split(string(output), "\n")
var filteredLines []string
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if trimmedLine != "" && !strings.Contains(trimmedLine, "begins") {
filteredLines = append(filteredLines, trimmedLine)
}
}
for i, j := 0, len(filteredLines)-1; i < j; i, j = i+1, j-1 {
filteredLines[i], filteredLines[j] = filteredLines[j], filteredLines[i]
}
app.currentLogLines = filteredLines
// Выводим содержимое из команды lastlog
case strings.HasSuffix(logFullPath, "lastlog"):
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "lastlog")...)
} else {
cmd = exec.Command("lastlog")
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using lastlog tool.\n", err, "\033[0m")
return
}
app.currentLogLines = strings.Split(string(output), "\n")
// lastlogin for FreeBSD
case strings.HasSuffix(logFullPath, "lastlogin"):
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "lastlogin")...)
} else {
cmd = exec.Command("lastlogin")
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using lastlogin tool.\n", err, "\033[0m")
return
}
app.currentLogLines = strings.Split(string(output), "\n")
default:
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "tail", "-n", app.logViewCount, logFullPath)...)
} else {
cmd = exec.Command("tail", "-n", app.logViewCount, logFullPath)
}
output, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, " \033[31mError reading log using tail tool.\n", err, "\033[0m")
return
}
app.currentLogLines = strings.Split(string(output), "\n")
}
}
if !app.testMode {
app.updateDelimiter(newUpdate)
app.applyFilter(false)
}
}
}
// Функция для чтения файла с опредилением кодировки в Windows
func (app *App) loadWinFileLog(filePath string) (output []byte, stringErrors string) {
app.lastContainerizationSystem = ""
if filePath == "" {
return nil, "file not selected"
}
app.debugStartTime = time.Now()
// Открываем файл
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Sprintf("open file: %v", err)
}
defer file.Close()
// Получаем информацию о файле
stat, err := file.Stat()
if err != nil {
return nil, fmt.Sprintf("get file stat: %v", err)
}
// Получаем размер файла
fileSize := stat.Size()
// Буфер для хранения последних строк
var buffer []byte
lineCount := 0
// Размер буфера чтения (читаем по 1КБ за раз)
readSize := int64(1024)
// Преобразуем строку с максимальным количеством строк в int
logViewCountInt, _ := strconv.Atoi(app.logViewCount)
// Читаем файл с конца
for fileSize > 0 && lineCount < logViewCountInt {
if fileSize < readSize {
readSize = fileSize
}
_, err := file.Seek(fileSize-readSize, 0)
if err != nil {
return nil, fmt.Sprintf("detect the end of a file via seek: %v", err)
}
tempBuffer := make([]byte, readSize)
_, err = file.Read(tempBuffer)
if err != nil {
return nil, fmt.Sprintf("read file: %v", err)
}
buffer = append(tempBuffer, buffer...)
lineCount = strings.Count(string(buffer), "\n")
fileSize -= int64(readSize)
}
// Проверка на UTF-16 с BOM
utf16withBOM := func(data []byte) bool {
return len(data) >= 2 && ((data[0] == 0xFF && data[1] == 0xFE) || (data[0] == 0xFE && data[1] == 0xFF))
}
// Проверка на UTF-16 LE без BOM
utf16withoutBOM := func(data []byte) bool {
if len(data)%2 != 0 {
return false
}
for i := 1; i < len(data); i += 2 {
if data[i] != 0x00 {
return false
}
}
return true
}
var decodedOutput []byte
switch {
case utf16withBOM(buffer):
// Декодируем UTF-16 с BOM
decodedOutput, err = unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder().Bytes(buffer)
if err != nil {
return nil, fmt.Sprintf("decoding from UTF-16 with BOM: %v", err)
}
case utf16withoutBOM(buffer):
// Декодируем UTF-16 LE без BOM
decodedOutput, err = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder().Bytes(buffer)
if err != nil {
return nil, fmt.Sprintf("decoding from UTF-16 LE without BOM: %v", err)
}
case utf8.Valid(buffer):
// Декодируем UTF-8
decodedOutput = buffer
default:
// Декодируем Windows-1251
decodedOutput, err = charmap.Windows1251.NewDecoder().Bytes(buffer)
if err != nil {
return nil, fmt.Sprintf("decoding from Windows-1251: %v", err)
}
}
return decodedOutput, "nil"
}
// ---------------------------------------- Docker/Compose/Podman/k8s ----------------------------------------
func (app *App) loadDockerContainer(containerizationSystem string) {
app.dockerContainers = nil
// Получаем версию для проверки, что система контейнеризации установлена
var cmd *exec.Cmd
if app.sshMode {
// Для compose передаем два аргумента команды (проверяем compose как плагин docker)
if containerizationSystem == "compose" {
cmd = exec.Command("ssh", append(app.sshOptions,
"docker", "compose", "version",
)...)
} else {
cmd = exec.Command("ssh", append(app.sshOptions,
containerizationSystem, "version",
)...)
}
} else {
if containerizationSystem == "compose" {
cmd = exec.Command(
"docker", "compose", "version",
)
} else {
cmd = exec.Command(
containerizationSystem, "version",
)
}
}
_, err := cmd.Output()
if err != nil && !app.testMode {
vError, _ := app.gui.View("docker")
vError.Clear()
app.dockerFrameColor = gocui.ColorRed
vError.FrameColor = app.dockerFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31m"+containerizationSystem+" not installed (environment not found)\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error:", containerizationSystem+" not installed (environment not found)")
}
switch containerizationSystem {
case "kubectl":
// Получаем список подов из k8s
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions,
containerizationSystem, "get", "pods", "-A",
"-o", "'jsonpath={range .items[*]}{.metadata.uid} {.metadata.name} {.status.phase} {.metadata.namespace}{\"\\n\"}{end}'",
)...)
} else {
cmd = exec.Command(
containerizationSystem, "get", "pods", "-A", // -A/--all-namespaces
"-o", "jsonpath={range .items[*]}{.metadata.uid} {.metadata.name} {.status.phase} {.metadata.namespace}{\"\\n\"}{end}",
)
}
case "compose":
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions,
"docker", "compose", "ls", "-a",
)...)
} else {
cmd = exec.Command(
"docker", "compose", "ls", "-a",
)
}
default:
// Получаем список контейнеров из Docker или Podman
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions,
containerizationSystem, "ps", "-a",
"--format", "'{{.ID}} {{.Names}} {{.State}}'", // добавляем кавычки для передаваемых через пробел параметров в ssh
)...)
} else {
cmd = exec.Command(
containerizationSystem, "ps", "-a",
"--format", "{{.ID}} {{.Names}} {{.State}}",
)
}
}
output, err := cmd.Output()
if !app.testMode {
if err != nil {
vError, _ := app.gui.View("docker")
vError.Clear()
app.dockerFrameColor = gocui.ColorRed
vError.FrameColor = app.dockerFrameColor
vError.Highlight = false
fmt.Fprintln(vError, "\033[31mAccess denied or "+containerizationSystem+" not running\033[0m")
return
} else {
vError, _ := app.gui.View("docker")
app.dockerFrameColor = gocui.ColorDefault
vError.Highlight = true
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
}
}
if err != nil && app.testMode {
log.Print("Error: access denied or " + containerizationSystem + " not running")
}
var containers []string
var stringOutput string
// Парсим вывод compose
if containerizationSystem == "compose" {
stacks := strings.Split(strings.TrimSpace(string(output)), "\n")
// Удаляем первую строку (элемент массива)
stacks = stacks[1:]
if len(stacks) != 0 {
// Удаляем путь к конфигурационному файлу compose для каждой строки (элемента)
for i, e := range stacks {
line := strings.Split(e, "/")
stacks[i] = line[0]
}
}
containers = stacks
stringOutput = strings.Join(containers, "\n")
} else {
containers = strings.Split(strings.TrimSpace(string(output)), "\n")
stringOutput = string(output)
}
// Проверяем, что список контейнеров не пустой
if !app.testMode {
if len(containers) == 0 || (len(containers) == 1 && containers[0] == "") {
vError, _ := app.gui.View("docker")
vError.Clear()
vError.Highlight = false
fmt.Fprintln(vError, "\033[32mNo running containers\033[0m")
return
} else {
vError, _ := app.gui.View("docker")
app.fileSystemFrameColor = gocui.ColorDefault
if vError.FrameColor != gocui.ColorDefault {
vError.FrameColor = gocui.ColorGreen
}
vError.Highlight = true
}
}
// Заполняем структуру dockerContainers (название и статус)
serviceMap := make(map[string]bool)
scanner := bufio.NewScanner(strings.NewReader(stringOutput))
for scanner.Scan() {
idName := scanner.Text()
parts := strings.Fields(idName)
if idName != "" && !serviceMap[idName] {
serviceMap[idName] = true
var containerName string
var containerStatus string
if containerizationSystem == "compose" {
// Извлекаем имя стеке из первого параметра (в compose отсутствует id)
containerName = parts[0]
// Собираем все статусы в одну строку
containerStatus = strings.Join(parts[1:], "\n")
} else {
containerName = parts[1]
containerStatus = parts[2]
}
// Проверяем статус для покраски
switch {
case strings.HasPrefix(strings.ToLower(containerStatus), "running") ||
strings.HasPrefix(strings.ToLower(containerStatus), "succe"):
containerStatus = "\033[32m" + containerStatus + "\033[0m"
case strings.HasPrefix(strings.ToLower(containerStatus), "pending") ||
strings.HasPrefix(strings.ToLower(containerStatus), "pause") ||
strings.HasPrefix(strings.ToLower(containerStatus), "restart"):
containerStatus = "\033[33m" + containerStatus + "\033[0m"
default:
containerStatus = "\033[31m" + containerStatus + "\033[0m"
}
containerName = "[" + containerStatus + "] " + containerName
// Фиксируем название namespace для k8s
var namespace string
if containerizationSystem != "kubectl" || parts[3] == "" {
namespace = ""
} else {
namespace = parts[3]
}
app.dockerContainers = append(app.dockerContainers, DockerContainers{
name: containerName,
id: parts[0],
namespace: namespace,
})
}
}
sort.Slice(app.dockerContainers, func(i, j int) bool {
return app.dockerContainers[i].name < app.dockerContainers[j].name
})
if !app.testMode {
app.dockerContainersNotFilter = app.dockerContainers
app.applyFilterList()
}
// Заполняем карту уникальных цветов для контейнеров (используется для покраски префиксов в compose)
if containerizationSystem == "docker" {
for _, dc := range app.dockerContainers {
cn := strings.SplitN(dc.name, "] ", 2)[1]
if cn != "" {
newColor := uniquePrefixColorArr[len(app.uniquePrefixColorMap)%len(uniquePrefixColorArr)]
app.uniquePrefixColorMap[cn] = newColor
}
}
}
}
func (app *App) updateDockerContainerList() {
v, err := app.gui.View("docker")
if err != nil {
return
}
v.Clear()
visibleEnd := app.startDockerContainers + app.maxVisibleDockerContainers
if visibleEnd > len(app.dockerContainers) {
visibleEnd = len(app.dockerContainers)
}
for i := app.startDockerContainers; i < visibleEnd; i++ {
fmt.Fprintln(v, app.dockerContainers[i].name)
}
}
func (app *App) nextDockerContainer(v *gocui.View, step int) error {
_, viewHeight := v.Size()
app.maxVisibleDockerContainers = viewHeight
if len(app.dockerContainers) == 0 {
return nil
}
if app.selectedDockerContainer < len(app.dockerContainers)-1 {
app.selectedDockerContainer += step
if app.selectedDockerContainer >= len(app.dockerContainers) {
app.selectedDockerContainer = len(app.dockerContainers) - 1
}
if app.selectedDockerContainer >= app.startDockerContainers+app.maxVisibleDockerContainers {
app.startDockerContainers += step
if app.startDockerContainers > len(app.dockerContainers)-app.maxVisibleDockerContainers {
app.startDockerContainers = len(app.dockerContainers) - app.maxVisibleDockerContainers
}
app.updateDockerContainerList()
}
if app.selectedDockerContainer < app.startDockerContainers+app.maxVisibleDockerContainers {
return app.selectDockerByIndex(app.selectedDockerContainer - app.startDockerContainers)
}
}
return nil
}
func (app *App) prevDockerContainer(v *gocui.View, step int) error {
_, viewHeight := v.Size()
app.maxVisibleDockerContainers = viewHeight
if len(app.dockerContainers) == 0 {
return nil
}
if app.selectedDockerContainer > 0 {
app.selectedDockerContainer -= step
if app.selectedDockerContainer < 0 {
app.selectedDockerContainer = 0
}
if app.selectedDockerContainer < app.startDockerContainers {
app.startDockerContainers -= step
if app.startDockerContainers < 0 {
app.startDockerContainers = 0
}
app.updateDockerContainerList()
}
if app.selectedDockerContainer >= app.startDockerContainers {
return app.selectDockerByIndex(app.selectedDockerContainer - app.startDockerContainers)
}
}
return nil
}
func (app *App) selectDockerByIndex(index int) error {
v, err := app.gui.View("docker")
if err != nil {
return err
}
// Обновляем счетчик в заголовке
re := regexp.MustCompile(`\s\(.+\) >`)
updateTitle := " (0) >"
if len(app.dockerContainers) != 0 {
updateTitle = " (" + strconv.Itoa(app.selectedDockerContainer+1) + "/" + strconv.Itoa(len(app.dockerContainers)) + ") >"
}
v.Title = re.ReplaceAllString(v.Title, updateTitle)
if err := v.SetCursor(0, index); err != nil {
return nil
}
return nil
}
func (app *App) selectDocker(g *gocui.Gui, v *gocui.View) error {
if v == nil || len(app.dockerContainers) == 0 {
return nil
}
_, cy := v.Cursor()
line, err := v.Line(cy)
if err != nil {
return err
}
if app.fastMode {
go func() {
app.loadDockerLogs(strings.TrimSpace(line), true)
}()
} else {
app.loadDockerLogs(strings.TrimSpace(line), true)
}
app.lastWindow = "docker"
app.lastSelected = strings.TrimSpace(line)
return nil
}
func (app *App) loadDockerLogs(containerName string, newUpdate bool) {
// Прерываем выполнение функции, если имя контейнера пустое (при выборе пустого поля с помощью мыши)
if containerName == "" {
return
}
app.debugStartTime = time.Now()
containerizationSystem := app.selectContainerizationSystem
// Сохраняем систему контейнеризации для автообновления при смене окна
if newUpdate {
app.lastContainerizationSystem = app.selectContainerizationSystem
} else {
containerizationSystem = app.lastContainerizationSystem
}
var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`)
// Извлекаем id контейнера и namespace для подов k8s
var containerId string
var namespace string
for _, dockerContainer := range app.dockerContainers {
dockerContainerName := ansiEscape.ReplaceAllString(dockerContainer.name, "")
if dockerContainerName == containerName {
containerId = dockerContainer.id
namespace = dockerContainer.namespace
}
}
// Сохраняем id контейнера для автообновления при смене окна
if newUpdate {
app.lastContainerId = containerId
} else {
containerId = app.lastContainerId
}
// Читаем журналы Docker из файловой системы в формате JSON (если не отключено флагом и есть доступ)
var readFileContainer bool
if containerizationSystem == "docker" && !app.dockerStreamLogs {
// Получаем путь к журналу контейнера в файловой системе по id с помощью метода docker cli
var cmd *exec.Cmd
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "docker", "inspect", "--format", "{{.LogPath}}", containerId)...)
} else {
cmd = exec.Command("docker", "inspect", "--format", "{{.LogPath}}", containerId)
}
logFilePathBytes, err := cmd.Output()
if err != nil && !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, "\033[31mError get log path via docker inspect:", err, "\033[0m")
return
}
if err != nil && app.testMode {
log.Print("Error: get log path via docker inspect. ", err)
}
logFilePath := strings.TrimSpace(string(logFilePathBytes))
// Читаем файл с конца с помощью tail
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions, "tail", "-n", app.logViewCount, logFilePath)...)
} else {
cmd = exec.Command("tail", "-n", app.logViewCount, logFilePath)
}
output, err := cmd.Output()
// Если ошибка чтения, значит нет доступа и переходим к чтению из потока
if err != nil && app.dockerStreamLogsStr == "json" {
readFileContainer = false
app.dockerStreamLogsStr = "stream"
app.dockerStreamLogs = true
if !app.testMode {
go func() {
text := "Access denied to json logs (use root)"
app.showInterfaceInfo(g, true, text)
time.Sleep(3 * time.Second)
app.closeInfo(g)
}()
}
} else {
readFileContainer = true
app.dockerStreamLogsStr = "json"
}
if readFileContainer {
// Проверяем, что есть изменения в файле при повторном считывание
if newUpdate {
// Фиксируем новую дату изменения и размер для выбранного файла
fileInfo, err := app.statFile(logFilePath)
if err != nil {
return
}
fileModTime := fileInfo.ModTime()
fileSize := fileInfo.Size()
app.lastDateUpdateFile = fileModTime
app.lastSizeFile = fileSize
app.updateFile = true
} else {
// Проверяем дату изменения
fileInfo, err := app.statFile(logFilePath)
if err != nil {
return
}
fileModTime := fileInfo.ModTime()
fileSize := fileInfo.Size()
// Обновлять файл, только если есть изменения (проверяем дату модификации и размер)
if fileModTime != app.lastDateUpdateFile || fileSize != app.lastSizeFile {
app.lastDateUpdateFile = fileModTime
app.lastSizeFile = fileSize
app.updateFile = true
} else {
app.updateFile = false
}
}
// Читаем файл, толькое если были изменения
if app.updateFile {
// Разбиваем строки на массив
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
var formattedLines []string
// Обрабатываем вывод в формате JSON построчно
for _, line := range lines {
// JSON-структура для парсинга
var jsonData map[string]interface{}
err := json.Unmarshal([]byte(line), &jsonData)
if err != nil {
continue
}
// Извлекаем JSON данные
stream, _ := jsonData["stream"].(string)
timeStr, _ := jsonData["time"].(string)
logMessage, _ := jsonData["log"].(string)
// Проверяем режим вывода потоков и пропускаем лишние строки
// Если текущий режим соответствует стандартному выводу и текущая строка содержит поток ошибки (или наоборот), пропускаем интерацию
if app.dockerStreamMode == "stdout" && stream == "stderr" {
continue
}
if app.dockerStreamMode == "stderr" && stream == "stdout" {
continue
}
// Удаляем встроенный экранированный символ переноса строки
logMessage = strings.TrimSuffix(logMessage, "\n")
// Парсим строку времени в объект time.Time
parsedTime, err := time.Parse(time.RFC3339Nano, timeStr)
if err == nil {
// Форматируем дату в формате: YYYY-MM-DDTHH:MM:SS.MS(x9)Z
timeStr = parsedTime.Format("2006-01-02T15:04:05.000000000Z")
}
var formattedLine string
// Заполняем строку в формате
switch {
case app.timestampDocker && app.streamTypeDocker:
// stream time log
formattedLine = fmt.Sprintf("%s %s %s", stream, timeStr, logMessage)
case app.timestampDocker && !app.streamTypeDocker:
// time log
formattedLine = fmt.Sprintf("%s %s", timeStr, logMessage)
case !app.timestampDocker && app.streamTypeDocker:
// stream log
formattedLine = fmt.Sprintf("%s %s", stream, logMessage)
case !app.timestampDocker && !app.streamTypeDocker:
// log only
formattedLine = logMessage
}
formattedLines = append(formattedLines, formattedLine)
// Если это последняя строка в выводе, добавляем перенос строки
}
app.currentLogLines = formattedLines
}
}
}
// Читаем лог через docker cli (если файл не найден или к нему нет доступа) или это compose/podman/kubectl
if !readFileContainer || containerizationSystem != "docker" {
// Извлекаем имя без статуса в containerId для k8s и docker compose
if containerizationSystem == "kubectl" || containerizationSystem == "compose" {
parts := strings.Split(containerName, "] ")
containerId = parts[1]
}
var cmd *exec.Cmd
switch containerizationSystem {
case "kubectl":
// Формируем команду kubectl с нужными ключами
if app.sshMode {
cmd = exec.Command("ssh", append(app.sshOptions,
containerizationSystem, "logs", "-n", namespace, "--timestamps=true", "--tail", app.logViewCount, containerId,
)...)
} else {
cmd = exec.Command(
containerizationSystem, "logs", "-n", namespace, "--timestamps=true", "--tail", app.logViewCount, containerId,
)
}
case "compose":
sinceFilterTextNotSpace := reSpace.ReplaceAllString(app.sinceFilterText, "T")
untilFilterTextNotSpace := reSpace.ReplaceAllString(app.untilFilterText, "T")
if app.sshMode {
switch {
case app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
"ssh", append(app.sshOptions,
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--since", sinceFilterTextNotSpace,
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
)...,
)
case app.sinceTimestampFilterMode && !app.untilTimestampFilterMode:
cmd = exec.Command(
"ssh", append(app.sshOptions,
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--since", sinceFilterTextNotSpace,
"--tail", app.logViewCount,
)...,
)
case !app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
"ssh", append(app.sshOptions,
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
)...,
)
default:
cmd = exec.Command(
"ssh", append(app.sshOptions,
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--tail", app.logViewCount,
)...,
)
}
} else {
switch {
case app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--since", sinceFilterTextNotSpace,
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
)
case app.sinceTimestampFilterMode && !app.untilTimestampFilterMode:
cmd = exec.Command(
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--since", sinceFilterTextNotSpace,
"--tail", app.logViewCount,
)
case !app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
)
default:
cmd = exec.Command(
"docker", "compose", "-p", containerId, "logs", "--timestamps", "--no-color", // "--no-log-prefix",
"--tail", app.logViewCount,
)
}
}
default:
// docker/podman cli
sinceFilterTextNotSpace := reSpace.ReplaceAllString(app.sinceFilterText, "T")
untilFilterTextNotSpace := reSpace.ReplaceAllString(app.untilFilterText, "T")
if app.sshMode {
switch {
case app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
"ssh", append(app.sshOptions,
containerizationSystem, "logs", "--timestamps",
"--since", sinceFilterTextNotSpace,
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
containerId,
)...,
)
case app.sinceTimestampFilterMode && !app.untilTimestampFilterMode:
cmd = exec.Command(
"ssh", append(app.sshOptions,
containerizationSystem, "logs", "--timestamps",
"--since", sinceFilterTextNotSpace,
"--tail", app.logViewCount,
containerId,
)...,
)
case !app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
"ssh", append(app.sshOptions,
containerizationSystem, "logs", "--timestamps",
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
containerId,
)...,
)
default:
cmd = exec.Command(
"ssh", append(app.sshOptions,
containerizationSystem, "logs", "--timestamps",
"--tail", app.logViewCount,
containerId,
)...,
)
}
} else {
switch {
case app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
containerizationSystem, "logs", "--timestamps",
"--since", sinceFilterTextNotSpace,
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
containerId,
)
case app.sinceTimestampFilterMode && !app.untilTimestampFilterMode:
cmd = exec.Command(
containerizationSystem, "logs", "--timestamps",
"--since", sinceFilterTextNotSpace,
"--tail", app.logViewCount,
containerId,
)
case !app.sinceTimestampFilterMode && app.untilTimestampFilterMode:
cmd = exec.Command(
containerizationSystem, "logs", "--timestamps",
"--until", untilFilterTextNotSpace,
"--tail", app.logViewCount,
containerId,
)
default:
cmd = exec.Command(
containerizationSystem, "logs", "--timestamps",
"--tail", app.logViewCount,
containerId,
)
}
}
}
// Храним байты вывода
var stdoutBytes, stderrBytes []byte
var stdoutErr, stderrErr error
// Храним комбинированный вывод двух потоков
var combined []dockerLogLines
switch {
// Читаем только один поток в режиме stdout или compose
case app.dockerStreamMode == "stdout" || containerizationSystem == "compose" || containerizationSystem == "kubectl":
// Читаем стандартный вывод
stdoutPipe, _ := cmd.StdoutPipe()
_ = cmd.Start()
stdoutBytes, stdoutErr = io.ReadAll(stdoutPipe)
stdoutLines := strings.Split(string(stdoutBytes), "\n")
// Формируем итоговый массив
for _, line := range stdoutLines {
// Пропускаем пустые строки
if strings.TrimSpace(line) == "" {
continue
}
var ts time.Time
var err error
// Извлекаем время из compose
if containerizationSystem == "compose" {
// Сначала извлекаем имя сервиса
parts1 := strings.SplitN(line, " | ", 2)
// Затем извлекаем timestamp
parts2 := strings.SplitN(parts1[1], " ", 2)
tsStr := strings.TrimSpace(parts2[0])
ts, err = time.Parse(time.RFC3339Nano, tsStr)
} else {
// Извлекаем время из префикса docker/podman
ts, err = parseTimestamp(line)
}
if err != nil {
continue
}
combined = append(combined, dockerLogLines{
isError: false,
timestamp: ts,
content: line,
})
}
// Сортируем вывод по timestamp для compose
if containerizationSystem == "compose" {
sort.Slice(
combined,
func(i, j int) bool {
return combined[i].timestamp.Before(combined[j].timestamp)
},
)
}
case app.dockerStreamMode == "stderr":
// Читаем вывод ошибок
stderrPipe, _ := cmd.StderrPipe()
_ = cmd.Start()
stderrBytes, stderrErr = io.ReadAll(stderrPipe)
stderrLines := strings.Split(string(stderrBytes), "\n")
// Формируем итоговый массив
for _, line := range stderrLines {
if strings.TrimSpace(line) == "" {
continue
}
ts, err := parseTimestamp(line)
if err != nil {
continue
}
combined = append(combined, dockerLogLines{
isError: true,
timestamp: ts,
content: line,
})
}
default:
// Читаем стандартный вывод
stdoutPipe, _ := cmd.StdoutPipe()
// Читаем вывод ошибок
stderrPipe, _ := cmd.StderrPipe()
// Запускаем команду
_ = cmd.Start()
// Читаем два потока параллельно, чтобы не блокировать
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
stdoutBytes, stdoutErr = io.ReadAll(stdoutPipe)
}()
go func() {
defer wg.Done()
stderrBytes, stderrErr = io.ReadAll(stderrPipe)
}()
wg.Wait()
_ = cmd.Wait()
// Обработка ошибок чтения
if stdoutErr != nil || stderrErr != nil {
if !app.testMode {
v, _ := app.gui.View("logs")
v.Clear()
fmt.Fprintln(v, "\033[31mError getting logs from", containerName, "(id:", containerId, ")", "container.\033[0m")
return
} else {
log.Print("Error: getting logs from ", containerName, " (id:", containerId, ")", " container.")
}
}
// Получаем 2 массива вывода
stdoutLines := strings.Split(string(stdoutBytes), "\n")
stderrLines := strings.Split(string(stderrBytes), "\n")
// Объединяем два массив
for _, line := range stdoutLines {
if strings.TrimSpace(line) == "" {
continue
}
ts, err := parseTimestamp(line)
if err != nil {
continue
}
combined = append(combined, dockerLogLines{
isError: false,
timestamp: ts,
content: line,
})
}
for _, line := range stderrLines {
if strings.TrimSpace(line) == "" {
continue
}
ts, err := parseTimestamp(line)
if err != nil {
continue
}
combined = append(combined, dockerLogLines{
isError: true,
timestamp: ts,
content: line,
})
}
// Cортируем итоговый массив по timestamp
sort.Slice(
combined,
func(i, j int) bool {
return combined[i].timestamp.Before(combined[j].timestamp)
},
)
}
// Добавляем префиксы с типом данных (stdout или stderr) в зависимости от режима флагов
var finalLines []string
for _, entry := range combined {
entryLine := entry.content
// Удаляем из строки timestamp
if !app.timestampDocker {
entryLine = removeTimestamp(entry.content)
}
// Не добавляем профексы в отключенном режиме, а также для compose и kubectl
if !app.streamTypeDocker || containerizationSystem == "compose" || containerizationSystem == "kubectl" {
finalLines = append(finalLines, entryLine)
} else {
prefix := "stdout "
if entry.isError {
prefix = "stderr "
}
finalLine := prefix + entryLine
finalLines = append(finalLines, finalLine)
}
}
app.currentLogLines = finalLines
}
// Обновляем фильтр и делиметр всегда для потоков ИЛИ если есть изменения в файле при его чтение
if !readFileContainer || (readFileContainer && app.updateFile) || containerizationSystem != "docker" {
app.updateDelimiter(newUpdate)
app.applyFilter(false)
}
}
// Функция извлечения parseTimestamp для сортировки
func parseTimestamp(line string) (time.Time, error) {
// Делим строку на две части по первому пробелу
parts := strings.SplitN(line, " ", 2)
// Удаляем лишние пробелы
tsStr := strings.TrimSpace(parts[0])
// Парсим строку (извлекаем временную метку)
return time.Parse(time.RFC3339Nano, tsStr)
}
// Функция для удаления timestamp из строки (первого слова до первого пробела)
func removeTimestamp(line string) string {
// Находим индекс первого пробела
spaceIndex := strings.Index(line, " ")
if spaceIndex == -1 {
// Если пробела нет, возвращаем строку как есть
return line
}
// Возвращаем строку начиная с символа после первого пробела
return line[spaceIndex+1:]
}
// ---------------------------------------- Filter ----------------------------------------
// Редактор обработки ввода текста для фильтрации
func (app *App) createFilterEditor(window string) gocui.Editor {
return gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
// Добавляем символ в поле ввода
case ch != 0 && mod == 0:
v.EditWrite(ch)
// Добавляем пробел
case key == gocui.KeySpace:
v.EditWrite(' ')
// Удаляем символ слева от курсора
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
// Удаляем символ справа от курсора
case key == gocui.KeyDelete:
v.EditDelete(false)
// Перемещение курсора влево
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0) // удалить 3-й булевой параметр для форка
// Перемещение курсора вправо
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0)
}
switch window {
case "logs":
// Обновляем текст в буфере
app.filterText = strings.TrimSpace(v.Buffer())
// Применяем функцию фильтрации к выводу записей журнала
app.applyFilter(true)
case "lists":
app.filterListText = strings.TrimSpace(v.Buffer())
app.applyFilterList()
}
})
}
// Функция для обработки фильтрации по временной метке
func (app *App) timestampFilterEditor(window string) gocui.Editor {
return gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
// Пропускаем только цифры (0-9)
case ch >= '0' && ch <= '9':
v.EditWrite(ch)
// Пропускаем ":" для времени, "-" для даты, а также [+-] и [smh] для сокращенного формата
case ch == ':' || ch == '-' || ch == '+' || ch == 's' || ch == 'm' || ch == 'h' || ch == 'd':
v.EditWrite(ch)
// Пропускаем пробел (работает в journalctl для разделения времени, но необходимо обновить в docker logs на T)
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0)
}
switch window {
case "sinceFilter":
// Обновляем текст в буфере
app.sinceFilterText = strings.TrimSpace(v.Buffer())
// Если фильтр пустой, отключаем фильтрацию
switch {
case strings.TrimSpace(v.Buffer()) == "":
v.FrameColor = gocui.ColorGreen
app.sinceTimestampFilterMode = false
// Проверяем формат и активируем фильтрацию
case app.timestampCheckFormat(strings.TrimSpace(v.Buffer())):
v.FrameColor = gocui.ColorGreen
app.sinceTimestampFilterMode = true
default:
v.FrameColor = gocui.ColorRed
app.sinceTimestampFilterMode = false
}
case "untilFilter":
app.untilFilterText = strings.TrimSpace(v.Buffer())
switch {
case strings.TrimSpace(v.Buffer()) == "":
v.FrameColor = gocui.ColorGreen
app.untilTimestampFilterMode = false
// Проверяем формат и активируем фильтрацию
case app.timestampCheckFormat(strings.TrimSpace(v.Buffer())):
v.FrameColor = gocui.ColorGreen
app.untilTimestampFilterMode = true
default:
v.FrameColor = gocui.ColorRed
app.untilTimestampFilterMode = false
}
}
})
}
// Функция проверки формата времени для фильтрации
func (app *App) timestampCheckFormat(input string) bool {
formats := []string{
"15:04", // 00:00
"15:04:05", // 00:00:00
"2006-01-02", // 2025-04-14
"2006-01-02 15:04", // 2025-04-14 00:00
"2006-01-02 15:04:05", // 2025-04-14 00:00:00
}
if filterTimeRegex.MatchString(input) {
return true
} else {
for _, layout := range formats {
if _, err := time.Parse(layout, input); err == nil {
return true
}
}
}
return false
}
// Функция для фильтрации всех списоков журналов
func (app *App) applyFilterList() {
filter := strings.ToLower(app.filterListText)
// Временные массивы для отфильтрованных журналов
var filteredJournals []Journal
var filteredLogFiles []Logfile
var filteredDockerContainers []DockerContainers
for _, j := range app.journalsNotFilter {
if strings.Contains(strings.ToLower(j.name), filter) {
filteredJournals = append(filteredJournals, j)
}
}
for _, j := range app.logfilesNotFilter {
if strings.Contains(strings.ToLower(j.name), filter) {
filteredLogFiles = append(filteredLogFiles, j)
}
}
for _, j := range app.dockerContainersNotFilter {
if strings.Contains(strings.ToLower(j.name), filter) {
filteredDockerContainers = append(filteredDockerContainers, j)
}
}
// Сбрасываем индексы выбранного журнала для правильного позиционирования
app.selectedJournal = 0
app.selectedFile = 0
app.selectedDockerContainer = 0
app.startServices = 0
app.startFiles = 0
app.startDockerContainers = 0
// Сохраняем отфильтрованные и отсортированные данные
app.journals = filteredJournals
app.logfiles = filteredLogFiles
app.dockerContainers = filteredDockerContainers
// Обновляем статус количества служб
if !app.testMode {
// Обновляем списки в интерфейсе
app.updateServicesList()
app.updateLogsList()
app.updateDockerContainerList()
v, _ := app.gui.View("services")
// Обновляем счетчик в заголовке
re := regexp.MustCompile(`\s\(.+\) >`)
updateTitle := " (0) >"
if len(app.journals) != 0 {
updateTitle = " (" + strconv.Itoa(app.selectedJournal+1) + "/" + strconv.Itoa(len(app.journals)) + ") >"
}
v.Title = re.ReplaceAllString(v.Title, updateTitle)
// Обновляем статус количества файлов
v, _ = app.gui.View("varLogs")
// Обновляем счетчик в заголовке
re = regexp.MustCompile(`\s\(.+\) >`)
updateTitle = " (0) >"
if len(app.logfiles) != 0 {
updateTitle = " (" + strconv.Itoa(app.selectedFile+1) + "/" + strconv.Itoa(len(app.logfiles)) + ") >"
}
v.Title = re.ReplaceAllString(v.Title, updateTitle)
// Обновляем статус количества контейнеров
v, _ = app.gui.View("docker")
// Обновляем счетчик в заголовке
re = regexp.MustCompile(`\s\(.+\) >`)
updateTitle = " (0) >"
if len(app.dockerContainers) != 0 {
updateTitle = " (" + strconv.Itoa(app.selectedDockerContainer+1) + "/" + strconv.Itoa(len(app.dockerContainers)) + ") >"
}
v.Title = re.ReplaceAllString(v.Title, updateTitle)
}
}
// Функция для фильтрации записей текущего журнала + покраска
func (app *App) applyFilter(color bool) {
filter := app.filterText
var skip bool = false
var size int
var viewHeight int
var err error
if !app.testMode {
v, err := app.gui.View("filter")
if err != nil {
return
}
if color {
v.FrameColor = gocui.ColorGreen
}
// Если текст фильтра не менялся и позиция курсора не в самом конце журнала, то пропускаем фильтрацию и покраску при пролистывании
vLogs, _ := app.gui.View("logs")
_, viewHeight := vLogs.Size()
size = app.logScrollPos + viewHeight + 1
if app.lastFilterText == filter && size < len(app.filteredLogLines) {
skip = true
}
// Фиксируем текущий текст из фильтра
app.lastFilterText = filter
}
// Фильтруем и красим, только если это не скроллинг
if !skip {
// Debug end load time
endLoadTime := time.Since(app.debugStartTime)
// Фиксируем время окончания загрузки журнала
app.debugLoadTime = endLoadTime.Truncate(time.Millisecond).String()
// Debug start color time
// Фиксируем время начала покраски журнала
startTime := time.Now()
// Debug: если текст фильтра пустой или равен любому символу для regex, возвращяем вывод без фильтрации
if filter == "" || (filter == "." && app.selectFilterMode == "regex") {
app.filteredLogLines = app.currentLogLines
} else {
app.filteredLogLines = make([]string, 0)
// Опускаем регистр ввода текста для фильтра
filter = strings.ToLower(filter)
// Проверка регулярного выражения
var regex *regexp.Regexp
if app.selectFilterMode == "regex" {
// Добавляем флаг для нечувствительности к регистру по умолчанию
filter = "(?i)" + filter
// Компилируем регулярное выражение
regex, err = regexp.Compile(filter)
// В случае синтаксической ошибки регулярного выражения, красим окно красным цветом и завершаем цикл
if err != nil && !app.testMode {
v, _ := app.gui.View("filter")
v.FrameColor = gocui.ColorRed
return
}
if err != nil && !app.testMode {
log.Print("Error: regex syntax")
return
}
}
// Проходимся по каждой строке
for _, line := range app.currentLogLines {
switch app.selectFilterMode {
// Fuzzy (неточный поиск без учета регистра)
case "fuzzy":
outputLine := app.fuzzyFilter(line, filter)
if outputLine != "" {
app.filteredLogLines = append(app.filteredLogLines, outputLine)
}
// Regex (с использованием регулярных выражений и без учета регистра по умолчанию)
case "regex":
outputLine := app.regexFilter(line, regex)
if outputLine != "" {
app.filteredLogLines = append(app.filteredLogLines, outputLine)
}
// Default (точный поиск с учетом регистра)
default:
filter = app.filterText
if filter == "" || strings.Contains(line, filter) {
lineColor := strings.ReplaceAll(line, filter, "\x1b[0;44m"+filter+"\033[0m")
app.filteredLogLines = append(app.filteredLogLines, lineColor)
}
}
}
}
// Если последняя строка не содержит пустую строку, то добавляем две пустые строки или одну по умолчанию
if len(app.filteredLogLines) > 0 && app.filteredLogLines[len(app.filteredLogLines)-1] != "" {
app.filteredLogLines = append(app.filteredLogLines, "", "")
} else {
app.filteredLogLines = append(app.filteredLogLines, "")
}
// Отключаем покраску в режиме colorMode
if app.colorMode {
// Режим покраски через tailspin
if app.tailSpinMode {
cmd := exec.Command(app.tailSpinBinName)
logLines := strings.Join(app.filteredLogLines, "\n")
// Создаем пайп для передачи данных
cmd.Stdin = bytes.NewBufferString(logLines)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
fmt.Println(err)
}
colorLogLines := strings.Split(out.String(), "\n")
app.filteredLogLines = colorLogLines
} else {
app.filteredLogLines = app.mainColor(app.filteredLogLines)
}
}
// Debug end time
endTime := time.Since(startTime)
app.debugColorTime = endTime.Truncate(time.Millisecond).String()
}
// Debug: корректируем текущую позицию скролла, если размер массива стал меньше
if size > len(app.filteredLogLines) {
newScrollPos := len(app.filteredLogLines) - viewHeight
if newScrollPos > 0 {
app.logScrollPos = newScrollPos
} else {
app.logScrollPos = 0
}
}
// Обновляем автоскролл (всегда опускаем вывод в самый низ) для отображения отфильтрованных записей
if !app.testMode {
// Включаем автоскролл и сбрасываем позицию
if !app.disableAutoScroll {
app.autoScroll = true
} else {
app.autoScroll = false
}
vLog, _ := app.gui.View("logs")
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
app.logScrollPos = 0
app.updateLogsView(true)
}
}
// Fyzzy: Функция для неточного поиска (параметры: строка из цикла и текст фильтрации)
func (app *App) fuzzyFilter(inputLine, filter string) string {
// Разбиваем текст фильтра на массив из строк
filterWords := strings.Fields(filter)
// Опускаем регистр текущей строки цикла
lineLower := strings.ToLower(inputLine)
var match bool = true
// Проверяем, если строка не содержит хотя бы одно слово из фильтра, то пропускаем строку
for _, word := range filterWords {
if !strings.Contains(lineLower, word) {
match = false
break
}
}
// Если строка подходит под фильтр, возвращаем ее с покраской
if match {
// Временные символы для обозначения начала и конца покраски найденных символов
startColor := "►"
endColor := "◄"
originalLine := inputLine
// Проходимся по всем словосочетаниям фильтра (массив через пробел) для позиционирования покраски
for _, word := range filterWords {
wordLower := strings.ToLower(word)
start := 0
// Ищем все вхождения слова в строке с учетом регистра
for {
// Находим индекс вхождения с учетом регистра
idx := strings.Index(strings.ToLower(originalLine[start:]), wordLower)
if idx == -1 {
break // Если больше нет вхождений, выходим
}
start += idx // корректируем индекс с учетом текущей позиции
// Вставляем временные символы для покраски
originalLine = originalLine[:start] + startColor + originalLine[start:start+len(word)] + endColor + originalLine[start+len(word):]
// Сдвигаем индекс для поиска в оставшейся части строки
start += len(startColor) + len(word) + len(endColor)
}
}
// Заменяем временные символы на ANSI escape-последовательности
originalLine = strings.ReplaceAll(originalLine, startColor, "\x1b[0;44m")
originalLine = strings.ReplaceAll(originalLine, endColor, "\033[0m")
return originalLine
} else {
return ""
}
}
// Regex: Функция для поска с использованием регулярных выражений (параметры: строка из цикла и скомпилированное регулярное выражение)
func (app *App) regexFilter(inputLine string, regex *regexp.Regexp) string {
// Проверяем, что строка подходит под регулярное выражение
if regex.MatchString(inputLine) {
// Находим все найденные совпадени
matches := regex.FindAllString(inputLine, -1)
// Красим только первое найденное совпадение
inputLine = strings.ReplaceAll(inputLine, matches[0], "\x1b[0;44m"+matches[0]+"\033[0m")
return inputLine
} else {
return ""
}
}
// -f/--command-fuzzy
func (app *App) commandLineFuzzy(filter string) {
stat, err := os.Stdin.Stat()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if (stat.Mode() & os.ModeCharDevice) != 0 {
fmt.Fprintln(os.Stderr, "No data. Use pipe to transfer data.")
return
}
scanner := bufio.NewScanner(os.Stdin)
var inputLines []string
for scanner.Scan() {
inputLines = append(inputLines, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if len(inputLines) == 0 {
fmt.Fprintln(os.Stderr)
return
}
for _, line := range inputLines {
outputLine := app.fuzzyFilter(line, filter)
if outputLine != "" {
app.filteredLogLines = append(app.filteredLogLines, outputLine)
}
}
for _, line := range app.filteredLogLines {
fmt.Println(line)
}
}
// --command-regex/-r
func (app *App) commandLineRegex(regex *regexp.Regexp) {
stat, err := os.Stdin.Stat()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if (stat.Mode() & os.ModeCharDevice) != 0 {
fmt.Fprintln(os.Stderr, "No data. Use pipe to transfer data.")
return
}
scanner := bufio.NewScanner(os.Stdin)
var inputLines []string
for scanner.Scan() {
inputLines = append(inputLines, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if len(inputLines) == 0 {
fmt.Fprintln(os.Stderr)
return
}
for _, line := range inputLines {
outputLine := app.regexFilter(line, regex)
if outputLine != "" {
app.filteredLogLines = append(app.filteredLogLines, outputLine)
}
}
for _, line := range app.filteredLogLines {
fmt.Println(line)
}
}
// ---------------------------------------- Coloring ----------------------------------------
// Функция для покраски вывода в режиме командной строки
func (app *App) commandLineColor() {
// Проверяем, подключен ли stdin через pipe или перенаправлен
stat, err := os.Stdin.Stat()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
// Проверяем, пуст ли stdin (например, если нет pipe или перенаправления)
if (stat.Mode() & os.ModeCharDevice) != 0 {
fmt.Fprintln(os.Stderr, "No data. Use pipe to transfer data.")
return
}
scanner := bufio.NewScanner(os.Stdin)
var inputLines []string
for scanner.Scan() {
inputLines = append(inputLines, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if len(inputLines) == 0 {
fmt.Fprintln(os.Stderr)
return
}
inputColoring := app.mainColor(inputLines)
for _, line := range inputColoring {
fmt.Println(line)
}
}
// Основная функция покраски
func (app *App) mainColor(inputText []string) []string {
// Максимальное количество потоков
const maxWorkers = 10
// Канал для передачи индексов всех строк
tasks := make(chan int, len(inputText))
// Срез для хранения обработанных строк
colorLogLines := make([]string, len(inputText))
// Объявляем группу ожидания для синхронизации всех горутин (воркеров)
var wg sync.WaitGroup
// Создаем maxWorkers горутин, где каждая будет обрабатывать задачи из канала tasks
for i := 0; i < maxWorkers; i++ {
go func() {
// Горутина будет работать, пока в канале tasks есть задачи
for index := range tasks {
// Обрабатываем строку и сохраняем результат по соответствующему индексу
colorLogLines[index] = app.lineColor(inputText[index])
// Уменьшаем счетчик задач в группе ожидания.
wg.Done()
}
}()
}
// Добавляем задачи в канал
for i := range inputText {
// Увеличиваем счетчик задач в группе ожидания
wg.Add(1)
// Передаем индекс строки в канал tasks
tasks <- i
}
// Закрываем канал задач, чтобы воркеры завершили работу после обработки всех задач
close(tasks)
// Ждем завершения всех задач
wg.Wait()
return colorLogLines
}
func (app *App) lineColor(inputLine string) string {
// Если строка пустая, пропускаем ее сразу
if inputLine == "" {
return ""
}
var colorLine string
var filterColor bool = false
// Извлекаем название контейнера в логах стека compose
var containerName string
if app.lastContainerizationSystem == "compose" {
// Исключаем строку с делиметром
if !strings.HasPrefix(inputLine, "⎯") {
splitLine := strings.SplitN(inputLine, " | ", 2)
if splitLine[0] != "" && splitLine[1] != "" {
containerName = splitLine[0]
// Удаляем название контейнера из покраски
inputLine = splitLine[1]
}
}
}
// Разбиваем строку по пробелам, сохраняя их
words := strings.Split(inputLine, " ")
for i, word := range words {
// Исключаем строки с покраской при поиске (Background)
if strings.Contains(word, "\x1b[0;44m") {
filterColor = true
}
// Красим слово в функции
if !filterColor {
word = app.wordColor(word)
}
// Возобновляем покраску
if strings.Contains(word, "\033[0m") {
filterColor = false
}
// Добавляем слово обратно с пробелами
if i != len(words)-1 {
colorLine += word + " "
} else {
colorLine += word
}
}
if app.selectContainerizationSystem == "compose" && containerName != "" {
// Возвращяем название контейнера с уникальной покраской
if app.uniquePrefixColorMap[strings.TrimSpace(containerName)] != "" {
return app.uniquePrefixColorMap[strings.TrimSpace(containerName)] + containerName + " |\033[0m " + colorLine
} else {
return containerName + " | " + colorLine
}
} else {
return colorLine
}
}
// Игнорируем регистр и проверяем, что слово окружено границами (не буквы и цифры)
func (app *App) replaceWordLower(word, keyword, color string) string {
re := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(keyword) + `\b`)
return re.ReplaceAllStringFunc(word, func(match string) string {
return color + match + "\033[0m"
})
}
// Поиск пользователей
func (app *App) containsUser(searchWord string) bool {
for _, user := range app.userNameArray {
if user == searchWord {
return true
}
}
return false
}
// Поиск корневых директорий
func (app *App) containsPath(searchWord string) bool {
for _, dir := range app.rootDirArray {
if strings.Contains(searchWord, dir) {
return true
}
}
return false
}
// Покраска url путей
func (app *App) urlPathColor(cleanedWord string) string {
// Используем Builder для объединения строк
var sb strings.Builder
// Начинаем с желтого цвета
sb.WriteString("\033[33m")
for _, char := range cleanedWord {
switch {
// Пурпурный цвет для символов и возвращяем желтый
case char == '/' || char == '?' || char == '&' || char == '=' || char == ':' || char == '.':
sb.WriteString("\033[35m")
sb.WriteRune(char)
sb.WriteString("\033[33m")
// Синий цвет для цифр
// case unicode.IsDigit(char):
case char >= '0' && char <= '9':
sb.WriteString("\033[34m")
sb.WriteRune(char)
sb.WriteString("\033[33m")
default:
sb.WriteRune(char)
}
}
// Сброс цвета
sb.WriteString("\033[0m")
return sb.String()
}
// Функция для покраски словосочетаний
func (app *App) wordColor(inputWord string) string {
// Опускаем регистр слова
inputWordLower := strings.ToLower(inputWord)
// Значение по умолчанию
var coloredWord string = inputWord
switch {
// URL
case strings.Contains(inputWord, "http://"):
cleanedWord := app.trimHttpRegex.ReplaceAllString(inputWord, "")
coloredChars := app.urlPathColor(cleanedWord)
// Красный для http
coloredWord = strings.ReplaceAll(inputWord, "http://"+cleanedWord, "\033[31mhttp\033[35m://"+coloredChars)
case strings.Contains(inputWord, "https://"):
cleanedWord := app.trimHttpsRegex.ReplaceAllString(inputWord, "")
coloredChars := app.urlPathColor(cleanedWord)
// Зеленый для https
coloredWord = strings.ReplaceAll(inputWord, "https://"+cleanedWord, "\033[32mhttps\033[35m://"+coloredChars)
// UNIX file paths
case app.containsPath(inputWord):
cleanedWord := app.trimPrefixPathRegex.ReplaceAllString(inputWord, "")
cleanedWord = app.trimPostfixPathRegex.ReplaceAllString(cleanedWord, "")
// Начинаем с желтого цвета
coloredChars := "\033[33m"
for _, char := range cleanedWord {
// Красим символы разделителя путей в пурпурный и возвращяем цвет
if char == '/' {
coloredChars += "\033[35m" + string(char) + "\033[33m"
} else {
coloredChars += string(char)
}
}
coloredWord = strings.ReplaceAll(inputWord, cleanedWord, "\033[35m"+coloredChars+"\033[0m")
// Желтый (известные имена: hostname и username) [33m]
case strings.Contains(inputWord, app.hostName):
coloredWord = strings.ReplaceAll(inputWord, app.hostName, "\033[33m"+app.hostName+"\033[0m")
case strings.Contains(inputWord, app.userName):
coloredWord = strings.ReplaceAll(inputWord, app.userName, "\033[33m"+app.userName+"\033[0m")
// Список пользователей из passwd
case app.containsUser(inputWord):
coloredWord = app.replaceWordLower(inputWord, inputWord, "\033[33m")
case strings.Contains(inputWordLower, "warn"):
words := []string{"warnings", "warning", "warn"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[33m")
break
}
}
// UNIX processes
case app.syslogUnitRegex.MatchString(inputWord):
unitSplit := strings.Split(inputWord, "[")
unitName := unitSplit[0]
unitId := strings.ReplaceAll(unitSplit[1], "]:", "")
coloredWord = strings.ReplaceAll(inputWord, inputWord, "\033[36m"+unitName+"\033[0m"+"\033[33m"+"["+"\033[0m"+"\033[34m"+unitId+"\033[0m"+"\033[33m"+"]"+"\033[0m"+":")
case strings.HasPrefix(inputWordLower, "kernel:"):
coloredWord = app.replaceWordLower(inputWord, "kernel", "\033[36m")
case strings.HasPrefix(inputWordLower, "rsyslogd:"):
coloredWord = app.replaceWordLower(inputWord, "rsyslogd", "\033[36m")
case strings.HasPrefix(inputWordLower, "sudo:"):
coloredWord = app.replaceWordLower(inputWord, "sudo", "\033[36m")
// Исключения
case strings.Contains(inputWordLower, "unblock"):
words := []string{"unblocking", "unblocked", "unblock"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
// Красный (ошибки) [31m]
case strings.Contains(inputWordLower, "err"):
words := []string{"stderr", "errors", "error", "erro", "err"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "dis"):
words := []string{"disconnected", "disconnection", "disconnects", "disconnect", "disabled", "disabling", "disable"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "crash"):
words := []string{"crashed", "crashing", "crash"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "delet"):
words := []string{"deletion", "deleted", "deleting", "deletes", "delete"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "remov"):
words := []string{"removing", "removed", "removes", "remove"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "stop"):
words := []string{"stopping", "stopped", "stoped", "stops", "stop"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "invalid"):
words := []string{"invalidation", "invalidating", "invalidated", "invalidate", "invalid"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "abort"):
words := []string{"aborted", "aborting", "abort"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "block"):
words := []string{"blocked", "blocker", "blocking", "blocks", "block"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "activ"):
words := []string{"inactive", "deactivated", "deactivating", "deactivate"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "exit"):
words := []string{"exited", "exiting", "exits", "exit"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "crit"):
words := []string{"critical", "critic", "crit"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "fail"):
words := []string{"failed", "failure", "failing", "fails", "fail"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "reject"):
words := []string{"rejecting", "rejection", "rejected", "reject"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "fatal"):
words := []string{"fatality", "fataling", "fatals", "fatal"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "clos"):
words := []string{"closed", "closing", "close"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "drop"):
words := []string{"dropped", "droping", "drops", "drop"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "kill"):
words := []string{"killer", "killing", "kills", "kill"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "cancel"):
words := []string{"cancellation", "cancelation", "canceled", "cancelling", "canceling", "cancel"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "refus"):
words := []string{"refusing", "refused", "refuses", "refuse"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "restrict"):
words := []string{"restricting", "restricted", "restriction", "restrict"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "panic"):
words := []string{"panicked", "panics", "panic"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[31m")
break
}
}
case strings.Contains(inputWordLower, "unknown"):
coloredWord = app.replaceWordLower(inputWord, "unknown", "\033[31m")
case strings.Contains(inputWordLower, "unavailable"):
coloredWord = app.replaceWordLower(inputWord, "unavailable", "\033[31m")
case strings.Contains(inputWordLower, "unsuccessful"):
coloredWord = app.replaceWordLower(inputWord, "unsuccessful", "\033[31m")
case strings.Contains(inputWordLower, "found"):
coloredWord = app.replaceWordLower(inputWord, "found", "\033[31m")
case strings.Contains(inputWordLower, "denied"):
coloredWord = app.replaceWordLower(inputWord, "denied", "\033[31m")
case strings.Contains(inputWordLower, "conflict"):
coloredWord = app.replaceWordLower(inputWord, "conflict", "\033[31m")
case strings.Contains(inputWordLower, "false"):
coloredWord = app.replaceWordLower(inputWord, "false", "\033[31m")
case strings.Contains(inputWordLower, "none"):
coloredWord = app.replaceWordLower(inputWord, "none", "\033[31m")
case strings.Contains(inputWordLower, "null"):
coloredWord = app.replaceWordLower(inputWord, "null", "\033[31m")
// Исключения
case strings.Contains(inputWordLower, "res"):
words := []string{"resolved", "resolving", "resolve", "restarting", "restarted", "restart"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
// Зеленый (успех) [32m]
case strings.Contains(inputWordLower, "succe"):
words := []string{"successfully", "successful", "succeeded", "succeed", "success"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "complet"):
words := []string{"completed", "completing", "completion", "completes", "complete"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "accept"):
words := []string{"accepted", "accepting", "acception", "acceptance", "acceptable", "acceptably", "accepte", "accepts", "accept"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "connect"):
words := []string{"connected", "connecting", "connection", "connects", "connect"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "finish"):
words := []string{"finished", "finishing", "finish"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "start"):
words := []string{"started", "starting", "startup", "start"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "creat"):
words := []string{"created", "creating", "creates", "create"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "enable"):
words := []string{"enabled", "enables", "enable"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "allow"):
words := []string{"allowed", "allowing", "allow"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "post"):
words := []string{"posting", "posted", "postrouting", "post"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "rout"):
words := []string{"prerouting", "routing", "routes", "route"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "forward"):
words := []string{"forwarding", "forwards", "forward"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "pass"):
words := []string{"passed", "passing", "password"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "run"):
words := []string{"running", "runs", "run"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "add"):
words := []string{"added", "add"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "open"):
words := []string{"opening", "opened", "open"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[32m")
break
}
}
case strings.Contains(inputWordLower, "ok"):
coloredWord = app.replaceWordLower(inputWord, "ok", "\033[32m")
case strings.Contains(inputWordLower, "available"):
coloredWord = app.replaceWordLower(inputWord, "available", "\033[32m")
case strings.Contains(inputWordLower, "accessible"):
coloredWord = app.replaceWordLower(inputWord, "accessible", "\033[32m")
case strings.Contains(inputWordLower, "done"):
coloredWord = app.replaceWordLower(inputWord, "done", "\033[32m")
case strings.Contains(inputWordLower, "true"):
coloredWord = app.replaceWordLower(inputWord, "true", "\033[32m")
// Синий (статусы) [36m]
case strings.Contains(inputWordLower, "req"):
words := []string{"requested", "requests", "request"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "reg"):
words := []string{"registered", "registeration"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "boot"):
words := []string{"reboot", "booting", "boot"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "out"):
words := []string{"stdout", "timeout", "output"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "put"):
words := []string{"input", "put"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "get"):
words := []string{"getting", "get"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "set"):
words := []string{"settings", "setting", "setup", "set"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "head"):
words := []string{"headers", "header", "heades", "head"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "log"):
words := []string{"logged", "login"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "load"):
words := []string{"overloading", "overloaded", "overload", "uploading", "uploaded", "uploads", "upload", "downloading", "downloaded", "downloads", "download", "loading", "loaded", "load"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "read"):
words := []string{"reading", "readed", "read"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "patch"):
words := []string{"patching", "patched", "patch"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "up"):
words := []string{"updates", "updated", "updating", "update", "upgrades", "upgraded", "upgrading", "upgrade", "backup", "up"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "listen"):
words := []string{"listening", "listener", "listen"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "launch"):
words := []string{"launched", "launching", "launch"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "chang"):
words := []string{"changed", "changing", "change"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "clea"):
words := []string{"cleaning", "cleaner", "clearing", "cleared", "clear"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "skip"):
words := []string{"skipping", "skipped", "skip"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "miss"):
words := []string{"missing", "missed"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "mount"):
words := []string{"mountpoint", "mounted", "mounting", "mount"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "auth"):
words := []string{"authenticating", "authentication", "authorization"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "conf"):
words := []string{"configurations", "configuration", "configuring", "configured", "configure", "config", "conf"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "option"):
words := []string{"options", "option"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "writ"):
words := []string{"writing", "writed", "write"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "sav"):
words := []string{"saved", "saving", "save"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "paus"):
words := []string{"paused", "pausing", "pause"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "filt"):
words := []string{"filtration", "filtr", "filtering", "filtered", "filter"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "norm"):
words := []string{"normal", "norm"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "noti"):
words := []string{"notifications", "notification", "notify", "noting", "notice"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "alert"):
words := []string{"alerting", "alert"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "in"):
words := []string{"informations", "information", "informing", "informed", "info", "installation", "installed", "installing", "install", "initialization", "initial", "using"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "down"):
words := []string{"shutdown", "down"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "us"):
words := []string{"status", "used", "use"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[36m")
break
}
}
case strings.Contains(inputWordLower, "debug"):
coloredWord = app.replaceWordLower(inputWord, "debug", "\033[36m")
case strings.Contains(inputWordLower, "verbose"):
coloredWord = app.replaceWordLower(inputWord, "verbose", "\033[36m")
case strings.HasPrefix(inputWordLower, "trace"):
coloredWord = app.replaceWordLower(inputWord, "trace", "\033[36m")
case strings.HasPrefix(inputWordLower, "protocol"):
coloredWord = app.replaceWordLower(inputWord, "protocol", "\033[36m")
case strings.Contains(inputWordLower, "level"):
coloredWord = app.replaceWordLower(inputWord, "level", "\033[36m")
// Голубой (цифры) [34m]
// Byte (0x04)
case app.hexByteRegex.MatchString(inputWord):
coloredWord = app.hexByteRegex.ReplaceAllStringFunc(inputWord, func(match string) string {
colored := ""
for _, char := range match {
if char == 'x' {
colored += "\033[35m" + string(char) + "\033[0m"
} else {
colored += "\033[34m" + string(char) + "\033[0m"
}
}
return colored
})
// DateTime
case app.dateTimeRegex.MatchString(inputWord):
coloredWord = app.dateTimeRegex.ReplaceAllStringFunc(inputWord, func(match string) string {
colored := ""
for _, char := range match {
if char == '-' || char == '.' || char == ':' || char == '+' || char == 'T' || char == 'Z' {
// Пурпурный для символов
colored += "\033[35m" + string(char) + "\033[0m"
} else {
// Синий для цифр
colored += "\033[34m" + string(char) + "\033[0m"
}
}
return colored
})
// Integers
case app.integersInputRegex.MatchString(inputWord):
var colored strings.Builder
// Флаги, для фиксации нахождения внутри числа/символа или нет
inNumber := false
inSymbol := false
for _, char := range inputWord {
switch {
case char >= '0' && char <= '9':
// Если это цифра и мы еще не в числе, открываем цвет
if !inNumber {
colored.WriteString("\033[34m")
inNumber = true
}
case char == '/' || char == ':' || char == '.' || char == '-' || char == '+' || char == '%':
// Красим символы
colored.WriteString("\033[35m")
inSymbol = true
inNumber = false
default:
// Если это не цифра и до этого было число, закрываем цвет
if inNumber {
inNumber = false
}
// Для всех других символов
colored.WriteString("\033[0m")
}
// Добавляем символ в результат
colored.WriteRune(char)
// Закрываем цвет для символа
if inSymbol {
colored.WriteString("\033[0m")
inSymbol = false
}
}
// Закрываем цвет, если строка закончилась на числе
if inNumber {
colored.WriteString("\033[0m")
}
return colored.String()
// tcpdump
case strings.Contains(inputWordLower, "tcp"):
coloredWord = app.replaceWordLower(inputWord, "tcp", "\033[33m")
case strings.Contains(inputWordLower, "udp"):
coloredWord = app.replaceWordLower(inputWord, "udp", "\033[33m")
case strings.Contains(inputWordLower, "icmp"):
coloredWord = app.replaceWordLower(inputWord, "icmp", "\033[33m")
case strings.Contains(inputWordLower, "ip"):
words := []string{"ip4", "ipv4", "ip6", "ipv6", "ip"}
for _, word := range words {
if strings.Contains(inputWordLower, word) {
coloredWord = app.replaceWordLower(inputWord, word, "\033[33m")
break
}
}
// Update delimiter
case strings.Contains(inputWord, "⎯"):
coloredWord = strings.ReplaceAll(inputWord, inputWord, "\033[35m"+inputWord+"\033[0m")
// Исключения
case strings.Contains(inputWordLower, "not"):
coloredWord = app.replaceWordLower(inputWord, "not", "\033[31m")
}
return coloredWord
}
// ---------------------------------------- Log output ----------------------------------------
// Функция для обновления вывода журнала (параметр для прокрутки в самый вниз)
func (app *App) updateLogsView(lowerDown bool) {
// Получаем доступ к выводу журнала
v, err := app.gui.View("logs")
if err != nil {
return
}
// Очищаем окно для отображения новых строк
v.Clear()
// Получаем ширину и высоту окна
viewWidth, viewHeight := v.Size()
// Опускаем в самый низ, только если это не ручной скролл (отключается параметром)
if lowerDown {
// Если количество строк больше высоты окна, опускаем в самый низ
if len(app.filteredLogLines) > viewHeight-1 {
app.logScrollPos = len(app.filteredLogLines) - viewHeight - 1
} else {
app.logScrollPos = 0
}
}
// Определяем количество строк для отображения, начиная с позиции logScrollPos
startLine := app.logScrollPos
endLine := startLine + viewHeight
if endLine > len(app.filteredLogLines) {
endLine = len(app.filteredLogLines)
}
// Учитываем auto wrap (только в конце лога)
if app.logScrollPos == len(app.filteredLogLines)-viewHeight-1 {
var viewLines int = 0 // количество строк для вывода
var viewCounter int = 0 // обратный счетчик видимых строк для остановки
var viewIndex int = len(app.filteredLogLines) - 1 // начальный индекс для строк с конца
for {
// Фиксируем текущую входную строку и счетчик
viewLines += 1
viewCounter += 1
// Получаем длинну видимых символов в строке с конца
var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`)
lengthLine := len([]rune(ansiEscape.ReplaceAllString(app.filteredLogLines[viewIndex], "")))
// Если длинна строки больше ширины окна, получаем количество дополнительных строк
if lengthLine > viewWidth {
// Увеличивая счетчик и пропускаем строки
viewCounter += lengthLine / viewWidth
}
// Если счетчик привысил количество видимых строк, вычетаем последнюю строку из видимости
if viewCounter > viewHeight {
viewLines -= 1
}
if viewCounter >= viewHeight {
break
}
// Уменьшаем индекс
viewIndex -= 1
}
for i := len(app.filteredLogLines) - viewLines - 1; i < endLine; i++ {
fmt.Fprintln(v, app.filteredLogLines[i])
}
} else {
// Проходим по отфильтрованным строкам и выводим их
for i := startLine; i < endLine; i++ {
fmt.Fprintln(v, app.filteredLogLines[i])
}
}
// Вычисляем процент прокрутки и обновляем заголовок
var percentage int = 0
if len(app.filteredLogLines) > 0 {
// Стартовая позиция + размер текущего вывода логов и округляем в большую сторону (math)
percentage = int(math.Ceil(float64((startLine+viewHeight)*100) / float64(len(app.filteredLogLines))))
if percentage > 100 {
v.Title = fmt.Sprintf(
"Logs: 100%% (%d) ["+app.debugLoadTime+"/"+app.debugColorTime+"]",
len(app.filteredLogLines),
)
} else {
v.Title = fmt.Sprintf("Logs: %d%% (%d/%d) ["+app.debugLoadTime+"/"+app.debugColorTime+"]",
percentage,
startLine+1+viewHeight,
len(app.filteredLogLines),
)
}
} else {
v.Title = "Logs: 0% (0) [" + app.debugLoadTime + "/" + app.debugColorTime + "]"
}
v.TitleColor = gocui.ColorYellow
app.viewScrollLogs(percentage)
}
// Функция для обновления интерфейса скроллинга
func (app *App) viewScrollLogs(percentage int) {
vScroll, _ := app.gui.View("scrollLogs")
vScroll.Clear()
// Определяем высоту окна
_, viewHeight := vScroll.Size()
// Заполняем скролл пробелами, если вывод пустой или не выходит за пределы окна
if percentage == 0 || percentage > 100 {
fmt.Fprintln(vScroll, "▲")
for i := 1; i < viewHeight-1; i++ {
fmt.Fprintln(vScroll, " ")
}
fmt.Fprintln(vScroll, "▼")
} else {
// Рассчитываем позицию курсора (корректируем процент на размер скролла и верхней стрелки)
scrollPosition := (viewHeight*percentage)/100 - 3 - 1
fmt.Fprintln(vScroll, "▲")
// Выводим строки с пробелами и символом █
for_scroll:
for i := 1; i < viewHeight-3; i++ {
// Проверяем текущую поизицию
switch {
case i == scrollPosition:
// Выводим скролл
fmt.Fprintln(vScroll, "███")
case scrollPosition <= 0 || app.logScrollPos == 0:
// Если вышли за пределы окна или текст находится в самом начале, устанавливаем курсор в начало
fmt.Fprintln(vScroll, "███")
// Остальное заполняем пробелами с учетом стрелки и курсора (-4) до последней стрелки (-1)
for i := 4; i < viewHeight-1; i++ {
fmt.Fprintln(vScroll, " ")
}
break for_scroll
default:
// Пробелы на остальных строках
fmt.Fprintln(vScroll, " ")
}
}
fmt.Fprintln(vScroll, "▼")
}
}
// Функция для скроллинга вниз
func (app *App) scrollDownLogs(step int) error {
v, err := app.gui.View("logs")
if err != nil {
return err
}
// Получаем высоту окна, что бы не опускать лог с пустыми строками
_, viewHeight := v.Size()
// Проверяем, что размер журнала больше размера окна
if len(app.filteredLogLines) > viewHeight {
// Увеличиваем позицию прокрутки
app.logScrollPos += step
// Если достигнут конец списка, останавливаем на максимальной длинне с учетом высоты окна
if app.logScrollPos > len(app.filteredLogLines)-1-viewHeight {
app.logScrollPos = len(app.filteredLogLines) - 1 - viewHeight
// Включаем автоскролл (если он не отключен)
if !app.disableAutoScroll {
app.autoScroll = true
} else {
app.autoScroll = false
}
if !app.testMode {
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
}
// Вызываем функцию для обновления отображения журнала
app.updateLogsView(false)
}
return nil
}
// Функция для скроллинга вверх
func (app *App) scrollUpLogs(step int) error {
app.logScrollPos -= step
if app.logScrollPos < 0 {
app.logScrollPos = 0
}
// Отключаем автоскролл
app.autoScroll = false
if !app.testMode {
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
app.updateLogsView(false)
return nil
}
// Функция для переход к началу журнала
func (app *App) pageUpLogs() {
app.logScrollPos = 0
app.autoScroll = false
if !app.testMode {
vLog, _ := app.gui.View("logs")
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
app.updateLogsView(false)
}
// Функция для очистки поля ввода фильтра вывода лога
func (app *App) clearFilterEditor(g *gocui.Gui) {
v, _ := g.View("filter")
// Очищаем содержимое View
v.Clear()
// Устанавливаем курсор на начальную позицию
if err := v.SetCursor(0, 0); err != nil {
return
}
// Очищаем буфер фильтра
app.filterText = ""
app.applyFilter(false)
}
// Функция для очистки поля ввода фильтра списков
func (app *App) clearFilterListEditor(g *gocui.Gui) {
v, _ := g.View("filterList")
v.Clear()
if err := v.SetCursor(0, 0); err != nil {
return
}
app.filterListText = ""
app.applyFilterList()
}
// Функция для обновления последнего выбранного вывода лога (параметр для загрузки журнала)
func (app *App) updateLogOutput(newUpdate bool) {
// Выполняем обновление интерфейса через метод Update для иницилизации перерисовки интерфейса
app.gui.Update(func(g *gocui.Gui) error {
// Сбрасываем автоскролл, что бы опустить журнал вниз, т.к. это всегда ручное обновление
if !app.disableAutoScroll {
app.autoScroll = true
} else {
app.autoScroll = false
}
if !app.testMode {
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
switch app.lastWindow {
case "services":
if app.fastMode {
go func() {
app.loadJournalLogs(app.lastSelected, newUpdate)
}()
} else {
app.loadJournalLogs(app.lastSelected, newUpdate)
}
case "varLogs":
if app.fastMode {
go func() {
app.loadFileLogs(app.lastSelected, newUpdate)
}()
} else {
app.loadFileLogs(app.lastSelected, newUpdate)
}
case "docker":
if app.fastMode {
go func() {
app.loadDockerLogs(app.lastSelected, newUpdate)
}()
} else {
app.loadDockerLogs(app.lastSelected, newUpdate)
}
}
return nil
})
}
// Запускает фоновое обновление с изменяемым интервалом (параметры для обновления времени и загрузки журнала)
func (app *App) updateLogBackground(secondsChan chan int, newUpdate bool) {
seconds := app.logUpdateSeconds
// Проверяем, есть ли в канале новое значение интервала
select {
case s := <-secondsChan:
seconds = s
default:
}
// Таймер
ticker := time.NewTicker(time.Duration(seconds) * time.Second)
// Гарантируем остановку таймера при выходе из функции
defer ticker.Stop()
for {
select {
// Если в канал поступило новое значение, перезапускаем таймер с новым интервалом
case newSeconds := <-secondsChan:
ticker.Reset(time.Duration(newSeconds) * time.Second)
// Когда срабатывает таймер, выполняем обновление логов
case <-ticker.C:
// Обновляем журнал только если включен автоскролл
if app.autoScroll {
app.gui.Update(func(g *gocui.Gui) error {
switch app.lastWindow {
case "services":
if app.fastMode {
go func() {
app.loadJournalLogs(app.lastSelected, newUpdate)
}()
} else {
app.loadJournalLogs(app.lastSelected, newUpdate)
}
case "varLogs":
if app.fastMode {
go func() {
app.loadFileLogs(app.lastSelected, newUpdate)
}()
} else {
app.loadFileLogs(app.lastSelected, newUpdate)
}
case "docker":
if app.fastMode {
go func() {
app.loadDockerLogs(app.lastSelected, newUpdate)
}()
} else {
app.loadDockerLogs(app.lastSelected, newUpdate)
}
}
return nil
})
}
}
}
}
// Функция для обновления вывода при изменение размера окна
func (app *App) updateWindowSize(seconds int) {
for {
app.gui.Update(func(g *gocui.Gui) error {
v, err := g.View("logs")
if err != nil {
log.Panicln(err)
}
windowWidth, windowHeight := v.Size()
if windowWidth != app.windowWidth || windowHeight != app.windowHeight {
app.windowWidth, app.windowHeight = windowWidth, windowHeight
app.updateLogsView(true)
if v, err := g.View("services"); err == nil {
_, viewHeight := v.Size()
app.maxVisibleServices = viewHeight
}
if v, err := g.View("varLogs"); err == nil {
_, viewHeight := v.Size()
app.maxVisibleFiles = viewHeight
}
if v, err := g.View("docker"); err == nil {
_, viewHeight := v.Size()
app.maxVisibleDockerContainers = viewHeight
}
app.applyFilterList()
}
// Обновляем ширину для фильтрации по дате
maxX, _ := g.Size()
leftPanelWidth := maxX / 4
filterWidth := (maxX - leftPanelWidth - 1) / 2
if _, err := g.View("sinceFilter"); err == nil {
if _, err := g.SetView("sinceFilter", leftPanelWidth+1, 0, leftPanelWidth+1+filterWidth, 2, 0); err != nil {
return nil
}
}
if _, err := g.View("untilFilter"); err == nil {
if _, err := g.SetView("untilFilter", leftPanelWidth+1+filterWidth+1, 0, maxX-1, 2, 0); err != nil {
return nil
}
}
return nil
})
time.Sleep(time.Duration(seconds) * time.Second)
}
}
// Функция для фиксации места загрузки журнала с помощью делимитра (параметр для обновления места и времени загрузки)
func (app *App) updateDelimiter(newUpdate bool) {
if newUpdate {
// Фиксируем (сохраняем) предпоследнюю (-2, т.к. последняя строка всегда пустая) строку для вставки делимитра (если это ручной выбор из списка) или выходим
if len(app.currentLogLines) > 2 {
app.lastUpdateLine = app.currentLogLines[len(app.currentLogLines)-2]
} else {
return
}
// Сбрасываем автоскролл
if !app.disableAutoScroll {
app.autoScroll = true
} else {
app.autoScroll = false
}
if !app.testMode {
vLog, _ := app.gui.View("logs")
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
// Фиксируем новое время загрузки журнала
app.updateTime = time.Now().Format("15:04:05")
} else {
// Ищем индекс строки в массиве с конца
delimiterIndex := 0
for i := len(app.currentLogLines) - 1; i >= 0; i-- {
if app.currentLogLines[i] == app.lastUpdateLine {
delimiterIndex = i
break
}
}
// Проверяем, что строка найдена и найденный индекс меньше длинны массива строк
if delimiterIndex > 0 && delimiterIndex < len(app.currentLogLines)-2 {
// Формируем длинну делимитра
v, _ := app.gui.View("logs")
width, _ := v.Size()
lengthDelimiter := width/2 - 5
delimiter1 := strings.Repeat("⎯", lengthDelimiter)
delimiter2 := delimiter1
if width > lengthDelimiter+lengthDelimiter+10 {
delimiter2 = strings.Repeat("⎯", lengthDelimiter+1)
}
var delimiterString string = delimiter1 + " " + app.updateTime + " " + delimiter2
// Вставляем новую строку после указанного индекса + 1 пустая строка (сдвигая остальные строки массива)
app.currentLogLines = append(app.currentLogLines[:delimiterIndex+1],
append([]string{delimiterString}, app.currentLogLines[delimiterIndex+1:]...)...)
}
}
}
// ---------------------------------------- Key Binding ----------------------------------------
// Карта для сопостовления сочетаний клавиш со значениями из конфигурации
var keyMap = map[string]gocui.Key{
"f1": gocui.KeyF1,
"f2": gocui.KeyF2,
"f3": gocui.KeyF3,
"f4": gocui.KeyF4,
"f5": gocui.KeyF5,
"f6": gocui.KeyF6,
"f7": gocui.KeyF7,
"f8": gocui.KeyF8,
"f9": gocui.KeyF9,
"f10": gocui.KeyF10,
"f11": gocui.KeyF11,
"f12": gocui.KeyF12,
"ctrl+a": gocui.KeyCtrlA,
"ctrl+b": gocui.KeyCtrlB,
"ctrl+c": gocui.KeyCtrlC,
"ctrl+d": gocui.KeyCtrlD,
"ctrl+e": gocui.KeyCtrlE,
"ctrl+f": gocui.KeyCtrlF,
"ctrl+g": gocui.KeyCtrlG,
"ctrl+h": gocui.KeyCtrlH,
"ctrl+i": gocui.KeyCtrlI,
"ctrl+j": gocui.KeyCtrlJ,
"ctrl+k": gocui.KeyCtrlK,
"ctrl+l": gocui.KeyCtrlL,
"ctrl+m": gocui.KeyCtrlM,
"ctrl+n": gocui.KeyCtrlN,
"ctrl+o": gocui.KeyCtrlO,
"ctrl+p": gocui.KeyCtrlP,
"ctrl+q": gocui.KeyCtrlQ,
"ctrl+r": gocui.KeyCtrlR,
"ctrl+s": gocui.KeyCtrlS,
"ctrl+t": gocui.KeyCtrlT,
"ctrl+u": gocui.KeyCtrlU,
"ctrl+v": gocui.KeyCtrlV,
"ctrl+w": gocui.KeyCtrlW,
"ctrl+x": gocui.KeyCtrlX,
"ctrl+y": gocui.KeyCtrlY,
"ctrl+z": gocui.KeyCtrlZ,
"tab": gocui.KeyTab,
"shift+tab": gocui.KeyBacktab,
"enter": gocui.KeyEnter,
"space": gocui.KeySpace,
"backspace": gocui.KeyBackspace,
"delete": gocui.KeyDelete,
"escape": gocui.KeyEsc,
}
// Функция для опредиления клавиш из конфигурации
func getHotkey(configKey, defaultKey string) any {
// Опускаем регистр для всех вхождений (букв и сочетаний)
inputKey := strings.ToLower(configKey)
// Если это одна буква, конвертируем string в rune (используя DecodeRuneInString) и извлекаем значение
if len(inputKey) == 1 {
if r, _ := utf8.DecodeRuneInString(inputKey); r != utf8.RuneError {
return r
}
} else {
// Если сочетание клавиш содержит shift, извлекаем последнюю букву в верхнем регистре
if strings.HasPrefix(inputKey, "shift+") && inputKey != "shift+tab" {
inputKey = strings.ToTitle(configKey)
return []rune(inputKey)[len(inputKey)-1]
} else {
// Ищем сочетание клавиш в карте
key, exists := keyMap[inputKey]
if exists {
return key
}
}
}
// Возвращяем значение по умолчанию (которое передается во втором параметре)
if len(defaultKey) == 1 {
if r, _ := utf8.DecodeRuneInString(defaultKey); r != utf8.RuneError {
return r
}
}
return keyMap[defaultKey]
}
// Функция для биндинга клавиш
func (app *App) setupKeybindings() error {
// Help (F1)
// Открытие окна справки
customHelp := getHotkey(config.Hotkeys.Help, "f1")
helpHandler := func(g *gocui.Gui, v *gocui.View) error {
app.showInterfaceHelp(g)
// Удаляем глобальные биндинги
g.DeleteKeybindings("")
// Удаляем все биндинги назначенные для окон
viewsRange := []string{"filterList", "services", "varLogs", "docker", "filter", "sinceFilter", "untilFilter", "logs"}
for _, viewName := range viewsRange {
g.DeleteKeybindings(viewName)
}
// Создаем временный биндинг на Esc для закрытия окна
if err := app.gui.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.closeHelp(g)
// Возвращяем стандартные биндиги после закрытия окна справки
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
return nil
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
return err
}
return nil
}
if err := app.gui.SetKeybinding("", customHelp, gocui.ModNone, helpHandler); err != nil {
return err
}
// ↑↑↑
// Пролистывание вверх
// Up (1)
if err := app.gui.SetKeybinding("services", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 1)
}); err != nil {
return err
}
// PgUp (1) #10
if err := app.gui.SetKeybinding("services", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 1)
}); err != nil {
return err
}
// Custom up from config
// Default: k (1)
customUp := getHotkey(config.Hotkeys.Up, "k")
if err := app.gui.SetKeybinding("services", customUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 1)
}); err != nil {
return err
}
// Shift+Up (10)
if err := app.gui.SetKeybinding("services", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 10)
}); err != nil {
return err
}
// Shift+PgUp (10)
if err := app.gui.SetKeybinding("services", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 10)
}); err != nil {
return err
}
// Custom up from config
// Default: shift+k (10)
customQuickUp := getHotkey(config.Hotkeys.QuickUp, "K")
if err := app.gui.SetKeybinding("services", customQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 10)
}); err != nil {
return err
}
// Alt+Up (100)
if err := app.gui.SetKeybinding("services", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 100)
}); err != nil {
return err
}
// Alt+PgUp (100)
if err := app.gui.SetKeybinding("services", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 100)
}); err != nil {
return err
}
// Custom up from config
// Default: ctrl+k (100)
customVeryQuickUp := getHotkey(config.Hotkeys.VeryQuickUp, "ctrl+k")
if err := app.gui.SetKeybinding("services", customVeryQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customVeryQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customVeryQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 100)
}); err != nil {
return err
}
// ↓↓↓
// Перемещение вниз к следующей службе (функция nextService), файлу (nextFileName) или контейнеру (nextDockerContainer)
// Down (1)
if err := app.gui.SetKeybinding("services", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 1)
}); err != nil {
return err
}
// PgDown (1) #10
if err := app.gui.SetKeybinding("services", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 1)
}); err != nil {
return err
}
// Custom down from config
// Default: j (1)
customDown := getHotkey(config.Hotkeys.Down, "j")
if err := app.gui.SetKeybinding("services", customDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 1)
}); err != nil {
return err
}
// Быстрое пролистывание вниз через 10 записей
// Shift+Down (10)
if err := app.gui.SetKeybinding("services", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 10)
}); err != nil {
return err
}
// Shift+PgDown (10)
if err := app.gui.SetKeybinding("services", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 10)
}); err != nil {
return err
}
// Custom down from config
// Default: shift+j (10)
customQuickDown := getHotkey(config.Hotkeys.QuickDown, "J")
if err := app.gui.SetKeybinding("services", customQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 10)
}); err != nil {
return err
}
// Alt+Down (100)
if err := app.gui.SetKeybinding("services", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 100)
}); err != nil {
return err
}
// Alt+PgDown (100)
if err := app.gui.SetKeybinding("services", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 100)
}); err != nil {
return err
}
// Custom down from config
// Default: ctrl+j (100)
customVeryQuickDown := getHotkey(config.Hotkeys.VeryQuickDown, "ctrl+j")
if err := app.gui.SetKeybinding("services", customVeryQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customVeryQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customVeryQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 100)
}); err != nil {
return err
}
// Filtering mode (↑/↓)
// Переключение между режимами фильтрации через Up/Down для выбранного окна
if err := app.gui.SetKeybinding("filter", gocui.KeyArrowUp, gocui.ModNone, app.setFilterModeRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("filter", gocui.KeyArrowDown, gocui.ModNone, app.setFilterModeLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("sinceFilter", gocui.KeyArrowUp, gocui.ModNone, app.setFilterModeRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("sinceFilter", gocui.KeyArrowDown, gocui.ModNone, app.setFilterModeLeft); err != nil {
return err
}
// PgUp/PgDown
if err := app.gui.SetKeybinding("filter", gocui.KeyPgup, gocui.ModNone, app.setFilterModeRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("filter", gocui.KeyPgdn, gocui.ModNone, app.setFilterModeLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("sinceFilter", gocui.KeyPgup, gocui.ModNone, app.setFilterModeRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("sinceFilter", gocui.KeyPgdn, gocui.ModNone, app.setFilterModeLeft); err != nil {
return err
}
// Custom up and down for switch filter mode from config (ctrl+k b ctrl+j)
customUpFilterMode := getHotkey(config.Hotkeys.SwitchFilterMode, "ctrl+k")
customDownFilterMode := getHotkey(config.Hotkeys.BackSwitchFilterMode, "ctrl+j")
if err := app.gui.SetKeybinding("filter", customUpFilterMode, gocui.ModNone, app.setFilterModeRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("filter", customDownFilterMode, gocui.ModNone, app.setFilterModeLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("sinceFilter", customUpFilterMode, gocui.ModNone, app.setFilterModeRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("sinceFilter", customDownFilterMode, gocui.ModNone, app.setFilterModeLeft); err != nil {
return err
}
// ←/→
// Custom left and right from config
customLeft := getHotkey(config.Hotkeys.Left, "h")
customRight := getHotkey(config.Hotkeys.Right, "l")
// Переключение выбора журналов для systemd/journald (отключено для Windows)
if app.getOS != "windows" {
// Left/Right
if err := app.gui.SetKeybinding("services", gocui.KeyArrowLeft, gocui.ModNone, app.setUnitListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("services", gocui.KeyArrowRight, gocui.ModNone, app.setUnitListRight); err != nil {
return err
}
// [/]
if err := app.gui.SetKeybinding("services", '[', gocui.ModNone, app.setUnitListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("services", ']', gocui.ModNone, app.setUnitListRight); err != nil {
return err
}
// Default: h/l (100)
if err := app.gui.SetKeybinding("services", customLeft, gocui.ModNone, app.setUnitListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("services", customRight, gocui.ModNone, app.setUnitListRight); err != nil {
return err
}
}
// Переключение выбора журналов для File System
if app.keybindingsEnabled {
// Установка привязок
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowLeft, gocui.ModNone, app.setLogFilesListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowRight, gocui.ModNone, app.setLogFilesListRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", '[', gocui.ModNone, app.setLogFilesListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", ']', gocui.ModNone, app.setLogFilesListRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customLeft, gocui.ModNone, app.setLogFilesListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customRight, gocui.ModNone, app.setLogFilesListRight); err != nil {
return err
}
} else {
// Удаление привязок
if err := app.gui.DeleteKeybinding("varLogs", gocui.KeyArrowLeft, gocui.ModNone); err != nil {
return err
}
if err := app.gui.DeleteKeybinding("varLogs", gocui.KeyArrowRight, gocui.ModNone); err != nil {
return err
}
if err := app.gui.DeleteKeybinding("varLogs", '[', gocui.ModNone); err != nil {
return err
}
if err := app.gui.DeleteKeybinding("varLogs", ']', gocui.ModNone); err != nil {
return err
}
if err := app.gui.DeleteKeybinding("varLogs", customLeft, gocui.ModNone); err != nil {
return err
}
if err := app.gui.DeleteKeybinding("varLogs", customRight, gocui.ModNone); err != nil {
return err
}
}
// Переключение выбора журналов для Containerization System
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowLeft, gocui.ModNone, app.setContainersListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.KeyArrowRight, gocui.ModNone, app.setContainersListRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", '[', gocui.ModNone, app.setContainersListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", ']', gocui.ModNone, app.setContainersListRight); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customLeft, gocui.ModNone, app.setContainersListLeft); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customRight, gocui.ModNone, app.setContainersListRight); err != nil {
return err
}
// Logs ↓↓↓
// Пролистывание вывода журнала через 1/10/500 записей вниз
// Down/PgDown/j (1)
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", customDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(1)
}); err != nil {
return err
}
// Shift + Down/PgDown/j (10)
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", customQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(10)
}); err != nil {
return err
}
// Alt/Ctrl + Down/PgDown and Ctrl+j (500)
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowDown, gocui.ModMouseCtrl, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgdn, gocui.ModMouseCtrl, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", customVeryQuickDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(500)
}); err != nil {
return err
}
// Logs ↑↑↑
// Пролистывание вывода журнала через 1/10/500 записей вверх
// Up/PgUp/k (1)
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", customUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(1)
}); err != nil {
return err
}
// Shift + Up/PgUp/k (10)
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(10)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", customQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(10)
}); err != nil {
return err
}
// Alt/Ctrl + Up/PgUp and Ctrl+k (500)
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyArrowUp, gocui.ModMouseCtrl, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.KeyPgup, gocui.ModMouseCtrl, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(500)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", customVeryQuickUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(500)
}); err != nil {
return err
}
// Tab для переключения между окнами
customTab := getHotkey(config.Hotkeys.SwitchWindow, "tab")
if err := app.gui.SetKeybinding("", customTab, gocui.ModNone, app.nextView); err != nil {
return err
}
// Shift+Tab (Back Tab) для переключения между окнами в обратном порядке
customBackTab := getHotkey(config.Hotkeys.BackSwitchWindows, "shift+tab")
if err := app.gui.SetKeybinding("", customBackTab, gocui.ModNone, app.backView); err != nil {
return err
}
// Enter для выбора службы и загрузки журналов
customEnter := getHotkey(config.Hotkeys.LoadJournal, "enter")
if err := app.gui.SetKeybinding("services", customEnter, gocui.ModNone, app.selectService); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customEnter, gocui.ModNone, app.selectFile); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customEnter, gocui.ModNone, app.selectDocker); err != nil {
return err
}
// Enter для загрузки журнала из фильтра по времени
if err := app.gui.SetKeybinding("sinceFilter", customEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.updateLogOutput(true)
return nil
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("untilFilter", customEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.updateLogOutput(true)
return nil
}); err != nil {
return err
}
// filter (/) slash
// Переключение фокуса на окно фильтрации списков журналов
customSlash := getHotkey(config.Hotkeys.GoToFilter, "/")
if err := app.gui.SetKeybinding("services", customSlash, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.lastCurrentView = "services"
app.backCurrentView = true
return app.setSelectView(app.gui, "filterList")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customSlash, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.lastCurrentView = "varLogs"
app.backCurrentView = true
return app.setSelectView(app.gui, "filterList")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customSlash, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.lastCurrentView = "docker"
app.backCurrentView = true
return app.setSelectView(app.gui, "filterList")
}); err != nil {
return err
}
// В окне вывода журнала переключаемся на фильтр журнала
if err := app.gui.SetKeybinding("logs", customSlash, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.lastCurrentView = "logs"
app.backCurrentView = true
return app.setSelectView(app.gui, "filter")
}); err != nil {
return err
}
// Enter for return to the window
// Возврат к последнему окну до использования слэша с использование Enter из окна фильтрации
if err := app.gui.SetKeybinding("filterList", customEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.backCurrentView {
app.backCurrentView = false
return app.setSelectView(app.gui, app.lastCurrentView)
} else {
return nil
}
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("filter", customEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.backCurrentView {
app.backCurrentView = false
return app.setSelectView(app.gui, app.lastCurrentView)
} else {
return nil
}
}); err != nil {
return err
}
// End/Ctrl+E
// Перемещение к концу журнала
if err := app.gui.SetKeybinding("", gocui.KeyEnd, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
// Сбрасываем автоскролл
if !app.disableAutoScroll {
app.autoScroll = true
} else {
app.autoScroll = false
}
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
app.updateLogsView(true)
return nil
}); err != nil {
return err
}
customEnd := getHotkey(config.Hotkeys.GoToEnd, "ctrl+e")
if err := app.gui.SetKeybinding("", customEnd, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if !app.disableAutoScroll {
app.autoScroll = true
} else {
app.autoScroll = false
}
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
app.updateLogsView(true)
return nil
}); err != nil {
return err
}
// Home/Ctrl+A
// Перемещение к началу журнала
if err := app.gui.SetKeybinding("", gocui.KeyHome, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.pageUpLogs()
return nil
}); err != nil {
return err
}
customHome := getHotkey(config.Hotkeys.GoToTop, "ctrl+a")
if err := app.gui.SetKeybinding("", customHome, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.pageUpLogs()
return nil
}); err != nil {
return err
}
// tail mode (Alt+Left/Right)
// Переключение для количества строк вывода
customTailMore := getHotkey(config.Hotkeys.TailModeMore, "ctrl+x")
customTailLess := getHotkey(config.Hotkeys.TailModeLess, "ctrl+z")
if err := app.gui.SetKeybinding("", customTailMore, gocui.ModNone, app.setCountLogViewUp); err != nil {
return err
}
if err := app.gui.SetKeybinding("", customTailLess, gocui.ModNone, app.setCountLogViewDown); err != nil {
return err
}
// update interval Shift+Left/Right
// Увеличение фоновго интервала обновления журнала
customUpdateIntervalMore := getHotkey(config.Hotkeys.UpdateIntervalMore, "ctrl+p")
customUpdateIntervalLess := getHotkey(config.Hotkeys.UpdateIntervalLess, "ctrl+o")
if err := app.gui.SetKeybinding("", customUpdateIntervalMore, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.logUpdateSeconds >= 2 && app.logUpdateSeconds <= 9 {
app.logUpdateSeconds++
v, err := app.gui.View("logs")
if err != nil {
return err
}
app.secondsChan <- app.logUpdateSeconds
v.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
return nil
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("", customUpdateIntervalLess, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.logUpdateSeconds >= 3 && app.logUpdateSeconds <= 10 {
app.logUpdateSeconds--
v, err := app.gui.View("logs")
if err != nil {
return err
}
// Изменяем интервал в горутине
app.secondsChan <- app.logUpdateSeconds
v.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
}
return nil
}); err != nil {
return err
}
// auto update (Ctrl+U)
// Включение или отключение автоматического скроллинга
customAutoUpdate := getHotkey(config.Hotkeys.AutoUpdateJournal, "ctrl+u")
if err := app.gui.SetKeybinding("", customAutoUpdate, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.disableAutoScroll {
app.disableAutoScroll = false
app.autoScroll = false
} else {
app.disableAutoScroll = true
app.autoScroll = false
}
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
app.updateLogOutput(false)
return nil
}); err != nil {
return err
}
// update journal (Ctrl+R)
// Ручное обновление текущего вывода журнала
// Актуально в режиме выключенного автоматического обновления
customUpdateJournal := getHotkey(config.Hotkeys.UpdateJournal, "ctrl+r")
if err := app.gui.SetKeybinding("", customUpdateJournal, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
app.updateLogOutput(false)
return nil
}); err != nil {
return err
}
// update lists (Ctrl+Q)
// Обновить все текущие списки журналов вручную
customUpdateLists := getHotkey(config.Hotkeys.UpdateLists, "ctrl+q")
if err := app.gui.SetKeybinding("", customUpdateLists, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.getOS != "windows" {
app.loadServices(app.selectUnits)
app.loadFiles(app.selectPath)
} else {
app.loadWinFiles(app.selectPath)
}
app.loadDockerContainer(app.selectContainerizationSystem)
return nil
}); err != nil {
return err
}
// color disable/enable (Ctrl+W)
// Выключение/включение встроенной (custom built-in) покраски или через tailspin
customColor := getHotkey(config.Hotkeys.ColorDisable, "ctrl+w")
if err := app.gui.SetKeybinding("", customColor, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.colorMode {
app.colorMode = false
} else {
app.colorMode = true
}
if len(app.currentLogLines) != 0 {
app.updateLogsView(true)
app.applyFilter(false)
app.updateLogOutput(false)
}
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
return nil
}); err != nil {
return err
}
// tailspin (Ctrl+N)
// Включение/выключение режима покраски через tailspin
customTailspin := getHotkey(config.Hotkeys.TailspinEnable, "ctrl+n")
if err := app.gui.SetKeybinding("", customTailspin, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.tailSpinMode {
app.tailSpinMode = false
} else {
// Проверяем, что tailspin или tspin установлен в системе
tsCommands := []string{"tailspin", "tspin"}
for _, ts := range tsCommands {
cmd := exec.Command(ts, "--version")
_, err := cmd.Output()
// Если не установлен, выводим интерфейс ошибки на 3 секунды
if err != nil {
if !app.testMode {
go func() {
text := "tailspin/tspin not found in environment"
app.showInterfaceInfo(g, true, text)
time.Sleep(3 * time.Second)
app.closeInfo(g)
}()
}
} else {
app.tailSpinMode = true
app.tailSpinBinName = ts
}
}
}
if len(app.currentLogLines) != 0 {
app.updateLogsView(true)
app.applyFilter(false)
app.updateLogOutput(false)
}
return nil
}); err != nil {
return err
}
// docker log load mode from stream or file system (Ctrl+D)
// Переключение режима чтения журналов Docker из потоков или файловой системы
customDockerMode := getHotkey(config.Hotkeys.SwitchDockerMode, "ctrl+d")
if err := app.gui.SetKeybinding("", customDockerMode, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.dockerStreamLogs {
app.dockerStreamLogs = false
app.dockerStreamLogsStr = "json"
} else {
app.dockerStreamLogs = true
app.dockerStreamLogsStr = "stream"
}
app.updateLogOutput(false)
return nil
}); err != nil {
return err
}
// docker stream (Ctrl+S)
// Переключение режима вывода потоков журналов (фильтрация по потоку)
customStreamMode := getHotkey(config.Hotkeys.SwitchStreamMode, "ctrl+s")
if err := app.gui.SetKeybinding("", customStreamMode, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
switch {
case app.dockerStreamMode == "all":
app.dockerStreamMode = "stdout"
case app.dockerStreamMode == "stdout":
app.dockerStreamMode = "stderr"
case app.dockerStreamMode == "stderr":
app.dockerStreamMode = "all"
}
app.updateLogOutput(false)
return nil
}); err != nil {
return err
}
// docker timestamp (Ctrl+T)
// Переключение режима вывода timestamp и название потока
customTimestamp := getHotkey(config.Hotkeys.TimestampShow, "ctrl+t")
if err := app.gui.SetKeybinding("", customTimestamp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
switch {
case app.timestampDocker && app.streamTypeDocker:
app.streamTypeDocker = false
case app.timestampDocker && !app.streamTypeDocker:
app.timestampDocker = false
case !app.timestampDocker && !app.streamTypeDocker:
app.streamTypeDocker = true
case !app.timestampDocker && app.streamTypeDocker:
app.timestampDocker = true
}
app.updateLogOutput(false)
return nil
}); err != nil {
return err
}
// Exit (ctrl+c)
// Очистка поля ввода для фильтрации списков или выход
customExit := getHotkey(config.Hotkeys.Exit, "ctrl+c")
if err := app.gui.SetKeybinding("filterList", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.filterListText == "" {
return quit(g, v)
} else {
// Очищаем фильтр
app.clearFilterListEditor(g)
// Возвращяемся к последнему окну из фильтра
if app.backCurrentView {
app.backCurrentView = false
return app.setSelectView(app.gui, app.lastCurrentView)
} else {
return nil
}
}
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("services", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.filterListText == "" {
return quit(g, v)
} else {
app.clearFilterListEditor(g)
return nil
}
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.filterListText == "" {
return quit(g, v)
} else {
app.clearFilterListEditor(g)
return nil
}
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.filterListText == "" {
return quit(g, v)
} else {
app.clearFilterListEditor(g)
return nil
}
}); err != nil {
return err
}
// Очистка поля ввода для фильтрации логов или выход
if err := app.gui.SetKeybinding("filter", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.filterText == "" {
return quit(g, v)
} else {
app.clearFilterEditor(g)
if app.backCurrentView {
app.backCurrentView = false
return app.setSelectView(app.gui, app.lastCurrentView)
} else {
return nil
}
}
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.filterText == "" {
return quit(g, v)
} else {
app.clearFilterEditor(g)
return nil
}
}); err != nil {
return err
}
// Очистка поля ввода для фильтрации по времени
if err := app.gui.SetKeybinding("sinceFilter", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.sinceFilterText == "" {
return quit(g, v)
} else {
v.Clear()
app.sinceFilterText = strings.TrimSpace(v.Buffer())
v.FrameColor = gocui.ColorGreen
app.sinceTimestampFilterMode = false
return nil
}
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("untilFilter", customExit, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if app.untilFilterText == "" {
return quit(g, v)
} else {
v.Clear()
app.untilFilterText = strings.TrimSpace(v.Buffer())
v.FrameColor = gocui.ColorGreen
app.untilTimestampFilterMode = false
return nil
}
}); err != nil {
return err
}
// Mouse control
// Привязка клика мыши для выбора элемента в списке журналов и изменения фокуса на окно
if err := app.gui.SetKeybinding("filterList", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.setSelectView(g, "filterList")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("services", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
err := app.selectService(g, v)
if err != nil {
return err
}
return app.setSelectView(g, "services")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
err := app.selectFile(g, v)
if err != nil {
return err
}
return app.setSelectView(g, "varLogs")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
err := app.selectDocker(g, v)
if err != nil {
return err
}
return app.setSelectView(g, "docker")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("filter", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.setSelectView(g, "filter")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("sinceFilter", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.setSelectView(g, "sinceFilter")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("untilFilter", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.setSelectView(g, "untilFilter")
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.MouseLeft, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.setSelectView(g, "logs")
}); err != nil {
return err
}
// Скроллинг колесом мыши вверх/вниз на 1 элемент
if err := app.gui.SetKeybinding("services", gocui.MouseWheelUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("services", gocui.MouseWheelDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextService(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.MouseWheelUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("varLogs", gocui.MouseWheelDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextFileName(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.MouseWheelUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.prevDockerContainer(v, 1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("docker", gocui.MouseWheelDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.nextDockerContainer(v, 1)
}); err != nil {
return err
}
// Скроллинг по журналу через 1 или 100 (alt/ctrl) строк
if err := app.gui.SetKeybinding("logs", gocui.MouseWheelUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.MouseWheelUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.MouseWheelUp, gocui.ModMouseCtrl, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollUpLogs(100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.MouseWheelDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(1)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.MouseWheelDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(100)
}); err != nil {
return err
}
if err := app.gui.SetKeybinding("logs", gocui.MouseWheelDown, gocui.ModMouseCtrl, func(g *gocui.Gui, v *gocui.View) error {
return app.scrollDownLogs(100)
}); err != nil {
return err
}
return nil
}
// Интерфейс справки
func (app *App) showInterfaceHelp(g *gocui.Gui) {
// Получаем размеры терминала
maxX, maxY := g.Size()
// Размеры окна help
width, height := 108, 54
// Вычисляем координаты для центрального расположения
x0 := (maxX - width) / 2
y0 := (maxY - height) / 2
x1 := x0 + width
y1 := y0 + height
helpView, err := g.SetView("help", x0, y0, x1, y1, 0)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
return
}
helpView.Title = " Help "
helpView.Autoscroll = true
helpView.Wrap = true
helpView.FrameColor = gocui.ColorGreen
helpView.TitleColor = gocui.ColorGreen
helpView.Clear()
fmt.Fprintln(helpView, "\n \033[32m_ \033[36m_ _ ")
fmt.Fprintln(helpView, " \033[32m| | \033[36m| | | |")
fmt.Fprintln(helpView, " \033[32m| | __ _ ____ _ _ \033[36m| | ___ _ _ _ __ _ __ __ _ | |")
fmt.Fprintln(helpView, " \033[32m| | / _` ||_ /| | | | \033[36m_ | | / _ \\ | | | || '__|| '_ \\ / _` || |")
fmt.Fprintln(helpView, " \033[32m| |____| (_| | / / | |_| |\033[36m| |__| || (_) || |_| || | | | | || (_| || |")
fmt.Fprintln(helpView, " \033[32m|______|\\__,_|/___| \\__, | \033[36m\\____/ \\___/ \\__,_||_| |_| |_| \\__,_||_|")
fmt.Fprintln(helpView, " \033[32m __/ | ")
fmt.Fprintln(helpView, " \033[32m |___/\033[0m")
fmt.Fprintln(helpView, "\n Version: "+app.wordColor(programVersion))
fmt.Fprintln(helpView, "\n Hotkeys description (default values):")
fmt.Fprintln(helpView, "\n \033[32mUp\033[0m/\033[32mPgUp\033[0m/\033[32mk\033[0m and \033[32mDown\033[0m/\033[32mPgDown\033[0m/\033[32mj\033[0m - move up and down through all journal lists and log output,")
fmt.Fprintln(helpView, " as well as changing the filtering mode in the filter window.")
fmt.Fprintln(helpView, " \033[32mShift\033[0m/\033[32mAlt\033[0m+\033[32mUp\033[0m/\033[32mDown\033[0m - quickly move up and down through all journal lists and log output")
fmt.Fprintln(helpView, " every 10 or 100 lines (500 for log output).")
fmt.Fprintln(helpView, " \033[32mShift\033[0m/\033[32mCtrl\033[0m+\033[32mk\033[0m/\033[32mj\033[0m - quickly move up and down (like Vim and alternative for macOS from config).")
fmt.Fprintln(helpView, " \033[32mLeft\033[0m/\033[32m[\033[0m/\033[32mh\033[0m and \033[32mRight\033[0m/\033[32m]\033[0m/\033[32ml\033[0m - switch between journal lists in the selected window.")
fmt.Fprintln(helpView, " \033[32mTab\033[0m - switch to next window.")
fmt.Fprintln(helpView, " \033[32mShift\033[0m+\033[32mTab\033[0m - return to previous window.")
fmt.Fprintln(helpView, " \033[32mEnter\033[0m - load a log from the list window or return to the previous window from the filter window.")
fmt.Fprintln(helpView, " \033[32m/\033[0m - go to the filter window from the current list window or logs window.")
fmt.Fprintln(helpView, " \033[32mEnd\033[0m/\033[32mCtrl\033[0m+\033[32mE\033[0m - go to the end of the log.")
fmt.Fprintln(helpView, " \033[32mHome\033[0m/\033[32mCtrl\033[0m+\033[32mA\033[0m - go to the top of the log.")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mX\033[0m/\033[32mZ\033[0m - change the number of log lines to output (default: 50000, range: 200-200000).")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mP\033[0m/\033[32mO\033[0m - change the auto refresh interval of the log output (default: 5, range: 2-10).")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mU\033[0m - disable streaming of new events (log is loaded once without automatic update).")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mR\033[0m - update the current log output manually (relevant in disable streaming mode).")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mQ\033[0m - update all log lists.")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mW\033[0m - enable or disable ANSI coloring for output.")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mN\033[0m - enable or disable coloring via tailspin.")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mD\033[0m - change read mode for docker logs (stream only or json from file system).")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mS\033[0m - change stream display mode for docker logs (all, stdout or stderr only).")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mT\033[0m - enable or disable built-in timestamp and stream type for docker logs.")
fmt.Fprintln(helpView, " \033[32mCtrl\033[0m+\033[32mC\033[0m - clear input text in the filter window or exit.")
fmt.Fprintln(helpView, "\n Supported formats for filtering by timestamp:")
fmt.Fprintln(helpView, "\n "+app.wordColor("00:00"))
fmt.Fprintln(helpView, " "+app.wordColor("00:00:00"))
fmt.Fprintln(helpView, " "+app.wordColor("2025-04-14"))
fmt.Fprintln(helpView, " "+app.wordColor("2025-04-14 00:00"))
fmt.Fprintln(helpView, " "+app.wordColor("2025-04-14 00:00:00"))
fmt.Fprintln(helpView, "\n Examples of short format:")
fmt.Fprintln(helpView, "\n Since \033[35m-\033[34m48h\033[0m until \033[35m-\033[34m24h\033[0m for container logs from journald (logs for the previous day).")
fmt.Fprintln(helpView, " Since \033[35m+\033[34m1h\033[0m until \033[35m+\033[34m30m\033[0m for system journals from docker or podman.")
fmt.Fprintln(helpView, "\n Source code: "+app.wordColor("https://github.com/Lifailon/lazyjournal"))
}
func (app *App) closeHelp(g *gocui.Gui) {
if err := g.DeleteView("help"); err != nil {
return
}
}
// Интерфейс ошибки
func (app *App) showInterfaceInfo(g *gocui.Gui, errInfo bool, text string) {
maxX, maxY := g.Size()
width, height := 50, 3
x0 := (maxX - width) - 5
y0 := (maxY - height) - 2
x1 := x0 + width
y1 := y0 + height
helpView, err := g.SetView("info", x0, y0, x1, y1, 0)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
return
}
if errInfo {
helpView.Title = " Error "
helpView.FrameColor = gocui.ColorRed
helpView.TitleColor = gocui.ColorRed
} else {
helpView.Title = " Info "
helpView.FrameColor = gocui.ColorGreen
helpView.TitleColor = gocui.ColorGreen
}
helpView.Wrap = true
helpView.Clear()
fmt.Fprintln(helpView, text)
}
func (app *App) closeInfo(g *gocui.Gui) {
if err := g.DeleteView("info"); err != nil {
return
}
}
// Функции для переключения количества строк для вывода логов
func (app *App) setCountLogViewUp(g *gocui.Gui, v *gocui.View) error {
switch app.logViewCount {
case "200":
app.logViewCount = "500"
case "500":
app.logViewCount = "1000"
case "1000":
app.logViewCount = "5000"
case "5000":
app.logViewCount = "10000"
case "10000":
app.logViewCount = "20000"
case "20000":
app.logViewCount = "30000"
case "30000":
app.logViewCount = "50000"
case "50000":
app.logViewCount = "100000"
case "100000":
app.logViewCount = "150000"
case "150000":
app.logViewCount = "200000"
case "200000":
app.logViewCount = "200000"
}
// Загружаем журнал заново
app.updateLogOutput(true)
// Обновляем статус
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
return nil
}
func (app *App) setCountLogViewDown(g *gocui.Gui, v *gocui.View) error {
switch app.logViewCount {
case "200000":
app.logViewCount = "150000"
case "150000":
app.logViewCount = "100000"
case "100000":
app.logViewCount = "50000"
case "50000":
app.logViewCount = "30000"
case "30000":
app.logViewCount = "20000"
case "20000":
app.logViewCount = "10000"
case "10000":
app.logViewCount = "5000"
case "5000":
app.logViewCount = "1000"
case "1000":
app.logViewCount = "500"
case "500":
app.logViewCount = "200"
case "200":
app.logViewCount = "200"
}
app.updateLogOutput(true)
vLog, err := app.gui.View("logs")
if err != nil {
return err
}
vLog.Subtitle = fmt.Sprintf("[tail: %s lines | auto-update: %t (%d sec) | docker: %s (%s) | color: %t]", app.logViewCount, app.autoScroll, app.logUpdateSeconds, app.dockerStreamLogsStr, app.dockerStreamMode, app.colorMode)
return nil
}
// Функция для переключения режима фильтрации (вверх)
func (app *App) setFilterModeRight(g *gocui.Gui, v *gocui.View) error {
selectedFilter, err := g.View("filter")
if err != nil {
log.Panicln(err)
}
switch selectedFilter.Title {
case "Filter (Default)":
selectedFilter.Title = "Filter (Fuzzy)"
app.selectFilterMode = "fuzzy"
case "Filter (Fuzzy)":
selectedFilter.Title = "Filter (Regex)"
app.selectFilterMode = "regex"
case "Filter (Regex)":
// Фиксируем название
selectedFilter.Title = "Filter (Timestamp)"
app.selectFilterMode = "timestamp"
// Создаем два новых окна
maxX, _ := g.Size()
leftPanelWidth := maxX / 4
filterWidth := (maxX - leftPanelWidth - 1) / 2
if v, err := g.SetView("sinceFilter", leftPanelWidth+1, 0, leftPanelWidth+1+filterWidth, 2, 0); err != nil {
v.Title = "Since timestamp"
v.Editable = true
v.Wrap = true
// Обработка времени и даты
v.Editor = app.timestampFilterEditor("sinceFilter")
// Изменить цвет окна
v.FrameColor = gocui.ColorGreen
v.TitleColor = gocui.ColorGreen
// Выбираем новое окно
if _, err := g.SetCurrentView("sinceFilter"); err != nil {
return nil
}
// Возобновляет текст из переменной
fmt.Fprint(v, app.sinceFilterText)
// Корректируем позицию курсора
if err = v.SetCursor(len(app.sinceFilterText), 0); err != nil {
return nil
}
}
if v2, err := g.SetView("untilFilter", leftPanelWidth+1+filterWidth+1, 0, maxX-1, 2, 0); err != nil {
v2.Title = "Until timestamp"
v2.Editable = true
v2.Wrap = true
v2.Editor = app.timestampFilterEditor("untilFilter")
fmt.Fprint(v2, app.untilFilterText)
if err = v2.SetCursor(len(app.untilFilterText), 0); err != nil {
return nil
}
}
case "Filter (Timestamp)":
// Удаляем временные два окна
if err = g.DeleteView("sinceFilter"); err != nil {
return nil
}
if err = g.DeleteView("untilFilter"); err != nil {
return nil
}
// Возвращяем фокус и цвет назад
if _, err := g.SetCurrentView("filter"); err != nil {
return nil
}
v.FrameColor = gocui.ColorGreen
v.TitleColor = gocui.ColorGreen
selectedFilter.Title = "Filter (Default)"
app.selectFilterMode = "default"
}
if app.selectFilterMode == "timestamp" {
} else {
app.applyFilter(false)
}
return nil
}
// Функция для переключения режима фильтрации (вниз)
func (app *App) setFilterModeLeft(g *gocui.Gui, v *gocui.View) error {
selectedFilter, err := g.View("filter")
if err != nil {
log.Panicln(err)
}
switch selectedFilter.Title {
case "Filter (Default)":
selectedFilter.Title = "Filter (Timestamp)"
app.selectFilterMode = "timestamp"
maxX, _ := g.Size()
leftPanelWidth := maxX / 4
filterWidth := (maxX - leftPanelWidth - 1) / 2
if v, err := g.SetView("sinceFilter", leftPanelWidth+1, 0, leftPanelWidth+1+filterWidth, 2, 0); err != nil {
v.Title = "Since timestamp"
v.Editable = true
v.Wrap = true
v.Editor = app.timestampFilterEditor("sinceFilter")
v.FrameColor = gocui.ColorGreen
v.TitleColor = gocui.ColorGreen
if _, err := g.SetCurrentView("sinceFilter"); err != nil {
return nil
}
fmt.Fprint(v, app.sinceFilterText)
if err = v.SetCursor(len(app.sinceFilterText), 0); err != nil {
return nil
}
}
if v2, err := g.SetView("untilFilter", leftPanelWidth+1+filterWidth+1, 0, maxX-1, 2, 0); err != nil {
v2.Title = "Until timestamp"
v2.Editable = true
v2.Wrap = true
v2.Editor = app.timestampFilterEditor("untilFilter")
fmt.Fprint(v2, app.untilFilterText)
if err = v2.SetCursor(len(app.untilFilterText), 0); err != nil {
return nil
}
}
case "Filter (Timestamp)":
if err = g.DeleteView("sinceFilter"); err != nil {
return nil
}
if err = g.DeleteView("untilFilter"); err != nil {
return nil
}
if _, err := g.SetCurrentView("filter"); err != nil {
return nil
}
v.FrameColor = gocui.ColorGreen
v.TitleColor = gocui.ColorGreen
selectedFilter.Title = "Filter (Regex)"
app.selectFilterMode = "regex"
case "Filter (Regex)":
selectedFilter.Title = "Filter (Fuzzy)"
app.selectFilterMode = "fuzzy"
case "Filter (Fuzzy)":
selectedFilter.Title = "Filter (Default)"
app.selectFilterMode = "default"
}
return nil
}
// Функции для переключения выбора журналов из journalctl
func (app *App) setUnitListRight(g *gocui.Gui, v *gocui.View) error {
selectedServices, err := g.View("services")
if err != nil {
log.Panicln(err)
}
// Сбрасываем содержимое массива и положение курсора
app.journals = app.journals[:0]
app.startServices = 0
app.selectedJournal = 0
// Меняем журнал и обновляем список
switch app.selectUnits {
case "services":
app.selectUnits = "UNIT"
selectedServices.Title = " < System journals (0) > "
app.loadServices(app.selectUnits)
case "UNIT":
app.selectUnits = "USER_UNIT"
selectedServices.Title = " < User journals (0) > "
app.loadServices(app.selectUnits)
case "USER_UNIT":
app.selectUnits = "kernel"
selectedServices.Title = " < Kernel boot (0) > "
app.loadServices(app.selectUnits)
case "kernel":
app.selectUnits = "auditd"
selectedServices.Title = " < Audit rules keys (0) > "
app.loadServices(app.selectUnits)
case "auditd":
app.selectUnits = "services"
selectedServices.Title = " < Unit list (0) > "
app.loadServices(app.selectUnits)
}
return nil
}
func (app *App) setUnitListLeft(g *gocui.Gui, v *gocui.View) error {
selectedServices, err := g.View("services")
if err != nil {
log.Panicln(err)
}
app.journals = app.journals[:0]
app.startServices = 0
app.selectedJournal = 0
switch app.selectUnits {
case "services":
app.selectUnits = "auditd"
selectedServices.Title = " < Audit rules keys (0) > "
app.loadServices(app.selectUnits)
case "auditd":
app.selectUnits = "kernel"
selectedServices.Title = " < Kernel boot (0) > "
app.loadServices(app.selectUnits)
case "kernel":
app.selectUnits = "USER_UNIT"
selectedServices.Title = " < User journals (0) > "
app.loadServices(app.selectUnits)
case "USER_UNIT":
app.selectUnits = "UNIT"
selectedServices.Title = " < System journals (0) > "
app.loadServices(app.selectUnits)
case "UNIT":
app.selectUnits = "services"
selectedServices.Title = " < Unit list (0) > "
app.loadServices(app.selectUnits)
}
return nil
}
// Функция для переключения выбора журналов файловой системы
func (app *App) setLogFilesListRight(g *gocui.Gui, v *gocui.View) error {
selectedVarLog, err := g.View("varLogs")
if err != nil {
log.Panicln(err)
}
// Добавляем сообщение о загрузке журнала
g.Update(func(g *gocui.Gui) error {
selectedVarLog.Clear()
fmt.Fprintln(selectedVarLog, "Searching log files...")
selectedVarLog.Highlight = false
return nil
})
// Отключаем переключение списков
app.keybindingsEnabled = false
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
// Полсекундная задержка, для корректного обновления интерфейса после выполнения функции
time.Sleep(500 * time.Millisecond)
app.logfiles = app.logfiles[:0]
app.startFiles = 0
app.selectedFile = 0
// Запускаем функцию загрузки журнала в горутине
if app.getOS == "windows" {
go func() {
switch app.selectPath {
case "ProgramFiles":
app.selectPath = "ProgramFiles86"
selectedVarLog.Title = " < Program Files x86 (0) > "
app.loadWinFiles(app.selectPath)
case "ProgramFiles86":
app.selectPath = "ProgramData"
selectedVarLog.Title = " < ProgramData (0) > "
app.loadWinFiles(app.selectPath)
case "ProgramData":
app.selectPath = "AppDataLocal"
selectedVarLog.Title = " < AppData Local (0) > "
app.loadWinFiles(app.selectPath)
case "AppDataLocal":
app.selectPath = "AppDataRoaming"
selectedVarLog.Title = " < AppData Roaming (0) > "
app.loadWinFiles(app.selectPath)
case "AppDataRoaming":
app.selectPath = "ProgramFiles"
selectedVarLog.Title = " < Program Files (0) > "
app.loadWinFiles(app.selectPath)
}
// Включаем переключение списков
app.keybindingsEnabled = true
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
}()
} else {
go func() {
switch app.selectPath {
case "/var/log/":
app.selectPath = "/opt/"
selectedVarLog.Title = " < Optional package logs (0) > "
app.loadFiles(app.selectPath)
case "/opt/":
app.selectPath = "/home/"
selectedVarLog.Title = " < Users home logs (0) > "
app.loadFiles(app.selectPath)
case "/home/":
app.selectPath = "descriptor"
selectedVarLog.Title = " < Process descriptor logs (0) > "
app.loadFiles(app.selectPath)
case "descriptor":
app.selectPath = "/var/log/"
selectedVarLog.Title = " < System var logs (0) > "
app.loadFiles(app.selectPath)
}
// Включаем переключение списков
app.keybindingsEnabled = true
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
}()
}
return nil
}
func (app *App) setLogFilesListLeft(g *gocui.Gui, v *gocui.View) error {
selectedVarLog, err := g.View("varLogs")
if err != nil {
log.Panicln(err)
}
g.Update(func(g *gocui.Gui) error {
selectedVarLog.Clear()
fmt.Fprintln(selectedVarLog, "Searching log files...")
selectedVarLog.Highlight = false
return nil
})
app.keybindingsEnabled = false
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
time.Sleep(500 * time.Millisecond)
app.logfiles = app.logfiles[:0]
app.startFiles = 0
app.selectedFile = 0
if app.getOS == "windows" {
go func() {
switch app.selectPath {
case "ProgramFiles":
app.selectPath = "AppDataRoaming"
selectedVarLog.Title = " < AppData Roaming (0) > "
app.loadWinFiles(app.selectPath)
case "AppDataRoaming":
app.selectPath = "AppDataLocal"
selectedVarLog.Title = " < AppData Local (0) > "
app.loadWinFiles(app.selectPath)
case "AppDataLocal":
app.selectPath = "ProgramData"
selectedVarLog.Title = " < ProgramData (0) > "
app.loadWinFiles(app.selectPath)
case "ProgramData":
app.selectPath = "ProgramFiles86"
selectedVarLog.Title = " < Program Files x86 (0) > "
app.loadWinFiles(app.selectPath)
case "ProgramFiles86":
app.selectPath = "ProgramFiles"
selectedVarLog.Title = " < Program Files (0) > "
app.loadWinFiles(app.selectPath)
}
app.keybindingsEnabled = true
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
}()
} else {
go func() {
switch app.selectPath {
case "/var/log/":
app.selectPath = "descriptor"
selectedVarLog.Title = " < Process descriptor logs (0) > "
app.loadFiles(app.selectPath)
case "descriptor":
app.selectPath = "/home/"
selectedVarLog.Title = " < Users home logs (0) > "
app.loadFiles(app.selectPath)
case "/home/":
app.selectPath = "/opt/"
selectedVarLog.Title = " < Optional package logs (0) > "
app.loadFiles(app.selectPath)
case "/opt/":
app.selectPath = "/var/log/"
selectedVarLog.Title = " < System var logs (0) > "
app.loadFiles(app.selectPath)
}
app.keybindingsEnabled = true
if err := app.setupKeybindings(); err != nil {
log.Panicln("Error key bindings", err)
}
}()
}
return nil
}
// Функция для переключения списков системы контейнеризации
func (app *App) setContainersListRight(g *gocui.Gui, v *gocui.View) error {
selectedDocker, err := g.View("docker")
if err != nil {
log.Panicln(err)
}
app.dockerContainers = app.dockerContainers[:0]
app.startDockerContainers = 0
app.selectedDockerContainer = 0
switch app.selectContainerizationSystem {
case "docker":
app.selectContainerizationSystem = "compose"
selectedDocker.Title = " < Compose stacks (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
case "compose":
app.selectContainerizationSystem = "podman"
selectedDocker.Title = " < Podman containers (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
case "podman":
app.selectContainerizationSystem = "kubectl"
selectedDocker.Title = " < Kubernetes pods (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
case "kubectl":
app.selectContainerizationSystem = "docker"
selectedDocker.Title = " < Docker containers (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
}
return nil
}
func (app *App) setContainersListLeft(g *gocui.Gui, v *gocui.View) error {
selectedDocker, err := g.View("docker")
if err != nil {
log.Panicln(err)
}
app.dockerContainers = app.dockerContainers[:0]
app.startDockerContainers = 0
app.selectedDockerContainer = 0
switch app.selectContainerizationSystem {
case "docker":
app.selectContainerizationSystem = "kubectl"
selectedDocker.Title = " < Kubernetes pods (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
case "kubectl":
app.selectContainerizationSystem = "podman"
selectedDocker.Title = " < Podman containers (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
case "podman":
app.selectContainerizationSystem = "compose"
selectedDocker.Title = " < Compose stacks (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
case "compose":
app.selectContainerizationSystem = "docker"
selectedDocker.Title = " < Docker containers (0) > "
app.loadDockerContainer(app.selectContainerizationSystem)
}
return nil
}
// Функция для переключения окон через Tab
func (app *App) nextView(g *gocui.Gui, v *gocui.View) error {
selectedFilterList, err := g.View("filterList")
if err != nil {
log.Panicln(err)
}
selectedServices, err := g.View("services")
if err != nil {
log.Panicln(err)
}
selectedVarLog, err := g.View("varLogs")
if err != nil {
log.Panicln(err)
}
selectedDocker, err := g.View("docker")
if err != nil {
log.Panicln(err)
}
selectedFilter, err := g.View("filter")
if err != nil {
log.Panicln(err)
}
sinceFilter, err := g.View("sinceFilter")
if err != nil {
app.timestampFilterView = false
} else {
app.timestampFilterView = true
}
untilFilter, err := g.View("untilFilter")
if err != nil {
app.timestampFilterView = false
} else {
app.timestampFilterView = true
}
selectedLogs, err := g.View("logs")
if err != nil {
log.Panicln(err)
}
selectedScrollLogs, err := g.View("scrollLogs")
if err != nil {
log.Panicln(err)
}
currentView := g.CurrentView()
var nextView string
// Начальное окно
if currentView == nil {
nextView = "services"
} else {
switch {
case currentView.Name() == "filterList":
nextView = "services"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = gocui.ColorGreen
selectedServices.TitleColor = gocui.ColorGreen
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "services":
nextView = "varLogs"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = gocui.ColorGreen
selectedVarLog.TitleColor = gocui.ColorGreen
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "varLogs":
nextView = "docker"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = gocui.ColorGreen
selectedDocker.TitleColor = gocui.ColorGreen
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "docker":
if app.timestampFilterView {
nextView = "sinceFilter"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
sinceFilter.FrameColor = gocui.ColorGreen // new
sinceFilter.TitleColor = gocui.ColorGreen // new
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
} else {
nextView = "filter"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorGreen
selectedFilter.TitleColor = gocui.ColorGreen
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
}
case currentView.Name() == "sinceFilter":
nextView = "untilFilter"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
sinceFilter.FrameColor = gocui.ColorDefault // new
sinceFilter.TitleColor = gocui.ColorDefault // new
untilFilter.FrameColor = gocui.ColorGreen // new
untilFilter.TitleColor = gocui.ColorGreen // new
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "filter" || currentView.Name() == "untilFilter":
if app.timestampFilterView {
untilFilter.FrameColor = gocui.ColorDefault // new
untilFilter.TitleColor = gocui.ColorDefault // new
}
nextView = "logs"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorGreen
selectedScrollLogs.FrameColor = gocui.ColorGreen
case currentView.Name() == "logs":
nextView = "filterList"
selectedFilterList.FrameColor = gocui.ColorGreen
selectedFilterList.TitleColor = gocui.ColorGreen
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
}
}
// Устанавливаем новое активное окно
if _, err := g.SetCurrentView(nextView); err != nil {
return err
}
return nil
}
// Функция для переключения окон в обратном порядке через Shift+Tab
func (app *App) backView(g *gocui.Gui, v *gocui.View) error {
selectedFilterList, err := g.View("filterList")
if err != nil {
log.Panicln(err)
}
selectedServices, err := g.View("services")
if err != nil {
log.Panicln(err)
}
selectedVarLog, err := g.View("varLogs")
if err != nil {
log.Panicln(err)
}
selectedDocker, err := g.View("docker")
if err != nil {
log.Panicln(err)
}
selectedFilter, err := g.View("filter")
if err != nil {
log.Panicln(err)
}
sinceFilter, err := g.View("sinceFilter")
if err != nil {
app.timestampFilterView = false
} else {
app.timestampFilterView = true
}
untilFilter, err := g.View("untilFilter")
if err != nil {
app.timestampFilterView = false
} else {
app.timestampFilterView = true
}
selectedLogs, err := g.View("logs")
if err != nil {
log.Panicln(err)
}
selectedScrollLogs, err := g.View("scrollLogs")
if err != nil {
log.Panicln(err)
}
currentView := g.CurrentView()
var nextView string
if currentView == nil {
nextView = "services"
} else {
switch {
case currentView.Name() == "filterList":
nextView = "logs"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorGreen
selectedScrollLogs.FrameColor = gocui.ColorGreen
case currentView.Name() == "services":
nextView = "filterList"
selectedFilterList.FrameColor = gocui.ColorGreen
selectedFilterList.TitleColor = gocui.ColorGreen
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "logs":
if app.timestampFilterView {
nextView = "untilFilter"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
untilFilter.FrameColor = gocui.ColorGreen // new
untilFilter.TitleColor = gocui.ColorGreen // new
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
} else {
nextView = "filter"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorGreen
selectedFilter.TitleColor = gocui.ColorGreen
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
}
case currentView.Name() == "untilFilter":
nextView = "sinceFilter"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
sinceFilter.FrameColor = gocui.ColorGreen // new
sinceFilter.TitleColor = gocui.ColorGreen // new
untilFilter.FrameColor = gocui.ColorDefault // new
untilFilter.TitleColor = gocui.ColorDefault // new
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "filter" || currentView.Name() == "sinceFilter":
if app.timestampFilterView {
sinceFilter.FrameColor = gocui.ColorDefault // new
sinceFilter.TitleColor = gocui.ColorDefault // new
}
nextView = "docker"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = gocui.ColorGreen
selectedDocker.TitleColor = gocui.ColorGreen
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "docker":
nextView = "varLogs"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = app.journalListFrameColor
selectedServices.TitleColor = gocui.ColorDefault
selectedVarLog.FrameColor = gocui.ColorGreen
selectedVarLog.TitleColor = gocui.ColorGreen
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
case currentView.Name() == "varLogs":
nextView = "services"
selectedFilterList.FrameColor = gocui.ColorDefault
selectedFilterList.TitleColor = gocui.ColorDefault
selectedServices.FrameColor = gocui.ColorGreen
selectedServices.TitleColor = gocui.ColorGreen
selectedVarLog.FrameColor = app.fileSystemFrameColor
selectedVarLog.TitleColor = gocui.ColorDefault
selectedDocker.FrameColor = app.dockerFrameColor
selectedDocker.TitleColor = gocui.ColorDefault
selectedFilter.FrameColor = gocui.ColorDefault
selectedFilter.TitleColor = gocui.ColorDefault
selectedLogs.FrameColor = gocui.ColorDefault
selectedScrollLogs.FrameColor = gocui.ColorDefault
}
}
if _, err := g.SetCurrentView(nextView); err != nil {
return err
}
return nil
}
func (app *App) setSelectView(g *gocui.Gui, viewName string) error {
// Сбрасываем цвет всех окон
views := []string{"filterList", "services", "varLogs", "docker", "filter", "sinceFilter", "untilFilter", "logs"}
for _, name := range views {
if v, err := g.View(name); err == nil {
v.FrameColor = gocui.ColorDefault
// Исключение для tail
if name != "logs" {
v.TitleColor = gocui.ColorDefault
}
}
}
// Устанавливаем цвет для активного окна
if v, err := g.View(viewName); err == nil {
v.FrameColor = gocui.ColorGreen
if viewName != "logs" {
v.TitleColor = gocui.ColorGreen
}
}
// Устанавливаем фокус на активное окно
_, err := g.SetCurrentView(viewName)
return err
}
// Функция для выхода
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}