<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>lazyjournal: Go Coverage Report</title> <style> body { background: black; color: rgb(80, 80, 80); } body, pre, #legend span { font-family: Menlo, monospace; font-weight: bold; } #topbar { background: black; position: fixed; top: 0; left: 0; right: 0; height: 42px; border-bottom: 1px solid rgb(80, 80, 80); } #content { margin-top: 50px; } #nav, #legend { float: left; margin-left: 10px; } #legend { margin-top: 12px; } #nav { margin-top: 10px; } #legend span { margin: 0 5px; } .cov0 { color: rgb(192, 0, 0) } .cov1 { color: rgb(128, 128, 128) } .cov2 { color: rgb(116, 140, 131) } .cov3 { color: rgb(104, 152, 134) } .cov4 { color: rgb(92, 164, 137) } .cov5 { color: rgb(80, 176, 140) } .cov6 { color: rgb(68, 188, 143) } .cov7 { color: rgb(56, 200, 146) } .cov8 { color: rgb(44, 212, 149) } .cov9 { color: rgb(32, 224, 152) } .cov10 { color: rgb(20, 236, 155) } </style> </head> <body> <div id="topbar"> <div id="nav"> <select id="files"> <option value="file0">github.com/Lifailon/lazyjournal/main.go (74.9%)</option> </select> </div> <div id="legend"> <span>not tracked</span> <span class="cov0">not covered</span> <span class="cov8">covered</span> </div> </div> <div id="content"> <pre class="file" id="file0" style="display: none">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" ) var programVersion string = "0.7.6" // Структура хранения информации о журналах type Journal struct { name string // название журнала (имя службы) или дата загрузки boot_id string // id загрузки системы } type Logfile struct { name string path string } type DockerContainers struct { name string id string } // Структура основного приложения (графический интерфейс и данные журналов) type App struct { gui *gocui.Gui // графический интерфейс (gocui) testMode bool // исключаем вызовы к gocui при тестирование функций tailSpinMode bool // режим покраски через tailspin colorMode bool // отключение/включение покраски ключевых слов getOS string // название ОС getArch string // архитектура процессора hostName string // текущее имя хоста для покраски в логах userName string // текущее имя пользователя systemDisk string // порядковая буква системного диска для Windows userNameArray []string // список всех пользователей rootDirArray []string // список всех корневых каталогов selectUnits string // название журнала (UNIT/USER_UNIT) selectPath string // путь к логам (/var/log/) selectContainerizationSystem string // название системы контейнеризации (docker/podman/kubernetes) selectFilterMode string // режим фильтрации (default/fuzzy/regex) logViewCount string // количество логов для просмотра (5000) journals []Journal // список (массив/срез) журналов для отображения maxVisibleServices int // максимальное количество видимых элементов в окне списка служб startServices int // индекс первого видимого элемента selectedJournal int // индекс выбранного журнала logfiles []Logfile maxVisibleFiles int startFiles int selectedFile int dockerContainers []DockerContainers maxVisibleDockerContainers int startDockerContainers int selectedDockerContainer int filterListText string // текст для фильтрации список журналов // Массивы для хранения списка журналов без фильтрации journalsNotFilter []Journal logfilesNotFilter []Logfile dockerContainersNotFilter []DockerContainers // Переменные для отслеживания изменений размера окна windowWidth int windowHeight int filterText string // текст для фильтрации записей журнала currentLogLines []string // набор строк (срез) для хранения журнала без фильтрации filteredLogLines []string // набор строк (срез) для хранения журнала после фильтра logScrollPos int // позиция прокрутки для отображаемых строк журнала lastFilterText string // фиксируем содержимое последнего ввода текста для фильтрации autoScroll bool // используется для автоматического скроллинга вниз при обновлении (если это не ручной скроллинг) newUpdateIndex int // фиксируем текущую длинну массива (индекс) для вставки строки обновления (если это ручной выбор из списка) 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 // Фиксируем последнее время загрузки журнала debugLoadTime string // Отключение привязки горячих клавиш на время загрузки списка keybindingsEnabled bool // Регулярные выражения для покраски строк trimHttpRegex *regexp.Regexp trimHttpsRegex *regexp.Regexp trimPrefixPathRegex *regexp.Regexp trimPostfixPathRegex *regexp.Regexp hexByteRegex *regexp.Regexp dateTimeRegex *regexp.Regexp timeMacAddressRegex *regexp.Regexp dateIpAddressRegex *regexp.Regexp dateRegex *regexp.Regexp ipAddressRegex *regexp.Regexp procRegex *regexp.Regexp syslogUnitRegex *regexp.Regexp } func showHelp() <span class="cov8" title="1">{ fmt.Println("lazyjournal - terminal user interface for reading logs from journalctl, file system, Docker and Podman containers, as well 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(" lazyjournal Run interface") fmt.Println(" lazyjournal --help, -h Show help") fmt.Println(" lazyjournal --version, -v Show version") fmt.Println(" lazyjournal --audit, -a Show audit information") }</span> func (app *App) showVersion() <span class="cov8" title="1">{ fmt.Println(programVersion) }</span> func (app *App) showAudit() <span class="cov8" title="1">{ 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 </span><span class="cov8" title="1">{ auditText = append(auditText, " os: "+app.getOS) }</span> else<span class="cov8" title="1"> { var name, version string for _, line := range strings.Split(string(data), "\n") </span><span class="cov8" title="1">{ if strings.HasPrefix(line, "NAME=") </span><span class="cov8" title="1">{ name = strings.Trim(line[5:], "\"") }</span> <span class="cov8" title="1">if strings.HasPrefix(line, "VERSION=") </span><span class="cov8" title="1">{ version = strings.Trim(line[8:], "\"") }</span> } <span class="cov8" title="1">auditText = append(auditText, " os: "+app.getOS+" "+name+" "+version)</span> } <span class="cov8" title="1">auditText = append(auditText, " arch: "+app.getArch) currentUser, _ := user.Current() app.userName = currentUser.Username if strings.Contains(app.userName, "\\") </span><span class="cov8" title="1">{ app.userName = strings.Split(app.userName, "\\")[1] }</span> <span class="cov8" title="1">auditText = append(auditText, " username: "+app.userName) if app.getOS != "windows" </span><span class="cov8" title="1">{ auditText = append(auditText, " privilege: "+(map[bool]string{true: "root", false: "user"})[os.Geteuid() == 0]) }</span> <span class="cov8" title="1">execPath, err := os.Executable() if err == nil </span><span class="cov8" title="1">{ if strings.Contains(execPath, "tmp/go-build") || strings.Contains(execPath, "Temp\\go-build") </span><span class="cov8" title="1">{ auditText = append(auditText, " execType: source code") }</span> else<span class="cov0" title="0"> { auditText = append(auditText, " execType: binary file") }</span> } <span class="cov8" title="1">auditText = append(auditText, " execPath: "+execPath) if app.getOS == "windows" </span><span class="cov8" title="1">{ // Windows Event app.loadWinEvents() auditText = append(auditText, "winEvent:", " logs: ", " - count: "+strconv.Itoa(len(app.journals)), ) // Filesystem if app.userName != "runneradmin" </span><span class="cov0" title="0">{ app.systemDisk = os.Getenv("SystemDrive") if len(app.systemDisk) >= 1 </span><span class="cov0" title="0">{ app.systemDisk = string(app.systemDisk[0]) }</span> else<span class="cov0" title="0"> { app.systemDisk = "C" }</span> <span class="cov0" title="0">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 </span><span class="cov0" title="0">{ // Увеличиваем счетчик горутин wg.Add(1) go func(path struct{ fullPath, path string }) </span><span class="cov0" title="0">{ // Отнимаем счетчик горутин при завершении выполнения горутины defer wg.Done() var fullPath string if strings.HasPrefix(path.fullPath, "Program") </span><span class="cov0" title="0">{ fullPath = "\"" + app.systemDisk + ":/" + path.fullPath + "\"" }</span> else<span class="cov0" title="0"> { fullPath = "\"" + app.systemDisk + ":/Users/" + app.userName + path.fullPath + "\"" }</span> <span class="cov0" title="0">app.loadWinFiles(path.path) lenLogFiles := strconv.Itoa(len(app.logfiles)) // Блокируем доступ на завись в переменную auditText mu.Lock() auditText = append(auditText, " - path: "+fullPath, " count: "+lenLogFiles, ) // Разблокировать мьютекс mu.Unlock()</span> }(path) } // Ожидаем завершения всех горутин <span class="cov0" title="0">wg.Wait()</span> } } else<span class="cov8" title="1"> { // systemd/journald auditText = append(auditText, "systemd:", " journald:", ) csCheck := exec.Command("journalctl", "--version") _, err := csCheck.Output() if err == nil </span><span class="cov8" title="1">{ 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 </span><span class="cov8" title="1">{ app.loadServices(journal.journalName) lenJournals := strconv.Itoa(len(app.journals)) auditText = append(auditText, " - name: "+journal.name, " count: "+lenJournals, ) }</span> } else<span class="cov0" title="0"> { auditText = append(auditText, " - installed: false") }</span> // Filesystem <span class="cov8" title="1">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 </span><span class="cov8" title="1">{ app.loadFiles(path.path) lenLogFiles := strconv.Itoa(len(app.logfiles)) auditText = append(auditText, " - name: "+path.name, " path: "+path.path, " count: "+lenLogFiles, ) }</span> } <span class="cov8" title="1">auditText = append(auditText, "containerization: ", " system: ", ) containerizationSystems := []string{ "docker", "podman", "kubernetes", } for _, cs := range containerizationSystems </span><span class="cov8" title="1">{ auditText = append(auditText, " - name: "+cs) if cs == "kubernetes" </span><span class="cov8" title="1">{ csCheck := exec.Command("kubectl", "version") output, _ := csCheck.Output() // По умолчанию у version код возврата всегда 1, по этому проверяем вывод if strings.Contains(string(output), "Version:") </span><span class="cov8" title="1">{ auditText = append(auditText, " installed: true") // Преобразуем байты в строку и обрезаем пробелы csVersion := strings.TrimSpace(string(output)) // Удаляем текст до номера версии csVersion = strings.Split(csVersion, "Version: ")[1] // Забираем первую строку csVersion = strings.Split(csVersion, "\n")[0] auditText = append(auditText, " version: "+csVersion) cmd := exec.Command( cs, "get", "pods", "-o", "jsonpath={range .items[*]}{.metadata.uid} {.metadata.name} {.status.phase}{'\\n'}{end}", ) _, err := cmd.Output() if err == nil </span><span class="cov0" title="0">{ app.loadDockerContainer(cs) auditText = append(auditText, " pods: "+strconv.Itoa(len(app.dockerContainers))) }</span> else<span class="cov8" title="1"> { auditText = append(auditText, " pods: 0") }</span> } else<span class="cov0" title="0"> { auditText = append(auditText, " installed: false") }</span> } else<span class="cov8" title="1"> { csCheck := exec.Command(cs, "--version") output, err := csCheck.Output() if err == nil </span><span class="cov8" title="1">{ auditText = append(auditText, " installed: true") csVersion := strings.TrimSpace(string(output)) csVersion = strings.Split(csVersion, "version ")[1] auditText = append(auditText, " version: "+csVersion) cmd := exec.Command( cs, "ps", "-a", "--format", "{{.ID}} {{.Names}} {{.State}}", ) _, err := cmd.Output() if err == nil </span><span class="cov8" title="1">{ app.loadDockerContainer(cs) auditText = append(auditText, " containers: "+strconv.Itoa(len(app.dockerContainers))) }</span> else<span class="cov0" title="0"> { auditText = append(auditText, " containers: 0") }</span> } else<span class="cov8" title="1"> { auditText = append(auditText, " installed: false") }</span> } } <span class="cov8" title="1">for _, line := range auditText </span><span class="cov8" title="1">{ fmt.Println(line) }</span> } // Предварительная компиляция регулярных выражений для покраски вывода и их доступности в тестах 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`) // Date: YYYY-MM-DDTHH:MM:SS.MS+HH:MM dateTimeRegex = regexp.MustCompile(`\b(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([\+\-]\d{2}:\d{2})?)\b`) // MAC address + Time: H:MM || HH:MM || HH:MM:SS || XX:XX:XX:XX || XX:XX:XX:XX:XX:XX || XX-XX-XX-XX-XX-XX || HH:MM:SS,XXX || HH:MM:SS.XXX || HH:MM:SS+03 timeMacAddressRegex = regexp.MustCompile(`\b(?:\d{1,2}:\d{2}(:\d{2}([\.\,\+]\d{2,6})?)?|\b(?:[0-9A-Fa-f]{2}[\:\-]){5}[0-9A-Fa-f]{2}\b)\b`) // Date + IP address + version (1.0 || 1.0.7 || 1.0-build) dateIpAddressRegex = regexp.MustCompile(`\b(\d{1,2}[\-\.]\d{1,2}[\-\.]\d{4}|\d{4}[\-\.]\d{1,2}[\-\.]\d{1,2}|(?:\d{1,3}\.){3}\d{1,3}(?::\d+|\.\d+|/\d+)?|\d+\.\d+[\+\-\.\w]+|\d+\.\d+)\b`) // Date: DD-MM-YYYY || DD.MM.YYYY || YYYY-MM-DD || YYYY.MM.DD dateRegex = regexp.MustCompile(`\b(\d{1,2}[\-\.]\d{1,2}[\-\.]\d{4}|\d{4}[\-\.]\d{1,2}[\-.]\d{1,2})\b`) // IP: 255.255.255.255 || 255.255.255.255:443 || 255.255.255.255.443 || 255.255.255.255/24 ipAddressRegex = regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+|\.\d+|/\d+)?\b`) // int% procRegex = regexp.MustCompile(`(\d+)%`) // Syslog UNIT syslogUnitRegex = regexp.MustCompile(`^[a-zA-Z-_.]+\[\d+\]:$`) ) var g *gocui.Gui func runGoCui(mock bool) <span class="cov8" title="1">{ // Инициализация значений по умолчанию + компиляция регулярных выражений для покраски app := &App{ testMode: false, tailSpinMode: false, colorMode: true, startServices: 0, // начальная позиция списка юнитов selectedJournal: 0, // начальный индекс выбранного журнала startFiles: 0, selectedFile: 0, startDockerContainers: 0, selectedDockerContainer: 0, selectUnits: "services", // "UNIT" || "USER_UNIT" || "kernel" selectPath: "/var/log/", // "/opt/", "/home/" или "/Users/" (для MacOS) + /root/ selectContainerizationSystem: "docker", // "podman" || kubernetes selectFilterMode: "default", // "fuzzy" || "regex" logViewCount: "200000", // 5000-300000 journalListFrameColor: gocui.ColorDefault, fileSystemFrameColor: gocui.ColorDefault, dockerFrameColor: gocui.ColorDefault, autoScroll: true, trimHttpRegex: trimHttpRegex, trimHttpsRegex: trimHttpsRegex, trimPrefixPathRegex: trimPrefixPathRegex, trimPostfixPathRegex: trimPostfixPathRegex, hexByteRegex: hexByteRegex, dateTimeRegex: dateTimeRegex, timeMacAddressRegex: timeMacAddressRegex, dateIpAddressRegex: dateIpAddressRegex, dateRegex: dateRegex, ipAddressRegex: ipAddressRegex, procRegex: procRegex, syslogUnitRegex: syslogUnitRegex, keybindingsEnabled: true, } // Определяем используемую ОС (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") audit := flag.Bool("audit", false, "Show audit information") flag.BoolVar(audit, "a", false, "Show audit information") // Обработка аргументов flag.Parse() if *help </span><span class="cov0" title="0">{ showHelp() os.Exit(0) }</span> <span class="cov8" title="1">if *version </span><span class="cov0" title="0">{ app.showVersion() os.Exit(0) }</span> <span class="cov8" title="1">if *audit </span><span class="cov0" title="0">{ app.showAudit() os.Exit(0) }</span> // Создаем GUI <span class="cov8" title="1">var err error if mock </span><span class="cov8" title="1">{ g, err = gocui.NewGui(gocui.OutputSimulator, true) // 1-й параметр для режима работы терминала (tcell) и 2-й параметр для форка }</span> else<span class="cov0" title="0"> { g, err = gocui.NewGui(gocui.OutputNormal, true) }</span> <span class="cov8" title="1">if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> // Закрываем GUI после завершения <span class="cov8" title="1">defer g.Close() app.gui = g // Функция, которая будет вызываться при обновлении интерфейса g.SetManagerFunc(app.layout) // Включить поддержку мыши g.Mouse = false // Цветовая схема GUI g.FgColor = gocui.ColorDefault // поля всех окон и цвет текста g.BgColor = gocui.ColorDefault // фон // Привязка клавиш для работы с интерфейсом из функции setupKeybindings() if err := app.setupKeybindings(); err != nil </span><span class="cov0" title="0">{ log.Panicln("Error key bindings", err) }</span> // Выполняем layout для инициализации интерфейса <span class="cov8" title="1">if err := app.layout(g); err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> // Определяем переменные и массивы для покраски вывода // Текущее имя хоста <span class="cov8" title="1">app.hostName, _ = os.Hostname() // Удаляем доменную часть, если она есть if strings.Contains(app.hostName, ".") </span><span class="cov0" title="0">{ app.hostName = strings.Split(app.hostName, ".")[0] }</span> // Текущее имя пользователя <span class="cov8" title="1">currentUser, _ := user.Current() app.userName = currentUser.Username // Удаляем доменную часть, если она есть if strings.Contains(app.userName, "\\") </span><span class="cov8" title="1">{ app.userName = strings.Split(app.userName, "\\")[1] }</span> // Определяем букву системного диска с установленной ОС Windows <span class="cov8" title="1">app.systemDisk = os.Getenv("SystemDrive") if len(app.systemDisk) >= 1 </span><span class="cov8" title="1">{ app.systemDisk = string(app.systemDisk[0]) }</span> else<span class="cov8" title="1"> { app.systemDisk = "C" }</span> // Имена пользователей <span class="cov8" title="1">passwd, _ := os.Open("/etc/passwd") scanner := bufio.NewScanner(passwd) for scanner.Scan() </span><span class="cov8" title="1">{ line := scanner.Text() userName := strings.Split(line, ":") if len(userName) > 0 </span><span class="cov8" title="1">{ app.userNameArray = append(app.userNameArray, userName[0]) }</span> } // Список корневых каталогов (ls -d /*/) с приставкой "/" <span class="cov8" title="1">files, _ := os.ReadDir("/") for _, file := range files </span><span class="cov8" title="1">{ if file.IsDir() </span><span class="cov8" title="1">{ app.rootDirArray = append(app.rootDirArray, "/"+file.Name()) }</span> } // Фиксируем текущее количество видимых строк в терминале (-1 заголовок) <span class="cov8" title="1">if v, err := g.View("services"); err == nil </span><span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleServices = viewHeight }</span> // Загрузка списка служб или событий Windows <span class="cov8" title="1">if app.getOS == "windows" </span><span class="cov8" title="1">{ v, err := g.View("services") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">v.Title = " < Windows Event Logs (0) > " // Загружаем список событий Windows в горутине go func() </span><span class="cov8" title="1">{ app.loadWinEvents() }</span>() } else<span class="cov8" title="1"> { app.loadServices(app.selectUnits) }</span> // Filesystem <span class="cov8" title="1">if v, err := g.View("varLogs"); err == nil </span><span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleFiles = viewHeight }</span> // Определяем ОС и загружаем файловые журналы <span class="cov8" title="1">if app.getOS == "windows" </span><span class="cov8" title="1">{ selectedVarLog, err := g.View("varLogs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">g.Update(func(g *gocui.Gui) error </span><span class="cov8" title="1">{ selectedVarLog.Clear() fmt.Fprintln(selectedVarLog, "Searching log files...") selectedVarLog.Highlight = false return nil }</span>) <span class="cov8" title="1">selectedVarLog.Title = " < Program Files (0) > " app.selectPath = "ProgramFiles" // Загружаем список файлов Windows в горутине go func() </span><span class="cov8" title="1">{ app.loadWinFiles(app.selectPath) }</span>() } else<span class="cov8" title="1"> { app.loadFiles(app.selectPath) }</span> // Docker <span class="cov8" title="1">if v, err := g.View("docker"); err == nil </span><span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleDockerContainers = viewHeight }</span> <span class="cov8" title="1">app.loadDockerContainer(app.selectContainerizationSystem) // Устанавливаем фокус на окно с журналами по умолчанию if _, err := g.SetCurrentView("filterList"); err != nil </span><span class="cov0" title="0">{ return }</span> // Горутина для автоматического обновления вывода журнала каждые 5 секунд <span class="cov8" title="1">go func() </span><span class="cov8" title="1">{ app.updateLogOutput(5) }</span>() // Горутина для отслеживания изменений размера окна <span class="cov8" title="1">go func() </span><span class="cov8" title="1">{ app.updateWindowSize(1) }</span>() // Запус GUI <span class="cov8" title="1">if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) </span><span class="cov0" title="0">{ log.Panicln(err) }</span> } func main() <span class="cov0" title="0">{ runGoCui(false) }</span> // Структура интерфейса окон GUI func (app *App) layout(g *gocui.Gui) error <span class="cov8" title="1">{ 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 </span><span class="cov8" title="1">{ if !errors.Is(err, gocui.ErrUnknownView) </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">v.Title = "Filtering lists" v.Editable = true v.Wrap = true v.FrameColor = gocui.ColorGreen // Цвет границ окна v.TitleColor = gocui.ColorGreen // Цвет заголовка v.Editor = app.createFilterEditor("lists")</span> } // Окно для отображения списка доступных журналов (UNIT) // Размеры окна: заголовок, отступ слева, отступ сверху, ширина, высота, 5-й параметр из форка для продолжение окна (2) <span class="cov8" title="1">if v, err := g.SetView("services", 0, inputHeight, leftPanelWidth-1, inputHeight+panelHeight-1, 0); err != nil </span><span class="cov8" title="1">{ if !errors.Is(err, gocui.ErrUnknownView) </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">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()</span> // выводим список журналов в это окно } // Окно для списка логов из файловой системы <span class="cov8" title="1">if v, err := g.SetView("varLogs", 0, inputHeight+panelHeight, leftPanelWidth-1, inputHeight+2*panelHeight-1, 0); err != nil </span><span class="cov8" title="1">{ if !errors.Is(err, gocui.ErrUnknownView) </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">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()</span> } // Окно для списка контейнеров Docker и Podman <span class="cov8" title="1">if v, err := g.SetView("docker", 0, inputHeight+2*panelHeight, leftPanelWidth-1, maxY-1, 0); err != nil </span><span class="cov8" title="1">{ if !errors.Is(err, gocui.ErrUnknownView) </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">v.Title = " < Docker containers (0) > " v.Highlight = true v.Wrap = false v.Autoscroll = true v.SelBgColor = gocui.ColorGreen v.SelFgColor = gocui.ColorBlack</span> } // Окно ввода текста для фильтрации <span class="cov8" title="1">if v, err := g.SetView("filter", leftPanelWidth+1, 0, maxX-1, 2, 0); err != nil </span><span class="cov8" title="1">{ if !errors.Is(err, gocui.ErrUnknownView) </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">v.Title = "Filter (Default)" v.Editable = true // включить окно редактируемым для ввода текста v.Editor = app.createFilterEditor("logs") // редактор для обработки ввода v.Wrap = true</span> } // Интерфейс скролла в окне вывода лога (maxX-3 ширина окна - отступ слева) <span class="cov8" title="1">if v, err := g.SetView("scrollLogs", maxX-3, 3, maxX-1, maxY-1, 0); err != nil </span><span class="cov8" title="1">{ if !errors.Is(err, gocui.ErrUnknownView) </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">v.Wrap = true v.Autoscroll = false // Цвет текста (зеленый) v.FgColor = gocui.ColorGreen // Заполняем окно стрелками _, viewHeight := v.Size() fmt.Fprintln(v, "▲") for i := 1; i < viewHeight-1; i++ </span><span class="cov8" title="1">{ fmt.Fprintln(v, " ") }</span> <span class="cov8" title="1">fmt.Fprintln(v, "▼")</span> } // Окно для вывода записей выбранного журнала (maxX-2 для отступа скролла и 8 для продолжения углов) <span class="cov8" title="1">if v, err := g.SetView("logs", leftPanelWidth+1, 3, maxX-1-2, maxY-1, 8); err != nil </span><span class="cov8" title="1">{ if !errors.Is(err, gocui.ErrUnknownView) </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">v.Title = "Logs" v.Wrap = true v.Autoscroll = false</span> } // Включение курсора в режиме фильтра и отключение в остальных окнах <span class="cov8" title="1">currentView := g.CurrentView() if currentView != nil && (currentView.Name() == "filter" || currentView.Name() == "filterList") </span><span class="cov8" title="1">{ g.Cursor = true }</span> else<span class="cov8" title="1"> { g.Cursor = false }</span> <span class="cov8" title="1">return nil</span> } // ---------------------------------------- journalctl/Windows Event Logs ---------------------------------------- // Функция для удаления ANSI-символов покраски func removeANSI(input string) string <span class="cov8" title="1">{ ansiEscapeRegex := regexp.MustCompile(`\033\[[0-9;]*m`) return ansiEscapeRegex.ReplaceAllString(input, "") }</span> // Функция для извлечения даты из строки для списка загрузок ядра func parseDateFromName(name string) time.Time <span class="cov8" title="1">{ cleanName := removeANSI(name) dateFormat := "02.01.2006 15:04:05" // Извлекаем дату, начиная с 22-го символа (после дефиса) parsedDate, _ := time.Parse(dateFormat, cleanName[22:]) return parsedDate }</span> // Функция для загрузки списка журналов служб или загрузок системы из journalctl func (app *App) loadServices(journalName string) <span class="cov8" title="1">{ app.journals = nil // Проверка, что в системе установлен/поддерживается утилита journalctl checkJournald := exec.Command("journalctl", "--version") // Проверяем на ошибки (очищаем список служб, отключаем курсор и выводим ошибку) _, err := checkJournald.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ 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 }</span> <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: systemd-journald not supported") }</span> <span class="cov8" title="1">switch </span>{ case journalName == "services":<span class="cov8" title="1"> // Получаем список всех юнитов в системе через systemctl в формате JSON unitsList := exec.Command("systemctl", "list-units", "--all", "--plain", "--no-legend", "--no-pager", "--output=json") // "--type=service" output, err := unitsList.Output() if !app.testMode </span><span class="cov8" title="1">{ if err != nil </span><span class="cov0" title="0">{ 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 }</span> <span class="cov8" title="1">v, _ := app.gui.View("services") app.journalListFrameColor = gocui.ColorDefault if v.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ v.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">v.Highlight = true</span> } <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: access denied in systemd via systemctl") }</span> // Чтение данных в формате JSON <span class="cov8" title="1">var units []map[string]interface{} err = json.Unmarshal(output, &units) // Если ошибка JSON, создаем массив вручную if err != nil </span><span class="cov0" title="0">{ lines := strings.Split(string(output), "\n") for _, line := range lines </span><span class="cov0" title="0">{ // Разбиваем строку на поля (эквивалентно: awk '{print $1,$3,$4}') fields := strings.Fields(line) // Пропускаем строки с недостаточным количеством полей if len(fields) < 3 </span><span class="cov0" title="0">{ continue</span> } // Заполняем временный массив из строки <span class="cov0" title="0">unit := map[string]interface{}{ "unit": fields[0], "active": fields[2], "sub": fields[3], } // Добавляем временный массив строки в основной массив units = append(units, unit)</span> } } <span class="cov8" title="1">serviceMap := make(map[string]bool) // Обработка записей for _, unit := range units </span><span class="cov8" title="1">{ // Извлечение данных в формате JSON и проверка статуса для покраски unitName, _ := unit["unit"].(string) active, _ := unit["active"].(string) if active == "active" </span><span class="cov8" title="1">{ active = "\033[32m" + active + "\033[0m" }</span> else<span class="cov8" title="1"> { active = "\033[31m" + active + "\033[0m" }</span> <span class="cov8" title="1">sub, _ := unit["sub"].(string) if sub == "exited" || sub == "dead" </span><span class="cov8" title="1">{ sub = "\033[31m" + sub + "\033[0m" }</span> else<span class="cov8" title="1"> { sub = "\033[32m" + sub + "\033[0m" }</span> <span class="cov8" title="1">name := unitName + " (" + active + "/" + sub + ")" bootID := unitName // Уникальный ключ для проверки uniqueKey := name + ":" + bootID if !serviceMap[uniqueKey] </span><span class="cov8" title="1">{ serviceMap[uniqueKey] = true // Добавление записи в массив app.journals = append(app.journals, Journal{ name: name, boot_id: bootID, }) }</span> } case journalName == "kernel":<span class="cov8" title="1"> // Получаем список загрузок системы bootCmd := exec.Command("journalctl", "--list-boots", "-o", "json") bootOutput, err := bootCmd.Output() if !app.testMode </span><span class="cov8" title="1">{ if err != nil </span><span class="cov0" title="0">{ 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 }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("services") app.journalListFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">vError.Highlight = true</span> } } <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: getting boot information from journald") }</span> // Структура для парсинга JSON <span class="cov8" title="1">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 </span><span class="cov8" title="1">{ // Парсим вывод построчно lines := strings.Split(string(bootOutput), "\n") for _, line := range lines </span><span class="cov8" title="1">{ // Разбиваем строку на массив 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 </span><span class="cov0" title="0">{ bootId := wordsArray[1] // Забираем дату, проверяем и изменяем формат var parseDate []string var bootDate string parseDate = strings.Split(wordsArray[3], "-") if len(parseDate) == 3 </span><span class="cov0" title="0">{ bootDate = fmt.Sprintf("%s.%s.%s", parseDate[2], parseDate[1], parseDate[0]) }</span> else<span class="cov0" title="0"> { continue</span> } <span class="cov0" title="0">var stopDate string parseDate = strings.Split(wordsArray[6], "-") if len(parseDate) == 3 </span><span class="cov0" title="0">{ stopDate = fmt.Sprintf("%s.%s.%s", parseDate[2], parseDate[1], parseDate[0]) }</span> else<span class="cov0" title="0"> { continue</span> } // Заполняем массив <span class="cov0" title="0">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, })</span> } } } <span class="cov8" title="1">if err == nil </span><span class="cov8" title="1">{ // Очищаем массив, если он был заполнен в режиме тестирования app.journals = []Journal{} // Добавляем информацию о загрузках в app.journals for _, bootRecord := range bootRecords </span><span class="cov8" title="1">{ // Преобразуем наносекунды в секунды 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, }) }</span> } // Сортируем по второй дате <span class="cov8" title="1">sort.Slice(app.journals, func(i, j int) bool </span><span class="cov8" title="1">{ date1 := parseDateFromName(app.journals[i].name) date2 := parseDateFromName(app.journals[j].name) // Сравниваем по второй дате в обратном порядке (After для сортировки по убыванию) return date1.After(date2) }</span>) default:<span class="cov8" title="1"> cmd := exec.Command("journalctl", "--no-pager", "-F", journalName) output, err := cmd.Output() if !app.testMode </span><span class="cov8" title="1">{ if err != nil </span><span class="cov0" title="0">{ 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 }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("services") app.journalListFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">vError.Highlight = true</span> } } <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: getting services from journald via journalctl") }</span> // Создаем массив (хеш-таблица с доступом по ключу) для уникальных имен служб <span class="cov8" title="1">serviceMap := make(map[string]bool) scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() </span><span class="cov8" title="1">{ serviceName := strings.TrimSpace(scanner.Text()) if serviceName != "" && !serviceMap[serviceName] </span><span class="cov8" title="1">{ serviceMap[serviceName] = true app.journals = append(app.journals, Journal{ name: serviceName, boot_id: "", }) }</span> } // Сортируем список служб по алфавиту <span class="cov8" title="1">sort.Slice(app.journals, func(i, j int) bool </span><span class="cov8" title="1">{ return app.journals[i].name < app.journals[j].name }</span>) } <span class="cov8" title="1">if !app.testMode </span><span class="cov8" title="1">{ // Сохраняем неотфильтрованный список app.journalsNotFilter = app.journals // Применяем фильтр при загрузки и обновляем список служб в интерфейсе через updateServicesList() внутри функции app.applyFilterList() }</span> } // Функция для загрузки списка всех журналов событий Windows через PowerShell func (app *App) loadWinEvents() <span class="cov8" title="1">{ 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 </span><span class="cov8" title="1">{ // Извлечение названия журнала и количество записей 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 </span><span class="cov8" title="1">{ LogView = "\033[33m" + LogViewSplit[0] + "\033[0m" + ": " + "\033[36m" + LogViewSplit[1] + "\033[0m" }</span> else<span class="cov8" title="1"> { LogView = "\033[36m" + LogView + "\033[0m" }</span> <span class="cov8" title="1">LogView = LogView + " (" + RecordCountString + ")" app.journals = append(app.journals, Journal{ name: LogView, boot_id: LogName, })</span> } <span class="cov8" title="1">if !app.testMode </span><span class="cov8" title="1">{ app.journalsNotFilter = app.journals app.applyFilterList() }</span> } // Функция для обновления окна со списком служб func (app *App) updateServicesList() <span class="cov8" title="1">{ // Выбираем окно для заполнения в зависимости от используемого журнала v, err := app.gui.View("services") if err != nil </span><span class="cov0" title="0">{ return }</span> // Очищаем окно <span class="cov8" title="1">v.Clear() // Вычисляем конечную позицию видимой области (стартовая позиция + максимальное количество видимых строк) visibleEnd := app.startServices + app.maxVisibleServices if visibleEnd > len(app.journals) </span><span class="cov8" title="1">{ visibleEnd = len(app.journals) }</span> // Отображаем только элементы в пределах видимой области <span class="cov8" title="1">for i := app.startServices; i < visibleEnd; i++ </span><span class="cov8" title="1">{ fmt.Fprintln(v, app.journals[i].name) }</span> } // Функция для перемещения по списку журналов вниз func (app *App) nextService(v *gocui.View, step int) error <span class="cov8" title="1">{ // Обновляем текущее количество видимых строк в терминале (-1 заголовок) _, viewHeight := v.Size() app.maxVisibleServices = viewHeight // Если список журналов пустой, ничего не делаем if len(app.journals) == 0 </span><span class="cov0" title="0">{ return nil }</span> // Переходим к следующему, если текущий выбранный журнал не последний <span class="cov8" title="1">if app.selectedJournal < len(app.journals)-1 </span><span class="cov8" title="1">{ // Увеличиваем индекс выбранного журнала app.selectedJournal += step // Проверяем, чтобы не выйти за пределы списка if app.selectedJournal >= len(app.journals) </span><span class="cov0" title="0">{ app.selectedJournal = len(app.journals) - 1 }</span> // Проверяем, вышли ли за пределы видимой области (увеличиваем стартовую позицию видимости, только если дошли до 0 + maxVisibleServices) <span class="cov8" title="1">if app.selectedJournal >= app.startServices+app.maxVisibleServices </span><span class="cov8" title="1">{ // Сдвигаем видимую область вниз app.startServices += step // Проверяем, чтобы не выйти за пределы списка if app.startServices > len(app.journals)-app.maxVisibleServices </span><span class="cov0" title="0">{ app.startServices = len(app.journals) - app.maxVisibleServices }</span> // Обновляем отображение списка служб <span class="cov8" title="1">app.updateServicesList()</span> } // Если сдвинули видимую область, корректируем индекс для смещения курсора в интерфейсе <span class="cov8" title="1">if app.selectedJournal < app.startServices+app.maxVisibleServices </span><span class="cov8" title="1">{ // Выбираем журнал по скорректированному индексу return app.selectServiceByIndex(app.selectedJournal - app.startServices) }</span> } <span class="cov0" title="0">return nil</span> } // Функция для перемещения по списку журналов вверх func (app *App) prevService(v *gocui.View, step int) error <span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleServices = viewHeight if len(app.journals) == 0 </span><span class="cov0" title="0">{ return nil }</span> // Переходим к предыдущему, если текущий выбранный журнал не первый <span class="cov8" title="1">if app.selectedJournal > 0 </span><span class="cov8" title="1">{ app.selectedJournal -= step // Если ушли в минус (за начало журнала), приводим к нулю if app.selectedJournal < 0 </span><span class="cov0" title="0">{ app.selectedJournal = 0 }</span> // Проверяем, вышли ли за пределы видимой области <span class="cov8" title="1">if app.selectedJournal < app.startServices </span><span class="cov8" title="1">{ app.startServices -= step if app.startServices < 0 </span><span class="cov0" title="0">{ app.startServices = 0 }</span> <span class="cov8" title="1">app.updateServicesList()</span> } <span class="cov8" title="1">if app.selectedJournal >= app.startServices </span><span class="cov8" title="1">{ return app.selectServiceByIndex(app.selectedJournal - app.startServices) }</span> } <span class="cov0" title="0">return nil</span> } // Функция для визуального выбора журнала по индексу (смещение курсора выделения) func (app *App) selectServiceByIndex(index int) error <span class="cov8" title="1">{ // Получаем доступ к представлению списка служб v, err := app.gui.View("services") if err != nil </span><span class="cov0" title="0">{ return err }</span> // Обновляем счетчик в заголовке <span class="cov8" title="1">re := regexp.MustCompile(`\s\(.+\) >`) updateTitle := " (0) >" if len(app.journals) != 0 </span><span class="cov8" title="1">{ updateTitle = " (" + strconv.Itoa(app.selectedJournal+1) + "/" + strconv.Itoa(len(app.journals)) + ") >" }</span> <span class="cov8" title="1">v.Title = re.ReplaceAllString(v.Title, updateTitle) // Устанавливаем курсор на нужный индекс (строку) // Первый столбец (0), индекс строки if err := v.SetCursor(0, index); err != nil </span><span class="cov0" title="0">{ return nil }</span> <span class="cov8" title="1">return nil</span> } // Функция для выбора журнала в списке сервисов по нажатию Enter func (app *App) selectService(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ // Проверка, что есть доступ к представлению и список журналов не пустой if v == nil || len(app.journals) == 0 </span><span class="cov0" title="0">{ return nil }</span> // Получаем текущую позицию курсора <span class="cov8" title="1">_, cy := v.Cursor() // Читаем строку, на которой находится курсор line, err := v.Line(cy) if err != nil </span><span class="cov0" title="0">{ return err }</span> // Загружаем журналы выбранной службы, обрезая пробелы в названии <span class="cov8" title="1">app.loadJournalLogs(strings.TrimSpace(line), true) // Включаем загрузку журнала (только при ручном выборе для Windows) app.updateFile = true // Фиксируем для ручного или автоматического обновления вывода журнала app.lastWindow = "services" app.lastSelected = strings.TrimSpace(line) return nil</span> } // Функция для загрузки записей журнала выбранной службы через journalctl // Второй параметр для обнолвения позиции делимитра нового вывода лога а также сброса автоскролл func (app *App) loadJournalLogs(serviceName string, newUpdate bool) <span class="cov8" title="1">{ var output []byte var err error selectUnits := app.selectUnits if newUpdate </span><span class="cov8" title="1">{ app.lastSelectUnits = app.selectUnits }</span> else<span class="cov8" title="1"> { selectUnits = app.lastSelectUnits }</span> <span class="cov8" title="1">switch </span>{ // Читаем журналы Windows case app.getOS == "windows":<span class="cov8" title="1"> if !app.updateFile </span><span class="cov8" title="1">{ return }</span> // Отключаем чтение в горутине <span class="cov8" title="1">app.updateFile = false // Извлекаем полное имя события var eventName string for _, journal := range app.journals </span><span class="cov8" title="1">{ journalBootName := removeANSI(journal.name) if journalBootName == serviceName </span><span class="cov8" title="1">{ eventName = journal.boot_id break</span> } } <span class="cov8" title="1">output = app.loadWinEventLog(eventName) if len(output) == 0 && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() return }</span> <span class="cov8" title="1">if len(output) == 0 && app.testMode </span><span class="cov8" title="1">{ app.currentLogLines = []string{} return }</span> // Читаем лог ядра загрузки системы case selectUnits == "kernel":<span class="cov8" title="1"> var boot_id string for _, journal := range app.journals </span><span class="cov8" title="1">{ journalBootName := removeANSI(journal.name) if journalBootName == serviceName </span><span class="cov8" title="1">{ boot_id = journal.boot_id break</span> } } // Сохраняем название для обновления вывода журнала при фильтрации списков <span class="cov8" title="1">if newUpdate </span><span class="cov8" title="1">{ app.lastBootId = boot_id }</span> else<span class="cov0" title="0"> { boot_id = app.lastBootId }</span> <span class="cov8" title="1">cmd := exec.Command("journalctl", "-k", "-b", boot_id, "--no-pager", "-n", app.logViewCount) output, err = cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, "\033[31mError getting kernal logs:", err, "\033[0m") return }</span> <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: getting kernal logs. ", err) }</span> // Для юнитов systemd default:<span class="cov8" title="1"> if selectUnits == "services" </span><span class="cov8" title="1">{ // Удаляем статусы с покраской из навзания var ansiEscape = regexp.MustCompile(`\s\(.+\)`) serviceName = ansiEscape.ReplaceAllString(serviceName, "") }</span> <span class="cov8" title="1">cmd := exec.Command("journalctl", "-u", serviceName, "--no-pager", "-n", app.logViewCount) output, err = cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, "\033[31mError getting journald logs:", err, "\033[0m") return }</span> <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: getting journald logs. ", err) }</span> } // Сохраняем строки журнала в массив <span class="cov8" title="1">app.currentLogLines = strings.Split(string(output), "\n") if !app.testMode </span><span class="cov8" title="1">{ app.updateDelimiter(newUpdate) // Очищаем поле ввода для фильтрации, что бы не применять фильтрацию к новому журналу // app.filterText = "" // Применяем текущий фильтр к записям для обновления вывода app.applyFilter(false) }</span> } // Функция для чтения и парсинга содержимого события Windows через PowerShell (возвращяет текст в формате байт и текст ошибки) // func (app *App) loadWinEventLog(eventName string) (output []byte) { // // Запуск во внешнем процессе PowerShell 5 // cmd := exec.Command("powershell", "-Command", // "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;"+ // "Get-WinEvent -LogName "+eventName+" -MaxEvents 5000 | "+ // "Select-Object TimeCreated,Id,LevelDisplayName,Message | "+ // "Sort-Object TimeCreated | "+ // "ConvertTo-Json") // eventsJson, _ := cmd.Output() // var eventMessage []string // var eventStrings []map[string]interface{} // _ = json.Unmarshal(eventsJson, &eventStrings) // for _, eventString := range eventStrings { // // Извлекаем метку времени из json // TimeCreated, _ := eventString["TimeCreated"].(string) // // Извлекаем метку времени из строки // parts := strings.Split(TimeCreated, "(") // timestampString := strings.Split(parts[1], ")")[0] // // Преобразуем строку в целое число (timestamp) // timestamp, _ := strconv.Atoi(timestampString) // // Преобразуем в Unix-формат (секунды и наносекунды) // dateTime := time.Unix(int64(timestamp/1000), int64((timestamp%1000)*1000000)) // Миллисекунды -> наносекунды // // Извлекаем остальные данные из json // LogId, _ := eventString["Id"].(float64) // LogIdInt := int(LogId) // LogIdString := strconv.Itoa(LogIdInt) // LevelDisplayName, _ := eventString["LevelDisplayName"].(string) // Message, _ := eventString["Message"].(string) // // Удаляем встроенные переносы строки // messageReplace := strings.ReplaceAll(Message, "\r\n", "") // // Формируем строку и заполняем временный массив // mess := dateTime.Format("02.01.2006 15:04:05") + " " + LevelDisplayName + " (" + LogIdString + "): " + messageReplace // eventMessage = append(eventMessage, mess) // } // // Собираем все строки в одну и возвращяем байты // fullMessage := strings.Join(eventMessage, "\n") // return []byte(fullMessage) // } // Функция для чтения и парсинга содержимого события Windows через wevtutil func (app *App) loadWinEventLog(eventName string) (output []byte) <span class="cov8" title="1">{ cmd := exec.Command("cmd", "/C", "chcp 65001 &&"+ "wevtutil qe "+eventName+" /f:text -l:en") eventData, _ := cmd.Output() // Декодирование вывода из Windows-1251 в UTF-8 decoder := charmap.Windows1251.NewDecoder() decodeEventData, decodeErr := decoder.Bytes(eventData) if decodeErr == nil </span><span class="cov8" title="1">{ eventData = decodeEventData }</span> // Разбиваем вывод на массив <span class="cov8" title="1">eventStrings := strings.Split(string(eventData), "Event[") var eventMessage []string for _, eventString := range eventStrings </span><span class="cov8" title="1">{ var dateTime, eventID, level, description string // Разбиваем элемент массива на строки lines := strings.Split(eventString, "\n") // Флаг для обработки последней строки Description с содержимым Message isDescription := false for _, line := range lines </span><span class="cov8" title="1">{ // Удаляем проблемы во всех строках trimmedLine := strings.TrimSpace(line) switch </span>{ // Обновляем формат даты case strings.HasPrefix(trimmedLine, "Date:"):<span class="cov8" title="1"> 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])</span> case strings.HasPrefix(trimmedLine, "Event ID:"):<span class="cov8" title="1"> eventID = strings.ReplaceAll(trimmedLine, "Event ID: ", "")</span> case strings.HasPrefix(trimmedLine, "Level:"):<span class="cov8" title="1"> level = strings.ReplaceAll(trimmedLine, "Level: ", "")</span> case strings.HasPrefix(trimmedLine, "Description:"):<span class="cov8" title="1"> // Фиксируем и пропускаем Description isDescription = true</span> case isDescription:<span class="cov8" title="1"> // Добавляем до конца текущего массива все не пустые строки if trimmedLine != "" </span><span class="cov8" title="1">{ description += "\n" + trimmedLine }</span> } } <span class="cov8" title="1">if dateTime != "" && eventID != "" && level != "" && description != "" </span><span class="cov8" title="1">{ eventMessage = append(eventMessage, fmt.Sprintf("%s %s (%s): %s", dateTime, level, eventID, strings.TrimSpace(description))) }</span> } <span class="cov8" title="1">fullMessage := strings.Join(eventMessage, "\n") return []byte(fullMessage)</span> } // ---------------------------------------- Filesystem ---------------------------------------- func (app *App) loadFiles(logPath string) <span class="cov8" title="1">{ app.logfiles = nil // сбрасываем (очищаем) массив перед загрузкой новых журналов var output []byte switch </span>{ case logPath == "descriptor":<span class="cov8" title="1"> // n - имя файла (путь) // c - имя команды (процесса) 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 </span><span class="cov8" title="1">{ if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="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 }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("varLogs") app.fileSystemFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">vError.Highlight = true</span> } } else<span class="cov8" title="1"> { if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="0">{ log.Print("Error: permission denied (files not found from descriptor)") }</span> } // Очищаем массив перед добавлением отфильтрованных файлов <span class="cov8" title="1">output = []byte{} // Фильтруем строки, которые заканчиваются на ".log" и удаляем префикс (имя файла) for _, file := range files </span><span class="cov8" title="1">{ if strings.HasSuffix(file, ".log") </span><span class="cov8" title="1">{ file = strings.TrimPrefix(file, "n") output = append(output, []byte(file+"\n")...) }</span> } case logPath == "/var/log/":<span class="cov8" title="1"> var cmd *exec.Cmd // Загрузка системных журналов для MacOS if app.getOS == "darwin" </span><span class="cov0" title="0">{ cmd = exec.Command( "find", 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", ) }</span> else<span class="cov8" title="1"> { // Загрузка системных журналов для Linux: все файлы, которые содержат log в расширение или названии (архивы включительно), а также расширение с цифрой (архивные) и pcap/pcapng cmd = exec.Command( "find", 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", ) }</span> <span class="cov8" title="1">output, _ = cmd.Output() // Преобразуем вывод команды в строку и делим на массив строк files := strings.Split(strings.TrimSpace(string(output)), "\n") // Если список файлов пустой, возвращаем ошибку Permission denied if !app.testMode </span><span class="cov8" title="1">{ if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="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 }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("varLogs") app.fileSystemFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">vError.Highlight = true</span> } } else<span class="cov8" title="1"> { if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="0">{ log.Print("Error: files not found in /var/log") }</span> } // Добавляем пути по умолчанию для /var/log <span class="cov8" title="1">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 </span><span class="cov8" title="1">{ output = append([]byte(path), output...) }</span> case logPath == "/opt/":<span class="cov8" title="1"> var cmd *exec.Cmd 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 </span><span class="cov8" title="1">{ if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="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 }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("varLogs") app.fileSystemFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">vError.Highlight = true</span> } } else<span class="cov8" title="1"> { if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="0">{ log.Print("Error: files not found in /opt/") }</span> } default:<span class="cov8" title="1"> // Домашние каталоги пользователей: /home/ для Linux и /Users/ для MacOS if app.getOS == "darwin" </span><span class="cov0" title="0">{ logPath = "/Users/" }</span> // Ищем файлы с помощью системной утилиты find <span class="cov8" title="1">cmd := exec.Command( "find", 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", ")", ) output, _ = cmd.Output() files := strings.Split(strings.TrimSpace(string(output)), "\n") if !app.testMode </span><span class="cov8" title="1">{ if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="0">{ vError, _ := app.gui.View("varLogs") vError.Clear() vError.Highlight = false fmt.Fprintln(vError, "\033[32mFiles not found\033[0m") return }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("varLogs") app.fileSystemFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">vError.Highlight = true</span> } } else<span class="cov8" title="1"> { if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="0">{ log.Print("Error: files not found in home directories") }</span> } // Получаем содержимое файлов из домашнего каталога пользователя root <span class="cov8" title="1">cmdRootDir := exec.Command( "find", "/root/", "-type", "f", "-name", "*.log", "-o", "-name", "*.pcap", "-o", "-name", "*.pcap.gz", "-o", "-name", "*.pcapng", "-o", "-name", "*.pcapng.gz", ) outputRootDir, err := cmdRootDir.Output() // Добавляем содержимое директории /root/ в общий массив, если есть доступ if err == nil </span><span class="cov8" title="1">{ output = append(output, outputRootDir...) }</span> <span class="cov8" title="1">if app.fileSystemFrameColor == gocui.ColorRed && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("varLogs") app.fileSystemFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov0" title="0">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov0" title="0">vError.Highlight = true</span> } } <span class="cov8" title="1">serviceMap := make(map[string]bool) scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() </span><span class="cov8" title="1">{ // Получаем строку полного пути logFullPath := scanner.Text() // Удаляем префикс пути и расширение файла в конце logName := logFullPath if logPath != "descriptor" </span><span class="cov8" title="1">{ logName = strings.TrimPrefix(logFullPath, logPath) }</span> <span class="cov8" title="1">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/" </span><span class="cov8" title="1">{ // Разбиваем строку на слова words := strings.Fields(logName) // Берем первое и последнее слово firstWord := words[0] lastWord := words[len(words)-1] logName = "\x1b[0;33m" + firstWord + "\033[0m" + ": " + lastWord }</span> // Получаем информацию о файле // cmd := exec.Command("bash", "-c", "stat --format='%y' /var/log/apache2/access.log | awk '{print $1}' | awk -F- '{print $3\".\"$2\".\"$1}'") <span class="cov8" title="1">fileInfo, err := os.Stat(logFullPath) if err != nil </span><span class="cov8" title="1">{ // Пропускаем файл, если к нему нет доступа (актуально для статических файлов из logPath) continue</span> } // Проверяем, что файл не пустой <span class="cov8" title="1">if fileInfo.Size() == 0 </span><span class="cov8" title="1">{ // Пропускаем пустой файл continue</span> } // Получаем дату изменения <span class="cov8" title="1">modTime := fileInfo.ModTime() // Форматирование даты в формат DD.MM.YYYY formattedDate := modTime.Format("02.01.2006") // Проверяем, что полного пути до файла еще нет в списке if logName != "" && !serviceMap[logFullPath] </span><span class="cov8" title="1">{ // Добавляем путь в массив для проверки уникальных путей serviceMap[logFullPath] = true // Получаем имя процесса для файла дескриптора if logPath == "descriptor" </span><span class="cov8" title="1">{ cmd := exec.Command("lsof", "-Fc", logFullPath) cmd.Stderr = nil outputLsof, _ := cmd.Output() processLines := strings.Split(strings.TrimSpace(string(outputLsof)), "\n") // Ищем строку, которая содержит имя процесса (только первый процесс) for _, line := range processLines </span><span class="cov8" title="1">{ if strings.HasPrefix(line, "c") </span><span class="cov8" title="1">{ // Удаляем префикс processName := line[1:] logName = "\x1b[0;33m" + processName + "\033[0m" + ": " + logName break</span> } } } // Добавляем в список <span class="cov8" title="1">app.logfiles = append(app.logfiles, Logfile{ name: "[" + "\033[34m" + formattedDate + "\033[0m" + "] " + logName, path: logFullPath, })</span> } } // Сортируем по дате <span class="cov8" title="1">sort.Slice(app.logfiles, func(i, j int) bool </span><span class="cov8" title="1">{ // Извлечение дат из имени layout := "02.01.2006" 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) }</span>) <span class="cov8" title="1">if !app.testMode </span><span class="cov8" title="1">{ app.logfilesNotFilter = app.logfiles app.applyFilterList() }</span> } func (app *App) loadWinFiles(logPath string) <span class="cov8" title="1">{ app.logfiles = nil // Определяем путь по параметру switch </span>{ case logPath == "ProgramFiles":<span class="cov8" title="1"> logPath = app.systemDisk + ":\\Program Files"</span> case logPath == "ProgramFiles86":<span class="cov0" title="0"> logPath = app.systemDisk + ":\\Program Files (x86)"</span> case logPath == "ProgramData":<span class="cov0" title="0"> logPath = app.systemDisk + ":\\ProgramData"</span> case logPath == "AppDataLocal":<span class="cov0" title="0"> logPath = app.systemDisk + ":\\Users\\" + app.userName + "\\AppData\\Local"</span> case logPath == "AppDataRoaming":<span class="cov8" title="1"> logPath = app.systemDisk + ":\\Users\\" + app.userName + "\\AppData\\Roaming"</span> } // Ищем файлы с помощью WalkDir <span class="cov8" title="1">var files []string // Доступ к срезу files из нескольких горутин var mu sync.Mutex // Группа ожидания для отслеживания завершения всех горутин var wg sync.WaitGroup // Получаем список корневых директорий rootDirs, _ := os.ReadDir(logPath) for _, rootDir := range rootDirs </span><span class="cov8" title="1">{ // Проверяем, является ли текущий элемент директорие if rootDir.IsDir() </span><span class="cov8" title="1">{ // Увеличиваем счетчик ожидаемых горутин wg.Add(1) go func(dir string) </span><span class="cov8" title="1">{ // Уменьшаем счетчик горутин после завершения текущей defer wg.Done() // Рекурсивно обходим все файлы и подкаталоги в текущей директории err := filepath.WalkDir(filepath.Join(logPath, dir), func(path string, d os.DirEntry, err error) error </span><span class="cov8" title="1">{ if err != nil </span><span class="cov8" title="1">{ // Игнорируем ошибки, чтобы не прерывать поиск return nil }</span> // Проверяем, что текущий элемент не является директорией и имеет расширение .log <span class="cov8" title="1">if !d.IsDir() && strings.HasSuffix(strings.ToLower(d.Name()), ".log") </span><span class="cov8" title="1">{ // Получаем относительный путь (без корневого пути logPath) relPath, _ := filepath.Rel(logPath, path) // Используем мьютекс для добавления файла в срез mu.Lock() files = append(files, relPath) mu.Unlock() }</span> <span class="cov8" title="1">return nil</span> }) <span class="cov8" title="1">if err != nil </span><span class="cov0" title="0">{ return }</span> }( // Передаем имя текущей директории в горутину rootDir.Name(), ) } } // Ждем завершения всех запущенных горутин <span class="cov8" title="1">wg.Wait() // Объединяем все пути в одну строку, разделенную символом новой строки output := strings.Join(files, "\n") if !app.testMode </span><span class="cov0" title="0">{ // Если список файлов пустой, возвращаем ошибку if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="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 }</span> else<span class="cov0" title="0"> { vError, _ := app.gui.View("varLogs") app.fileSystemFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov0" title="0">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov0" title="0">vError.Highlight = true</span> } } else<span class="cov8" title="1"> { if len(files) == 0 || (len(files) == 1 && files[0] == "") </span><span class="cov0" title="0">{ log.Print("Error: files not found in ", logPath) }</span> } <span class="cov8" title="1">serviceMap := make(map[string]bool) scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() </span><span class="cov8" title="1">{ // Формируем полный путь к файлу logFullPath := logPath + "\\" + scanner.Text() // Формируем имя файла для списка logName := scanner.Text() logName = strings.TrimSuffix(logName, ".log") logName = strings.ReplaceAll(logName, "\\", " ") // Получаем информацию о файле fileInfo, err := os.Stat(logFullPath) // Пропускаем файлы, к которым нет доступа if err != nil </span><span class="cov0" title="0">{ continue</span> } // Пропускаем пустые файлы <span class="cov8" title="1">if fileInfo.Size() == 0 </span><span class="cov0" title="0">{ continue</span> } // Получаем дату изменения <span class="cov8" title="1">modTime := fileInfo.ModTime() // Форматирование даты в формат DD.MM.YYYY formattedDate := modTime.Format("02.01.2006") // Проверяем, что полного пути до файла еще нет в списке if logName != "" && !serviceMap[logFullPath] </span><span class="cov8" title="1">{ // Добавляем путь в массив для проверки уникальных путей serviceMap[logFullPath] = true // Добавляем в список app.logfiles = append(app.logfiles, Logfile{ name: "[" + "\033[34m" + formattedDate + "\033[0m" + "] " + logName, path: logFullPath, }) }</span> } // Сортируем по дате <span class="cov8" title="1">sort.Slice(app.logfiles, func(i, j int) bool </span><span class="cov0" title="0">{ layout := "02.01.2006" dateI, _ := time.Parse(layout, extractDate(app.logfiles[i].name)) dateJ, _ := time.Parse(layout, extractDate(app.logfiles[j].name)) return dateI.After(dateJ) }</span>) <span class="cov8" title="1">if !app.testMode </span><span class="cov0" title="0">{ app.logfilesNotFilter = app.logfiles app.applyFilterList() }</span> } // Функция для извлечения первой втречающейся даты в формате DD.MM.YYYY func extractDate(name string) string <span class="cov8" title="1">{ re := regexp.MustCompile(`\d{2}\.\d{2}\.\d{4}`) return re.FindString(name) }</span> func (app *App) updateLogsList() <span class="cov8" title="1">{ v, err := app.gui.View("varLogs") if err != nil </span><span class="cov0" title="0">{ return }</span> <span class="cov8" title="1">v.Clear() visibleEnd := app.startFiles + app.maxVisibleFiles if visibleEnd > len(app.logfiles) </span><span class="cov8" title="1">{ visibleEnd = len(app.logfiles) }</span> <span class="cov8" title="1">for i := app.startFiles; i < visibleEnd; i++ </span><span class="cov8" title="1">{ fmt.Fprintln(v, app.logfiles[i].name) }</span> } func (app *App) nextFileName(v *gocui.View, step int) error <span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleFiles = viewHeight if len(app.logfiles) == 0 </span><span class="cov8" title="1">{ return nil }</span> <span class="cov8" title="1">if app.selectedFile < len(app.logfiles)-1 </span><span class="cov8" title="1">{ app.selectedFile += step if app.selectedFile >= len(app.logfiles) </span><span class="cov8" title="1">{ app.selectedFile = len(app.logfiles) - 1 }</span> <span class="cov8" title="1">if app.selectedFile >= app.startFiles+app.maxVisibleFiles </span><span class="cov8" title="1">{ app.startFiles += step if app.startFiles > len(app.logfiles)-app.maxVisibleFiles </span><span class="cov8" title="1">{ app.startFiles = len(app.logfiles) - app.maxVisibleFiles }</span> <span class="cov8" title="1">app.updateLogsList()</span> } <span class="cov8" title="1">if app.selectedFile < app.startFiles+app.maxVisibleFiles </span><span class="cov8" title="1">{ return app.selectFileByIndex(app.selectedFile - app.startFiles) }</span> } <span class="cov0" title="0">return nil</span> } func (app *App) prevFileName(v *gocui.View, step int) error <span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleFiles = viewHeight if len(app.logfiles) == 0 </span><span class="cov8" title="1">{ return nil }</span> <span class="cov8" title="1">if app.selectedFile > 0 </span><span class="cov8" title="1">{ app.selectedFile -= step if app.selectedFile < 0 </span><span class="cov8" title="1">{ app.selectedFile = 0 }</span> <span class="cov8" title="1">if app.selectedFile < app.startFiles </span><span class="cov8" title="1">{ app.startFiles -= step if app.startFiles < 0 </span><span class="cov8" title="1">{ app.startFiles = 0 }</span> <span class="cov8" title="1">app.updateLogsList()</span> } <span class="cov8" title="1">if app.selectedFile >= app.startFiles </span><span class="cov8" title="1">{ return app.selectFileByIndex(app.selectedFile - app.startFiles) }</span> } <span class="cov0" title="0">return nil</span> } func (app *App) selectFileByIndex(index int) error <span class="cov8" title="1">{ v, err := app.gui.View("varLogs") if err != nil </span><span class="cov0" title="0">{ return err }</span> // Обновляем счетчик в заголовке <span class="cov8" title="1">re := regexp.MustCompile(`\s\(.+\) >`) updateTitle := " (0) >" if len(app.logfiles) != 0 </span><span class="cov8" title="1">{ updateTitle = " (" + strconv.Itoa(app.selectedFile+1) + "/" + strconv.Itoa(len(app.logfiles)) + ") >" }</span> <span class="cov8" title="1">v.Title = re.ReplaceAllString(v.Title, updateTitle) if err := v.SetCursor(0, index); err != nil </span><span class="cov0" title="0">{ return nil }</span> <span class="cov8" title="1">return nil</span> } func (app *App) selectFile(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ if v == nil || len(app.logfiles) == 0 </span><span class="cov8" title="1">{ return nil }</span> <span class="cov8" title="1">_, cy := v.Cursor() line, err := v.Line(cy) if err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">app.loadFileLogs(strings.TrimSpace(line), true) app.lastWindow = "varLogs" app.lastSelected = strings.TrimSpace(line) return nil</span> } // Функция для чтения файла func (app *App) loadFileLogs(logName string, newUpdate bool) <span class="cov8" title="1">{ // В параметре logName имя файла при выборе возвращяется без символов покраски // Получаем путь из массива по имени var logFullPath string var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) for _, logfile := range app.logfiles </span><span class="cov8" title="1">{ // Удаляем покраску из имени файла в сохраненном массиве logFileName := ansiEscape.ReplaceAllString(logfile.name, "") // Ищем переданное в функцию имя файла и извлекаем путь if logFileName == logName </span><span class="cov8" title="1">{ logFullPath = logfile.path break</span> } } <span class="cov8" title="1">if newUpdate </span><span class="cov8" title="1">{ app.lastLogPath = logFullPath // Фиксируем новую дату изменения и размер для выбранного файла fileInfo, err := os.Stat(logFullPath) if err != nil </span><span class="cov0" title="0">{ return }</span> <span class="cov8" title="1">fileModTime := fileInfo.ModTime() fileSize := fileInfo.Size() app.lastDateUpdateFile = fileModTime app.lastSizeFile = fileSize app.updateFile = true</span> } else<span class="cov8" title="1"> { logFullPath = app.lastLogPath // Проверяем дату изменения fileInfo, err := os.Stat(logFullPath) if err != nil </span><span class="cov0" title="0">{ return }</span> <span class="cov8" title="1">fileModTime := fileInfo.ModTime() fileSize := fileInfo.Size() // Обновлять файл в горутине, только если есть изменения (проверяем дату модификации и размер) if fileModTime != app.lastDateUpdateFile || fileSize != app.lastSizeFile </span><span class="cov0" title="0">{ app.lastDateUpdateFile = fileModTime app.lastSizeFile = fileSize app.updateFile = true }</span> else<span class="cov8" title="1"> { app.updateFile = false }</span> } // Читаем файл, толькое если были изменения <span class="cov8" title="1">if app.updateFile </span><span class="cov8" title="1">{ // Читаем логи в системе Windows if app.getOS == "windows" </span><span class="cov8" title="1">{ decodedOutput, stringErrors := app.loadWinFileLog(logFullPath) if stringErrors != "nil" && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, "\033[31mError", stringErrors, "\033[0m") return }</span> <span class="cov8" title="1">if stringErrors != "nil" && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: ", stringErrors) }</span> <span class="cov8" title="1">app.currentLogLines = strings.Split(string(decodedOutput), "\n")</span> } else<span class="cov8" title="1"> { // Читаем логи в системах UNIX (Linux/Darwin/*BSD) switch </span>{ // Читаем файлы в формате ASL (Apple System Log) case strings.HasSuffix(logFullPath, "asl"):<span class="cov0" title="0"> cmd := exec.Command("syslog", "-f", logFullPath) output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ 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 }</span> <span class="cov0" title="0">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: reading log using syslog tool in ASL (Apple System Log) format. ", err) }</span> <span class="cov0" title="0">app.currentLogLines = strings.Split(string(output), "\n")</span> // Читаем журналы Packet Capture в формате pcap/pcapng case strings.HasSuffix(logFullPath, "pcap") || strings.HasSuffix(logFullPath, "pcapng"):<span class="cov8" title="1"> cmd := exec.Command("tcpdump", "-n", "-r", logFullPath) output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, " \033[31mError reading log using tcpdump tool.\n", err, "\033[0m") return }</span> <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: reading log using tcpdump tool. ", err) }</span> <span class="cov8" title="1">app.currentLogLines = strings.Split(string(output), "\n")</span> // Packet Filter (PF) Firewall OpenBSD case strings.HasSuffix(logFullPath, "pflog"):<span class="cov0" title="0"> cmd := exec.Command("tcpdump", "-e", "-n", "-r", logFullPath) output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, " \033[31mError reading log using tcpdump tool.\n", err, "\033[0m") return }</span> <span class="cov0" title="0">app.currentLogLines = strings.Split(string(output), "\n")</span> // Читаем архивные логи в формате pcap/pcapng (MacOS) case strings.HasSuffix(logFullPath, "pcap.gz") || strings.HasSuffix(logFullPath, "pcapng.gz"):<span class="cov8" title="1"> var unpacker string = "gzip" // Создаем временный файл tmpFile, err := os.CreateTemp("", "temp-*.pcap") if err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError create temp file.\n", err, "\033[0m") return }</span> // Удаляем временный файл после обработки <span class="cov8" title="1">defer os.Remove(tmpFile.Name()) cmdUnzip := exec.Command(unpacker, "-dc", logFullPath) cmdUnzip.Stdout = tmpFile if err := cmdUnzip.Start(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError starting", unpacker, "tool.\n", err, "\033[0m") return }</span> <span class="cov8" title="1">if err := cmdUnzip.Wait(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError decompressing file with", unpacker, "tool.\n", err, "\033[0m") return }</span> // Закрываем временный файл, чтобы tcpdump мог его открыть <span class="cov8" title="1">if err := tmpFile.Close(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError closing temp file.\n", err, "\033[0m") return }</span> // Создаем команду для tcpdump <span class="cov8" title="1">cmdTcpdump := exec.Command("tcpdump", "-n", "-r", tmpFile.Name()) tcpdumpOut, err := cmdTcpdump.StdoutPipe() if err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError creating stdout pipe for tcpdump.\n", err, "\033[0m") return }</span> // Запускаем tcpdump <span class="cov8" title="1">if err := cmdTcpdump.Start(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError starting tcpdump.\n", err, "\033[0m") return }</span> // Читаем вывод tcpdump построчно <span class="cov8" title="1">scanner := bufio.NewScanner(tcpdumpOut) var lines []string for scanner.Scan() </span><span class="cov8" title="1">{ lines = append(lines, scanner.Text()) }</span> <span class="cov8" title="1">if err := scanner.Err(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError reading output from tcpdump.\n", err, "\033[0m") return }</span> // Ожидаем завершения tcpdump <span class="cov8" title="1">if err := cmdTcpdump.Wait(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError finishing tcpdump.\n", err, "\033[0m") return }</span> <span class="cov8" title="1">app.currentLogLines = lines</span> // Читаем архивные логи (unpack + stdout) в формате: gz/xz/bz2 case strings.HasSuffix(logFullPath, ".gz") || strings.HasSuffix(logFullPath, ".xz") || strings.HasSuffix(logFullPath, ".bz2"):<span class="cov8" title="1"> var unpacker string switch </span>{ case strings.HasSuffix(logFullPath, ".gz"):<span class="cov8" title="1"> unpacker = "gzip"</span> case strings.HasSuffix(logFullPath, ".xz"):<span class="cov8" title="1"> unpacker = "xz"</span> case strings.HasSuffix(logFullPath, ".bz2"):<span class="cov0" title="0"> unpacker = "bzip2"</span> } <span class="cov8" title="1">cmdUnzip := exec.Command(unpacker, "-dc", logFullPath) cmdTail := exec.Command("tail", "-n", app.logViewCount) pipe, err := cmdUnzip.StdoutPipe() if err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError creating pipe for", unpacker, "tool.\n", err, "\033[0m") return }</span> // Стандартный вывод программы передаем в stdin tail <span class="cov8" title="1">cmdTail.Stdin = pipe out, err := cmdTail.StdoutPipe() if err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError creating stdout pipe for tail.\n", err, "\033[0m") return }</span> // Запуск команд <span class="cov8" title="1">if err := cmdUnzip.Start(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError starting", unpacker, "tool.\n", err, "\033[0m") return }</span> <span class="cov8" title="1">if err := cmdTail.Start(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError starting tail from", unpacker, "stdout.\n", err, "\033[0m") return }</span> // Чтение вывода <span class="cov8" title="1">output, err := io.ReadAll(out) if err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError reading output from tail.\n", err, "\033[0m") return }</span> // Ожидание завершения команд <span class="cov8" title="1">if err := cmdUnzip.Wait(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError reading archive log using", unpacker, "tool.\n", err, "\033[0m") return }</span> <span class="cov8" title="1">if err := cmdTail.Wait(); err != nil && !app.testMode </span><span class="cov0" title="0">{ vError, _ := app.gui.View("logs") vError.Clear() fmt.Fprintln(vError, " \033[31mError reading log using tail tool.\n", err, "\033[0m") return }</span> // Выводим содержимое <span class="cov8" title="1">app.currentLogLines = strings.Split(string(output), "\n")</span> // Читаем бинарные файлы с помощью last для wtmp, а также utmp (OpenBSD) и utx.log (FreeBSD) case strings.Contains(logFullPath, "wtmp") || strings.Contains(logFullPath, "utmp") || strings.Contains(logFullPath, "utx.log"):<span class="cov8" title="1"> cmd := exec.Command("last", "-f", logFullPath) output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, " \033[31mError reading log using last tool.\n", err, "\033[0m") return }</span> // Разбиваем вывод на строки <span class="cov8" title="1">lines := strings.Split(string(output), "\n") var filteredLines []string // Фильтруем строки, исключая последнюю строку и пустые строки for _, line := range lines </span><span class="cov8" title="1">{ trimmedLine := strings.TrimSpace(line) if trimmedLine != "" && !strings.Contains(trimmedLine, "begins") </span><span class="cov8" title="1">{ filteredLines = append(filteredLines, trimmedLine) }</span> } // Переворачиваем порядок строк <span class="cov8" title="1">for i, j := 0, len(filteredLines)-1; i < j; i, j = i+1, j-1 </span><span class="cov0" title="0">{ filteredLines[i], filteredLines[j] = filteredLines[j], filteredLines[i] }</span> <span class="cov8" title="1">app.currentLogLines = filteredLines</span> // lastb for btmp case strings.Contains(logFullPath, "btmp"):<span class="cov0" title="0"> cmd := exec.Command("lastb", "-f", logFullPath) output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, " \033[31mError reading log using lastb tool.\n", err, "\033[0m") return }</span> <span class="cov0" title="0">lines := strings.Split(string(output), "\n") var filteredLines []string for _, line := range lines </span><span class="cov0" title="0">{ trimmedLine := strings.TrimSpace(line) if trimmedLine != "" && !strings.Contains(trimmedLine, "begins") </span><span class="cov0" title="0">{ filteredLines = append(filteredLines, trimmedLine) }</span> } <span class="cov0" title="0">for i, j := 0, len(filteredLines)-1; i < j; i, j = i+1, j-1 </span><span class="cov0" title="0">{ filteredLines[i], filteredLines[j] = filteredLines[j], filteredLines[i] }</span> <span class="cov0" title="0">app.currentLogLines = filteredLines</span> // Выводим содержимое из команды lastlog case strings.HasSuffix(logFullPath, "lastlog"):<span class="cov0" title="0"> cmd := exec.Command("lastlog") output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, " \033[31mError reading log using lastlog tool.\n", err, "\033[0m") return }</span> <span class="cov0" title="0">app.currentLogLines = strings.Split(string(output), "\n")</span> // lastlogin for FreeBSD case strings.HasSuffix(logFullPath, "lastlogin"):<span class="cov0" title="0"> cmd := exec.Command("lastlogin") output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, " \033[31mError reading log using lastlogin tool.\n", err, "\033[0m") return }</span> <span class="cov0" title="0">app.currentLogLines = strings.Split(string(output), "\n")</span> default:<span class="cov8" title="1"> cmd := exec.Command("tail", "-n", app.logViewCount, logFullPath) output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, " \033[31mError reading log using tail tool.\n", err, "\033[0m") return }</span> <span class="cov8" title="1">app.currentLogLines = strings.Split(string(output), "\n")</span> } } <span class="cov8" title="1">if !app.testMode </span><span class="cov8" title="1">{ app.updateDelimiter(newUpdate) app.applyFilter(false) }</span> } } // Функция для чтения файла с опредилением кодировки в Windows func (app *App) loadWinFileLog(filePath string) (output []byte, stringErrors string) <span class="cov8" title="1">{ // Открываем файл file, err := os.Open(filePath) if err != nil </span><span class="cov0" title="0">{ return nil, fmt.Sprintf("open file: %v", err) }</span> <span class="cov8" title="1">defer file.Close() // Получаем информацию о файле stat, err := file.Stat() if err != nil </span><span class="cov0" title="0">{ return nil, fmt.Sprintf("get file stat: %v", err) }</span> // Получаем размер файла <span class="cov8" title="1">fileSize := stat.Size() // Буфер для хранения последних строк var buffer []byte lineCount := 0 // Размер буфера чтения (читаем по 1КБ за раз) readSize := int64(1024) // Преобразуем строку с максимальным количеством строк в int logViewCountInt, _ := strconv.Atoi(app.logViewCount) // Читаем файл с конца for fileSize > 0 && lineCount < logViewCountInt </span><span class="cov8" title="1">{ if fileSize < readSize </span><span class="cov8" title="1">{ readSize = fileSize }</span> <span class="cov8" title="1">_, err := file.Seek(fileSize-readSize, 0) if err != nil </span><span class="cov0" title="0">{ return nil, fmt.Sprintf("detect the end of a file via seek: %v", err) }</span> <span class="cov8" title="1">tempBuffer := make([]byte, readSize) _, err = file.Read(tempBuffer) if err != nil </span><span class="cov0" title="0">{ return nil, fmt.Sprintf("read file: %v", err) }</span> <span class="cov8" title="1">buffer = append(tempBuffer, buffer...) lineCount = strings.Count(string(buffer), "\n") fileSize -= int64(readSize)</span> } // Проверка на UTF-16 с BOM <span class="cov8" title="1">utf16withBOM := func(data []byte) bool </span><span class="cov8" title="1">{ return len(data) >= 2 && ((data[0] == 0xFF && data[1] == 0xFE) || (data[0] == 0xFE && data[1] == 0xFF)) }</span> // Проверка на UTF-16 LE без BOM <span class="cov8" title="1">utf16withoutBOM := func(data []byte) bool </span><span class="cov8" title="1">{ if len(data)%2 != 0 </span><span class="cov8" title="1">{ return false }</span> <span class="cov0" title="0">for i := 1; i < len(data); i += 2 </span><span class="cov0" title="0">{ if data[i] != 0x00 </span><span class="cov0" title="0">{ return false }</span> } <span class="cov0" title="0">return true</span> } <span class="cov8" title="1">var decodedOutput []byte switch </span>{ case utf16withBOM(buffer):<span class="cov0" title="0"> // Декодируем UTF-16 с BOM decodedOutput, err = unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder().Bytes(buffer) if err != nil </span><span class="cov0" title="0">{ return nil, fmt.Sprintf("decoding from UTF-16 with BOM: %v", err) }</span> case utf16withoutBOM(buffer):<span class="cov0" title="0"> // Декодируем UTF-16 LE без BOM decodedOutput, err = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder().Bytes(buffer) if err != nil </span><span class="cov0" title="0">{ return nil, fmt.Sprintf("decoding from UTF-16 LE without BOM: %v", err) }</span> case utf8.Valid(buffer):<span class="cov8" title="1"> // Декодируем UTF-8 decodedOutput = buffer</span> default:<span class="cov0" title="0"> // Декодируем Windows-1251 decodedOutput, err = charmap.Windows1251.NewDecoder().Bytes(buffer) if err != nil </span><span class="cov0" title="0">{ return nil, fmt.Sprintf("decoding from Windows-1251: %v", err) }</span> } <span class="cov8" title="1">return decodedOutput, "nil"</span> } // ---------------------------------------- Docker/Podman/k8s ---------------------------------------- func (app *App) loadDockerContainer(containerizationSystem string) <span class="cov8" title="1">{ app.dockerContainers = nil // Получаем версию для проверки, что система контейнеризации установлена cmd := exec.Command(containerizationSystem, "version") _, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov8" title="1">{ 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 }</span> <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error:", containerizationSystem+" not installed (environment not found)") }</span> <span class="cov8" title="1">if containerizationSystem == "kubectl" </span><span class="cov0" title="0">{ // Получаем список подов из k8s cmd = exec.Command( containerizationSystem, "get", "pods", "-o", "jsonpath={range .items[*]}{.metadata.uid} {.metadata.name} {.status.phase}{'\\n'}{end}", ) }</span> else<span class="cov8" title="1"> { // Получаем список контейнеров из Docker или Podman cmd = exec.Command( containerizationSystem, "ps", "-a", "--format", "{{.ID}} {{.Names}} {{.State}}", ) }</span> <span class="cov8" title="1">output, err := cmd.Output() if !app.testMode </span><span class="cov8" title="1">{ if err != nil </span><span class="cov0" title="0">{ 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 }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("docker") app.dockerFrameColor = gocui.ColorDefault vError.Highlight = true if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> } } <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: access denied or " + containerizationSystem + " not running") }</span> <span class="cov8" title="1">containers := strings.Split(strings.TrimSpace(string(output)), "\n") // Проверяем, что список контейнеров не пустой if !app.testMode </span><span class="cov8" title="1">{ if len(containers) == 0 || (len(containers) == 1 && containers[0] == "") </span><span class="cov8" title="1">{ vError, _ := app.gui.View("docker") vError.Clear() vError.Highlight = false fmt.Fprintln(vError, "\033[32mNo running containers\033[0m") return }</span> else<span class="cov8" title="1"> { vError, _ := app.gui.View("docker") app.fileSystemFrameColor = gocui.ColorDefault if vError.FrameColor != gocui.ColorDefault </span><span class="cov8" title="1">{ vError.FrameColor = gocui.ColorGreen }</span> <span class="cov8" title="1">vError.Highlight = true</span> } } // Проверяем статус для покраски и заполняем структуру dockerContainers <span class="cov8" title="1">serviceMap := make(map[string]bool) scanner := bufio.NewScanner(strings.NewReader(string(output))) for scanner.Scan() </span><span class="cov8" title="1">{ idName := scanner.Text() parts := strings.Fields(idName) if idName != "" && !serviceMap[idName] </span><span class="cov8" title="1">{ serviceMap[idName] = true containerStatus := parts[2] if containerStatus == "running" || containerStatus == "Running" </span><span class="cov8" title="1">{ containerStatus = "\033[32m" + containerStatus + "\033[0m" }</span> else<span class="cov0" title="0"> { containerStatus = "\033[31m" + containerStatus + "\033[0m" }</span> <span class="cov8" title="1">containerName := parts[1] + " (" + containerStatus + ")" app.dockerContainers = append(app.dockerContainers, DockerContainers{ name: containerName, id: parts[0], })</span> } } <span class="cov8" title="1">sort.Slice(app.dockerContainers, func(i, j int) bool </span><span class="cov8" title="1">{ return app.dockerContainers[i].name < app.dockerContainers[j].name }</span>) <span class="cov8" title="1">if !app.testMode </span><span class="cov8" title="1">{ app.dockerContainersNotFilter = app.dockerContainers app.applyFilterList() }</span> } func (app *App) updateDockerContainerList() <span class="cov8" title="1">{ v, err := app.gui.View("docker") if err != nil </span><span class="cov0" title="0">{ return }</span> <span class="cov8" title="1">v.Clear() visibleEnd := app.startDockerContainers + app.maxVisibleDockerContainers if visibleEnd > len(app.dockerContainers) </span><span class="cov8" title="1">{ visibleEnd = len(app.dockerContainers) }</span> <span class="cov8" title="1">for i := app.startDockerContainers; i < visibleEnd; i++ </span><span class="cov8" title="1">{ fmt.Fprintln(v, app.dockerContainers[i].name) }</span> } func (app *App) nextDockerContainer(v *gocui.View, step int) error <span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleDockerContainers = viewHeight if len(app.dockerContainers) == 0 </span><span class="cov8" title="1">{ return nil }</span> <span class="cov8" title="1">if app.selectedDockerContainer < len(app.dockerContainers)-1 </span><span class="cov8" title="1">{ app.selectedDockerContainer += step if app.selectedDockerContainer >= len(app.dockerContainers) </span><span class="cov8" title="1">{ app.selectedDockerContainer = len(app.dockerContainers) - 1 }</span> <span class="cov8" title="1">if app.selectedDockerContainer >= app.startDockerContainers+app.maxVisibleDockerContainers </span><span class="cov0" title="0">{ app.startDockerContainers += step if app.startDockerContainers > len(app.dockerContainers)-app.maxVisibleDockerContainers </span><span class="cov0" title="0">{ app.startDockerContainers = len(app.dockerContainers) - app.maxVisibleDockerContainers }</span> <span class="cov0" title="0">app.updateDockerContainerList()</span> } <span class="cov8" title="1">if app.selectedDockerContainer < app.startDockerContainers+app.maxVisibleDockerContainers </span><span class="cov8" title="1">{ return app.selectDockerByIndex(app.selectedDockerContainer - app.startDockerContainers) }</span> } <span class="cov0" title="0">return nil</span> } func (app *App) prevDockerContainer(v *gocui.View, step int) error <span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleDockerContainers = viewHeight if len(app.dockerContainers) == 0 </span><span class="cov8" title="1">{ return nil }</span> <span class="cov8" title="1">if app.selectedDockerContainer > 0 </span><span class="cov8" title="1">{ app.selectedDockerContainer -= step if app.selectedDockerContainer < 0 </span><span class="cov8" title="1">{ app.selectedDockerContainer = 0 }</span> <span class="cov8" title="1">if app.selectedDockerContainer < app.startDockerContainers </span><span class="cov0" title="0">{ app.startDockerContainers -= step if app.startDockerContainers < 0 </span><span class="cov0" title="0">{ app.startDockerContainers = 0 }</span> <span class="cov0" title="0">app.updateDockerContainerList()</span> } <span class="cov8" title="1">if app.selectedDockerContainer >= app.startDockerContainers </span><span class="cov8" title="1">{ return app.selectDockerByIndex(app.selectedDockerContainer - app.startDockerContainers) }</span> } <span class="cov0" title="0">return nil</span> } func (app *App) selectDockerByIndex(index int) error <span class="cov8" title="1">{ v, err := app.gui.View("docker") if err != nil </span><span class="cov0" title="0">{ return err }</span> // Обновляем счетчик в заголовке <span class="cov8" title="1">re := regexp.MustCompile(`\s\(.+\) >`) updateTitle := " (0) >" if len(app.dockerContainers) != 0 </span><span class="cov8" title="1">{ updateTitle = " (" + strconv.Itoa(app.selectedDockerContainer+1) + "/" + strconv.Itoa(len(app.dockerContainers)) + ") >" }</span> <span class="cov8" title="1">v.Title = re.ReplaceAllString(v.Title, updateTitle) if err := v.SetCursor(0, index); err != nil </span><span class="cov0" title="0">{ return nil }</span> <span class="cov8" title="1">return nil</span> } func (app *App) selectDocker(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ if v == nil || len(app.dockerContainers) == 0 </span><span class="cov8" title="1">{ return nil }</span> <span class="cov8" title="1">_, cy := v.Cursor() line, err := v.Line(cy) if err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">app.loadDockerLogs(strings.TrimSpace(line), true) app.lastWindow = "docker" app.lastSelected = strings.TrimSpace(line) return nil</span> } func (app *App) loadDockerLogs(containerName string, newUpdate bool) <span class="cov8" title="1">{ containerizationSystem := app.selectContainerizationSystem // Сохраняем систему контейнеризации для автообновления при смене окна if newUpdate </span><span class="cov8" title="1">{ app.lastContainerizationSystem = app.selectContainerizationSystem }</span> else<span class="cov8" title="1"> { containerizationSystem = app.lastContainerizationSystem }</span> <span class="cov8" title="1">var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) var containerId string for _, dockerContainer := range app.dockerContainers </span><span class="cov8" title="1">{ dockerContainerName := ansiEscape.ReplaceAllString(dockerContainer.name, "") if dockerContainerName == containerName </span><span class="cov8" title="1">{ containerId = dockerContainer.id }</span> } // Сохраняем id контейнера для автообновления при смене окна <span class="cov8" title="1">if newUpdate </span><span class="cov8" title="1">{ app.lastContainerId = containerId }</span> else<span class="cov8" title="1"> { containerId = app.lastContainerId }</span> // Читаем локальный лог Docker в формате JSON <span class="cov8" title="1">var readFileContainer bool = false if containerizationSystem == "docker" </span><span class="cov8" title="1">{ basePath := "/var/lib/docker/containers" var logFilePath string // Ищем файл лога в локальной системе по id _ = filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error </span><span class="cov8" title="1">{ if err == nil && strings.Contains(info.Name(), containerId) && strings.HasSuffix(info.Name(), "-json.log") </span><span class="cov8" title="1">{ logFilePath = path // Фиксируем, если найден файловый журнал readFileContainer = true // Останавливаем поиск return filepath.SkipDir }</span> <span class="cov8" title="1">return nil</span> }) // Читаем файл с конца с помощью tail <span class="cov8" title="1">if readFileContainer </span><span class="cov8" title="1">{ cmd := exec.Command("tail", "-n", app.logViewCount, logFilePath) output, err := cmd.Output() if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, "\033[31mError reading log (tail):", err, "\033[0m") return }</span> <span class="cov8" title="1">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: reading log via tail. ", err) }</span> // Разбиваем строки на массив <span class="cov8" title="1">lines := strings.Split(strings.TrimSpace(string(output)), "\n") var formattedLines []string // Обрабатываем вывод в формате JSON построчно for i, line := range lines </span><span class="cov8" title="1">{ // JSON-структура для парсинга var jsonData map[string]interface{} err := json.Unmarshal([]byte(line), &jsonData) if err != nil </span><span class="cov0" title="0">{ continue</span> } // Извлекаем JSON данные <span class="cov8" title="1">stream, _ := jsonData["stream"].(string) timeStr, _ := jsonData["time"].(string) logMessage, _ := jsonData["log"].(string) // Удаляем встроенный экранированный символ переноса строки logMessage = strings.TrimSuffix(logMessage, "\n") // Парсим строку времени в объект time.Time parsedTime, err := time.Parse(time.RFC3339Nano, timeStr) if err == nil </span><span class="cov8" title="1">{ // Форматируем дату в формат: DD:MM:YYYY HH:MM:SS timeStr = parsedTime.Format("02.01.2006 15:04:05") }</span> // Заполняем строку в формате: stream time: log <span class="cov8" title="1">formattedLine := fmt.Sprintf("%s %s: %s", stream, timeStr, logMessage) formattedLines = append(formattedLines, formattedLine) // Если это последняя строка в выводе, добавляем перенос строки if i == len(lines)-1 </span><span class="cov8" title="1">{ formattedLines = append(formattedLines, "\n") }</span> } <span class="cov8" title="1">app.currentLogLines = formattedLines</span> } } // Читаем лог через Docker cli (если файл не найден или к нему нет доступа) или Podman/k8s <span class="cov8" title="1">if !readFileContainer || containerizationSystem == "podman" || containerizationSystem == "kubectl" </span><span class="cov0" title="0">{ // Извлекаем имя без статуса для k8s в containerId if containerizationSystem == "kubectl" </span><span class="cov0" title="0">{ parts := strings.Split(containerName, " (") containerId = parts[0] }</span> <span class="cov0" title="0">cmd := exec.Command(containerizationSystem, "logs", "--tail", app.logViewCount, containerId) output, err := cmd.CombinedOutput() // читаем весь вывод, включая stderr if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("logs") v.Clear() fmt.Fprintln(v, "\033[31mError getting logs from", containerName, "(id:", containerId, ")", "container. Error:", err, "\033[0m") return }</span> <span class="cov0" title="0">if err != nil && app.testMode </span><span class="cov0" title="0">{ log.Print("Error: getting logs from ", containerName, " (id:", containerId, ")", " container. Error: ", err) }</span> <span class="cov0" title="0">app.currentLogLines = strings.Split(string(output), "\n")</span> } <span class="cov8" title="1">if !app.testMode </span><span class="cov8" title="1">{ app.updateDelimiter(newUpdate) app.applyFilter(false) }</span> } // ---------------------------------------- Filter ---------------------------------------- // Редактор обработки ввода текста для фильтрации func (app *App) createFilterEditor(window string) gocui.Editor <span class="cov8" title="1">{ return gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) </span><span class="cov0" title="0">{ switch </span>{ // добавляем символ в поле ввода case ch != 0 && mod == 0:<span class="cov0" title="0"> v.EditWrite(ch)</span> // добавляем пробел case key == gocui.KeySpace:<span class="cov0" title="0"> v.EditWrite(' ')</span> // удаляем символ слева от курсора case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:<span class="cov0" title="0"> v.EditDelete(true)</span> // Удаляем символ справа от курсора case key == gocui.KeyDelete:<span class="cov0" title="0"> v.EditDelete(false)</span> // Перемещение курсора влево case key == gocui.KeyArrowLeft:<span class="cov0" title="0"> v.MoveCursor(-1, 0)</span> // удалить 3-й булевой параметр для форка // Перемещение курсора вправо case key == gocui.KeyArrowRight:<span class="cov0" title="0"> v.MoveCursor(1, 0)</span> } <span class="cov0" title="0">if window == "logs" </span><span class="cov0" title="0">{ // Обновляем текст в буфере app.filterText = strings.TrimSpace(v.Buffer()) // Применяем функцию фильтрации к выводу записей журнала app.applyFilter(true) }</span> else<span class="cov0" title="0"> if window == "lists" </span><span class="cov0" title="0">{ app.filterListText = strings.TrimSpace(v.Buffer()) app.applyFilterList() }</span> }) } // Функция для фильтрации всех списоков журналов func (app *App) applyFilterList() <span class="cov8" title="1">{ filter := strings.ToLower(app.filterListText) // Временные массивы для отфильтрованных журналов var filteredJournals []Journal var filteredLogFiles []Logfile var filteredDockerContainers []DockerContainers for _, j := range app.journalsNotFilter </span><span class="cov8" title="1">{ if strings.Contains(strings.ToLower(j.name), filter) </span><span class="cov8" title="1">{ filteredJournals = append(filteredJournals, j) }</span> } <span class="cov8" title="1">for _, j := range app.logfilesNotFilter </span><span class="cov8" title="1">{ if strings.Contains(strings.ToLower(j.name), filter) </span><span class="cov8" title="1">{ filteredLogFiles = append(filteredLogFiles, j) }</span> } <span class="cov8" title="1">for _, j := range app.dockerContainersNotFilter </span><span class="cov8" title="1">{ if strings.Contains(strings.ToLower(j.name), filter) </span><span class="cov8" title="1">{ filteredDockerContainers = append(filteredDockerContainers, j) }</span> } // Сбрасываем индексы выбранного журнала для правильного позиционирования <span class="cov8" title="1">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 </span><span class="cov8" title="1">{ // Обновляем списки в интерфейсе app.updateServicesList() app.updateLogsList() app.updateDockerContainerList() v, _ := app.gui.View("services") // Обновляем счетчик в заголовке re := regexp.MustCompile(`\s\(.+\) >`) updateTitle := " (0) >" if len(app.journals) != 0 </span><span class="cov8" title="1">{ updateTitle = " (" + strconv.Itoa(app.selectedJournal+1) + "/" + strconv.Itoa(len(app.journals)) + ") >" }</span> <span class="cov8" title="1">v.Title = re.ReplaceAllString(v.Title, updateTitle) // Обновляем статус количества файлов v, _ = app.gui.View("varLogs") // Обновляем счетчик в заголовке re = regexp.MustCompile(`\s\(.+\) >`) updateTitle = " (0) >" if len(app.logfiles) != 0 </span><span class="cov8" title="1">{ updateTitle = " (" + strconv.Itoa(app.selectedFile+1) + "/" + strconv.Itoa(len(app.logfiles)) + ") >" }</span> <span class="cov8" title="1">v.Title = re.ReplaceAllString(v.Title, updateTitle) // Обновляем статус количества контейнеров v, _ = app.gui.View("docker") // Обновляем счетчик в заголовке re = regexp.MustCompile(`\s\(.+\) >`) updateTitle = " (0) >" if len(app.dockerContainers) != 0 </span><span class="cov8" title="1">{ updateTitle = " (" + strconv.Itoa(app.selectedDockerContainer+1) + "/" + strconv.Itoa(len(app.dockerContainers)) + ") >" }</span> <span class="cov8" title="1">v.Title = re.ReplaceAllString(v.Title, updateTitle)</span> } } // Функция для фильтрации записей текущего журнала + покраска func (app *App) applyFilter(color bool) <span class="cov8" title="1">{ filter := app.filterText var skip bool = false var size int var viewHeight int var err error if !app.testMode </span><span class="cov8" title="1">{ v, err := app.gui.View("filter") if err != nil </span><span class="cov0" title="0">{ return }</span> <span class="cov8" title="1">if color </span><span class="cov8" title="1">{ v.FrameColor = gocui.ColorGreen }</span> // Debug: если текст фильтра не менялся и позиция курсора не в самом конце журнала, то пропускаем фильтрацию и покраску при пролистывании <span class="cov8" title="1">vLogs, _ := app.gui.View("logs") _, viewHeight := vLogs.Size() size = app.logScrollPos + viewHeight + 1 if app.lastFilterText == filter && size < len(app.filteredLogLines) </span><span class="cov0" title="0">{ skip = true }</span> // Фиксируем текущий текст из фильтра <span class="cov8" title="1">app.lastFilterText = filter</span> } // Фильтруем и красим, только если это не строллинг <span class="cov8" title="1">if !skip </span><span class="cov8" title="1">{ // Debug start time startTime := time.Now() // Debug: если текст фильтра пустой или равен любому символу для regex, возвращяем вывод без фильтрации if filter == "" || (filter == "." && app.selectFilterMode == "regex") </span><span class="cov8" title="1">{ app.filteredLogLines = app.currentLogLines }</span> else<span class="cov8" title="1"> { app.filteredLogLines = make([]string, 0) // Опускаем регистр ввода текста для фильтра filter = strings.ToLower(filter) // Проверка регулярного выражения var regex *regexp.Regexp if app.selectFilterMode == "regex" </span><span class="cov8" title="1">{ // Добавляем флаг для нечувствительности к регистру по умолчанию filter = "(?i)" + filter // Компилируем регулярное выражение regex, err = regexp.Compile(filter) // В случае синтаксической ошибки регулярного выражения, красим окно красным цветом и завершаем цикл if err != nil && !app.testMode </span><span class="cov0" title="0">{ v, _ := app.gui.View("filter") v.FrameColor = gocui.ColorRed return }</span> <span class="cov8" title="1">if err != nil && !app.testMode </span><span class="cov0" title="0">{ log.Print("Error: regex syntax") return }</span> } // Проходимся по каждой строке <span class="cov8" title="1">for _, line := range app.currentLogLines </span><span class="cov8" title="1">{ switch </span>{ // Fuzzy (неточный поиск без учета регистра) case app.selectFilterMode == "fuzzy":<span class="cov8" title="1"> // Разбиваем текст фильтра на массив из строк filterWords := strings.Fields(filter) // Опускаем регистр текущей строки цикла lineLower := strings.ToLower(line) var match bool = true // Проверяем, если строка не содержит хотя бы одно слово из фильтра, то пропускаем строку for _, word := range filterWords </span><span class="cov8" title="1">{ if !strings.Contains(lineLower, word) </span><span class="cov8" title="1">{ match = false break</span> } } // Если строка подходит под фильтр, возвращаем ее с покраской <span class="cov8" title="1">if match </span><span class="cov8" title="1">{ // Временные символы для обозначения начала и конца покраски найденных символов startColor := "►" endColor := "◄" originalLine := line // Проходимся по всем словосочетаниям фильтра (массив через пробел) для позиционирования покраски for _, word := range filterWords </span><span class="cov8" title="1">{ wordLower := strings.ToLower(word) start := 0 // Ищем все вхождения слова в строке с учетом регистра for </span><span class="cov8" title="1">{ // Находим индекс вхождения с учетом регистра idx := strings.Index(strings.ToLower(originalLine[start:]), wordLower) if idx == -1 </span><span class="cov8" title="1">{ break</span> // Если больше нет вхождений, выходим } <span class="cov8" title="1">start += idx // корректируем индекс с учетом текущей позиции // Вставляем временные символы для покраски originalLine = originalLine[:start] + startColor + originalLine[start:start+len(word)] + endColor + originalLine[start+len(word):] // Сдвигаем индекс для поиска в оставшейся части строки start += len(startColor) + len(word) + len(endColor)</span> } } // Заменяем временные символы на ANSI escape-последовательности <span class="cov8" title="1">originalLine = strings.ReplaceAll(originalLine, startColor, "\x1b[0;44m") originalLine = strings.ReplaceAll(originalLine, endColor, "\033[0m") app.filteredLogLines = append(app.filteredLogLines, originalLine)</span> } // Regex (с использованием регулярных выражений и без учета регистра по умолчанию) case app.selectFilterMode == "regex":<span class="cov8" title="1"> // Проверяем, что строка подходит под регулярное выражение if regex.MatchString(line) </span><span class="cov8" title="1">{ originalLine := line // Находим все найденные совпадени matches := regex.FindAllString(originalLine, -1) // Красим только первое найденное совпадение originalLine = strings.ReplaceAll(originalLine, matches[0], "\x1b[0;44m"+matches[0]+"\033[0m") app.filteredLogLines = append(app.filteredLogLines, originalLine) }</span> // Default (точный поиск с учетом регистра) default:<span class="cov8" title="1"> filter = app.filterText if filter == "" || strings.Contains(line, filter) </span><span class="cov8" title="1">{ lineColor := strings.ReplaceAll(line, filter, "\x1b[0;44m"+filter+"\033[0m") app.filteredLogLines = append(app.filteredLogLines, lineColor) }</span> } } } // Если последняя строка не содержит пустую строку, то добавляем ее <span class="cov8" title="1">if len(app.filteredLogLines) > 0 && app.filteredLogLines[len(app.filteredLogLines)-1] != "" </span><span class="cov8" title="1">{ app.filteredLogLines = append(app.filteredLogLines, "") }</span> // Отключаем покраску в режиме colorMode <span class="cov8" title="1">if app.colorMode </span><span class="cov8" title="1">{ // Режим покраски через tailspin if app.tailSpinMode </span><span class="cov8" title="1">{ cmd := exec.Command("tailspin") logLines := strings.Join(app.filteredLogLines, "\n") // Создаем пайп для передачи данных cmd.Stdin = bytes.NewBufferString(logLines) var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil </span><span class="cov0" title="0">{ fmt.Println(err) }</span> <span class="cov8" title="1">colorLogLines := strings.Split(out.String(), "\n") app.filteredLogLines = colorLogLines</span> } else<span class="cov8" title="1"> { // Максимальное количество потоков const maxWorkers = 10 // Канал для передачи индексов всех строк tasks := make(chan int, len(app.filteredLogLines)) // Срез для хранения обработанных строк colorLogLines := make([]string, len(app.filteredLogLines)) // Объявляем группу ожидания для синхронизации всех горутин (воркеров) var wg sync.WaitGroup // Создаем maxWorkers горутин, где каждая будет обрабатывать задачи из канала tasks for i := 0; i < maxWorkers; i++ </span><span class="cov8" title="1">{ go func() </span><span class="cov8" title="1">{ // Горутина будет работать, пока в канале tasks есть задачи for index := range tasks </span><span class="cov8" title="1">{ // Обрабатываем строку и сохраняем результат по соответствующему индексу colorLogLines[index] = app.lineColor(app.filteredLogLines[index]) // Уменьшаем счетчик задач в группе ожидания. wg.Done() }</span> }() } // Добавляем задачи в канал <span class="cov8" title="1">for i := range app.filteredLogLines </span><span class="cov8" title="1">{ // Увеличиваем счетчик задач в группе ожидания. wg.Add(1) // Передаем индекс строки в канал tasks tasks <- i }</span> // Закрываем канал задач, чтобы воркеры завершили работу после обработки всех задач <span class="cov8" title="1">close(tasks) // Ждем завершения всех задач wg.Wait() app.filteredLogLines = colorLogLines</span> } } // Debug end time <span class="cov8" title="1">endTime := time.Since(startTime) app.debugLoadTime = endTime.Truncate(time.Millisecond).String()</span> } // Debug: корректируем текущую позицию скролла, если размер массива стал меньше <span class="cov8" title="1">if size > len(app.filteredLogLines) </span><span class="cov8" title="1">{ newScrollPos := len(app.filteredLogLines) - viewHeight if newScrollPos > 0 </span><span class="cov8" title="1">{ app.logScrollPos = newScrollPos }</span> else<span class="cov0" title="0"> { app.logScrollPos = 0 }</span> } // Обновляем окно для отображения отфильтрованных записей <span class="cov8" title="1">if !app.testMode </span><span class="cov8" title="1">{ if app.autoScroll </span><span class="cov8" title="1">{ app.logScrollPos = 0 app.updateLogsView(true) }</span> else<span class="cov8" title="1"> { app.updateLogsView(false) }</span> } } // ---------------------------------------- Coloring ---------------------------------------- // Функция для покраски строки func (app *App) lineColor(inputLine string) string <span class="cov8" title="1">{ // Разбиваем строку на слова words := strings.Fields(inputLine) var colorLine string var filterColor bool = false for _, word := range words </span><span class="cov8" title="1">{ // Исключаем строки с покраской при поиске (Background) if strings.Contains(word, "\x1b[0;44m") </span><span class="cov8" title="1">{ filterColor = true }</span> // Красим слово в функции <span class="cov8" title="1">if !filterColor </span><span class="cov8" title="1">{ word = app.wordColor(word) }</span> // Возобновляем покраску <span class="cov8" title="1">if strings.Contains(word, "\033[0m") </span><span class="cov8" title="1">{ filterColor = false }</span> <span class="cov8" title="1">colorLine += word + " "</span> } <span class="cov8" title="1">return strings.TrimSpace(colorLine)</span> } // Игнорируем регистр и проверяем, что слово окружено границами (не буквы и цифры) func (app *App) replaceWordLower(word, keyword, color string) string <span class="cov8" title="1">{ re := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(keyword) + `\b`) return re.ReplaceAllStringFunc(word, func(match string) string </span><span class="cov8" title="1">{ return color + match + "\033[0m" }</span>) } // Поиск пользователей func (app *App) containsUser(searchWord string) bool <span class="cov8" title="1">{ for _, user := range app.userNameArray </span><span class="cov8" title="1">{ if user == searchWord </span><span class="cov8" title="1">{ return true }</span> } <span class="cov8" title="1">return false</span> } // Поиск корневых директорий func (app *App) containsPath(searchWord string) bool <span class="cov8" title="1">{ for _, dir := range app.rootDirArray </span><span class="cov8" title="1">{ if strings.Contains(searchWord, dir) </span><span class="cov8" title="1">{ return true }</span> } <span class="cov8" title="1">return false</span> } // Покраска url путей func (app *App) urlPathColor(cleanedWord string) string <span class="cov8" title="1">{ // Используем Builder для объединения строк var sb strings.Builder // Начинаем с желтого цвета sb.WriteString("\033[33m") for _, char := range cleanedWord </span><span class="cov8" title="1">{ switch </span>{ // Пурпурный цвет для символов и возвращяем желтый case char == '/' || char == '?' || char == '&' || char == '=' || char == ':' || char == '.':<span class="cov8" title="1"> sb.WriteString("\033[35m") sb.WriteRune(char) sb.WriteString("\033[33m")</span> // Синий цвет для цифр // case unicode.IsDigit(char): case char >= '0' && char <= '9':<span class="cov8" title="1"> sb.WriteString("\033[34m") sb.WriteRune(char) sb.WriteString("\033[33m")</span> default:<span class="cov8" title="1"> sb.WriteRune(char)</span> } } // Сброс цвета <span class="cov8" title="1">sb.WriteString("\033[0m") return sb.String()</span> } // Функция для покраски словосочетаний func (app *App) wordColor(inputWord string) string <span class="cov8" title="1">{ // Опускаем регистр слова inputWordLower := strings.ToLower(inputWord) // Значение по умолчанию var coloredWord string = inputWord switch </span>{ // URL case strings.Contains(inputWord, "http://"):<span class="cov8" title="1"> cleanedWord := app.trimHttpRegex.ReplaceAllString(inputWord, "") coloredChars := app.urlPathColor(cleanedWord) // Красный для http coloredWord = strings.ReplaceAll(inputWord, "http://"+cleanedWord, "\033[31mhttp\033[35m://"+coloredChars)</span> case strings.Contains(inputWord, "https://"):<span class="cov8" title="1"> cleanedWord := app.trimHttpsRegex.ReplaceAllString(inputWord, "") coloredChars := app.urlPathColor(cleanedWord) // Зеленый для https coloredWord = strings.ReplaceAll(inputWord, "https://"+cleanedWord, "\033[32mhttps\033[35m://"+coloredChars)</span> // UNIX file paths case app.containsPath(inputWord):<span class="cov8" title="1"> cleanedWord := app.trimPrefixPathRegex.ReplaceAllString(inputWord, "") cleanedWord = app.trimPostfixPathRegex.ReplaceAllString(cleanedWord, "") // Начинаем с желтого цвета coloredChars := "\033[33m" for _, char := range cleanedWord </span><span class="cov8" title="1">{ // Красим символы разделителя путей в пурпурный и возвращяем цвет if char == '/' </span><span class="cov8" title="1">{ coloredChars += "\033[35m" + string(char) + "\033[33m" }</span> else<span class="cov8" title="1"> { coloredChars += string(char) }</span> } <span class="cov8" title="1">coloredWord = strings.ReplaceAll(inputWord, cleanedWord, "\033[35m"+coloredChars+"\033[0m")</span> // Желтый (известные имена: hostname и username) [33m] case strings.Contains(inputWord, app.hostName):<span class="cov8" title="1"> coloredWord = strings.ReplaceAll(inputWord, app.hostName, "\033[33m"+app.hostName+"\033[0m")</span> case strings.Contains(inputWord, app.userName):<span class="cov8" title="1"> coloredWord = strings.ReplaceAll(inputWord, app.userName, "\033[33m"+app.userName+"\033[0m")</span> // Список пользователей из passwd case app.containsUser(inputWord):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, inputWord, "\033[33m")</span> case strings.Contains(inputWordLower, "warn"):<span class="cov8" title="1"> words := []string{"warnings", "warning", "warn"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[33m") break</span> } } // UNIX processes case app.syslogUnitRegex.MatchString(inputWord):<span class="cov8" title="1"> 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"+":")</span> case strings.HasPrefix(inputWordLower, "kernel:"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "kernel", "\033[36m")</span> case strings.HasPrefix(inputWordLower, "rsyslogd:"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "rsyslogd", "\033[36m")</span> case strings.HasPrefix(inputWordLower, "sudo:"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "sudo", "\033[36m")</span> // Исключения case strings.Contains(inputWordLower, "unblock"):<span class="cov8" title="1"> words := []string{"unblocking", "unblocked", "unblock"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } // Красный (ошибки) [31m] case strings.Contains(inputWordLower, "err"):<span class="cov8" title="1"> words := []string{"stderr", "errors", "error", "erro", "err"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "dis"):<span class="cov8" title="1"> words := []string{"disconnected", "disconnection", "disconnects", "disconnect", "disabled", "disabling", "disable"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "crash"):<span class="cov8" title="1"> words := []string{"crashed", "crashing", "crash"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "delet"):<span class="cov8" title="1"> words := []string{"deletion", "deleted", "deleting", "deletes", "delete"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "remov"):<span class="cov8" title="1"> words := []string{"removing", "removed", "removes", "remove"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "stop"):<span class="cov8" title="1"> words := []string{"stopping", "stopped", "stoped", "stops", "stop"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "invalid"):<span class="cov8" title="1"> words := []string{"invalidation", "invalidating", "invalidated", "invalidate", "invalid"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "abort"):<span class="cov8" title="1"> words := []string{"aborted", "aborting", "abort"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "block"):<span class="cov8" title="1"> words := []string{"blocked", "blocker", "blocking", "blocks", "block"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "activ"):<span class="cov8" title="1"> words := []string{"inactive", "deactivated", "deactivating", "deactivate"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "exit"):<span class="cov8" title="1"> words := []string{"exited", "exiting", "exits", "exit"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "crit"):<span class="cov8" title="1"> words := []string{"critical", "critic", "crit"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "fail"):<span class="cov8" title="1"> words := []string{"failed", "failure", "failing", "fails", "fail"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "reject"):<span class="cov8" title="1"> words := []string{"rejecting", "rejection", "rejected", "reject"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "fatal"):<span class="cov8" title="1"> words := []string{"fatality", "fataling", "fatals", "fatal"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "clos"):<span class="cov8" title="1"> words := []string{"closed", "closing", "close"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "drop"):<span class="cov8" title="1"> words := []string{"dropped", "droping", "drops", "drop"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "kill"):<span class="cov8" title="1"> words := []string{"killer", "killing", "kills", "kill"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "cancel"):<span class="cov8" title="1"> words := []string{"cancellation", "cancelation", "canceled", "cancelling", "canceling", "cancel"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "refus"):<span class="cov8" title="1"> words := []string{"refusing", "refused", "refuses", "refuse"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "restrict"):<span class="cov8" title="1"> words := []string{"restricting", "restricted", "restriction", "restrict"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "panic"):<span class="cov8" title="1"> words := []string{"panicked", "panics", "panic"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[31m") break</span> } } case strings.Contains(inputWordLower, "unknown"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "unknown", "\033[31m")</span> case strings.Contains(inputWordLower, "unavailable"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "unavailable", "\033[31m")</span> case strings.Contains(inputWordLower, "unsuccessful"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "unsuccessful", "\033[31m")</span> case strings.Contains(inputWordLower, "found"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "found", "\033[31m")</span> case strings.Contains(inputWordLower, "denied"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "denied", "\033[31m")</span> case strings.Contains(inputWordLower, "conflict"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "conflict", "\033[31m")</span> case strings.Contains(inputWordLower, "false"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "false", "\033[31m")</span> case strings.Contains(inputWordLower, "none"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "none", "\033[31m")</span> case strings.Contains(inputWordLower, "null"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "null", "\033[31m")</span> // Исключения case strings.Contains(inputWordLower, "res"):<span class="cov8" title="1"> words := []string{"resolved", "resolving", "resolve", "restarting", "restarted", "restart"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } // Зеленый (успех) [32m] case strings.Contains(inputWordLower, "succe"):<span class="cov8" title="1"> words := []string{"successfully", "successful", "succeeded", "succeed", "success"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "complet"):<span class="cov8" title="1"> words := []string{"completed", "completing", "completion", "completes", "complete"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "accept"):<span class="cov8" title="1"> words := []string{"accepted", "accepting", "acception", "acceptance", "acceptable", "acceptably", "accepte", "accepts", "accept"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "connect"):<span class="cov8" title="1"> words := []string{"connected", "connecting", "connection", "connects", "connect"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "finish"):<span class="cov8" title="1"> words := []string{"finished", "finishing", "finish"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "start"):<span class="cov8" title="1"> words := []string{"started", "starting", "startup", "start"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "creat"):<span class="cov8" title="1"> words := []string{"created", "creating", "creates", "create"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "enable"):<span class="cov8" title="1"> words := []string{"enabled", "enables", "enable"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "allow"):<span class="cov8" title="1"> words := []string{"allowed", "allowing", "allow"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "post"):<span class="cov8" title="1"> words := []string{"posting", "posted", "postrouting", "post"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "rout"):<span class="cov8" title="1"> words := []string{"prerouting", "routing", "routes", "route"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "forward"):<span class="cov8" title="1"> words := []string{"forwarding", "forwards", "forward"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "pass"):<span class="cov8" title="1"> words := []string{"passed", "passing", "password"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "run"):<span class="cov8" title="1"> words := []string{"running", "runs", "run"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "add"):<span class="cov8" title="1"> words := []string{"added", "add"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "open"):<span class="cov8" title="1"> words := []string{"opening", "opened", "open"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[32m") break</span> } } case strings.Contains(inputWordLower, "ok"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "ok", "\033[32m")</span> case strings.Contains(inputWordLower, "available"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "available", "\033[32m")</span> case strings.Contains(inputWordLower, "accessible"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "accessible", "\033[32m")</span> case strings.Contains(inputWordLower, "done"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "done", "\033[32m")</span> case strings.Contains(inputWordLower, "true"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "true", "\033[32m")</span> // Синий (статусы) [36m] case strings.Contains(inputWordLower, "req"):<span class="cov8" title="1"> words := []string{"requested", "requests", "request"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "reg"):<span class="cov8" title="1"> words := []string{"registered", "registeration"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "boot"):<span class="cov8" title="1"> words := []string{"reboot", "booting", "boot"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "out"):<span class="cov8" title="1"> words := []string{"stdout", "timeout", "output"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "put"):<span class="cov8" title="1"> words := []string{"input", "put"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "get"):<span class="cov8" title="1"> words := []string{"getting", "get"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "set"):<span class="cov8" title="1"> words := []string{"settings", "setting", "setup", "set"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "head"):<span class="cov8" title="1"> words := []string{"headers", "header", "heades", "head"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "log"):<span class="cov8" title="1"> words := []string{"logged", "login"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "load"):<span class="cov8" title="1"> words := []string{"overloading", "overloaded", "overload", "uploading", "uploaded", "uploads", "upload", "downloading", "downloaded", "downloads", "download", "loading", "loaded", "load"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "read"):<span class="cov8" title="1"> words := []string{"reading", "readed", "read"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "patch"):<span class="cov8" title="1"> words := []string{"patching", "patched", "patch"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "up"):<span class="cov8" title="1"> words := []string{"updates", "updated", "updating", "update", "upgrades", "upgraded", "upgrading", "upgrade", "backup", "up"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "listen"):<span class="cov8" title="1"> words := []string{"listening", "listener", "listen"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "launch"):<span class="cov8" title="1"> words := []string{"launched", "launching", "launch"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "chang"):<span class="cov8" title="1"> words := []string{"changed", "changing", "change"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "clea"):<span class="cov8" title="1"> words := []string{"cleaning", "cleaner", "clearing", "cleared", "clear"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "skip"):<span class="cov8" title="1"> words := []string{"skipping", "skipped", "skip"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "miss"):<span class="cov8" title="1"> words := []string{"missing", "missed"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "mount"):<span class="cov8" title="1"> words := []string{"mountpoint", "mounted", "mounting", "mount"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "auth"):<span class="cov8" title="1"> words := []string{"authenticating", "authentication", "authorization"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "conf"):<span class="cov8" title="1"> words := []string{"configurations", "configuration", "configuring", "configured", "configure", "config", "conf"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "option"):<span class="cov8" title="1"> words := []string{"options", "option"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "writ"):<span class="cov8" title="1"> words := []string{"writing", "writed", "write"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "sav"):<span class="cov8" title="1"> words := []string{"saved", "saving", "save"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "paus"):<span class="cov8" title="1"> words := []string{"paused", "pausing", "pause"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "filt"):<span class="cov8" title="1"> words := []string{"filtration", "filtr", "filtering", "filtered", "filter"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "norm"):<span class="cov8" title="1"> words := []string{"normal", "norm"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "noti"):<span class="cov8" title="1"> words := []string{"notifications", "notification", "notify", "noting", "notice"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "alert"):<span class="cov8" title="1"> words := []string{"alerting", "alert"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "in"):<span class="cov8" title="1"> words := []string{"informations", "information", "informing", "informed", "info", "installation", "installed", "installing", "install", "initialization", "initial", "using"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "down"):<span class="cov8" title="1"> words := []string{"shutdown", "down"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "us"):<span class="cov8" title="1"> words := []string{"status", "used", "use"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[36m") break</span> } } case strings.Contains(inputWordLower, "debug"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "debug", "\033[36m")</span> case strings.Contains(inputWordLower, "verbose"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "verbose", "\033[36m")</span> case strings.HasPrefix(inputWordLower, "trace"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "trace", "\033[36m")</span> case strings.HasPrefix(inputWordLower, "protocol"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "protocol", "\033[36m")</span> case strings.Contains(inputWordLower, "level"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "level", "\033[36m")</span> // Голубой (цифры) [34m] // Byte (0x04) case app.hexByteRegex.MatchString(inputWord):<span class="cov8" title="1"> coloredWord = app.hexByteRegex.ReplaceAllStringFunc(inputWord, func(match string) string </span><span class="cov8" title="1">{ colored := "" for _, char := range match </span><span class="cov8" title="1">{ if char == 'x' </span><span class="cov8" title="1">{ colored += "\033[35m" + string(char) + "\033[0m" }</span> else<span class="cov8" title="1"> { colored += "\033[34m" + string(char) + "\033[0m" }</span> } <span class="cov8" title="1">return colored</span> }) // DateTime case app.dateTimeRegex.MatchString(inputWord):<span class="cov8" title="1"> coloredWord = app.dateTimeRegex.ReplaceAllStringFunc(inputWord, func(match string) string </span><span class="cov8" title="1">{ colored := "" for _, char := range match </span><span class="cov8" title="1">{ if char == '-' || char == '.' || char == ':' || char == '+' || char == 'T' </span><span class="cov8" title="1">{ // Пурпурный для символов colored += "\033[35m" + string(char) + "\033[0m" }</span> else<span class="cov8" title="1"> { // Синий для цифр colored += "\033[34m" + string(char) + "\033[0m" }</span> } <span class="cov8" title="1">return colored</span> }) // Time + MAC case app.timeMacAddressRegex.MatchString(inputWord):<span class="cov8" title="1"> coloredWord = app.timeMacAddressRegex.ReplaceAllStringFunc(inputWord, func(match string) string </span><span class="cov8" title="1">{ colored := "" for _, char := range match </span><span class="cov8" title="1">{ if char == '-' || char == ':' || char == '.' || char == ',' || char == '+' </span><span class="cov8" title="1">{ colored += "\033[35m" + string(char) + "\033[0m" }</span> else<span class="cov8" title="1"> { colored += "\033[34m" + string(char) + "\033[0m" }</span> } <span class="cov8" title="1">return colored</span> }) // Date + IP case app.dateIpAddressRegex.MatchString(inputWord):<span class="cov8" title="1"> coloredWord = app.dateIpAddressRegex.ReplaceAllStringFunc(inputWord, func(match string) string </span><span class="cov8" title="1">{ colored := "" for _, char := range match </span><span class="cov8" title="1">{ if char == '.' || char == ':' || char == '-' || char == '+' </span><span class="cov8" title="1">{ colored += "\033[35m" + string(char) + "\033[0m" }</span> else<span class="cov8" title="1"> { colored += "\033[34m" + string(char) + "\033[0m" }</span> } <span class="cov8" title="1">return colored</span> }) // Percentage (100%) case strings.Contains(inputWordLower, "%"):<span class="cov8" title="1"> coloredWord = app.procRegex.ReplaceAllStringFunc(inputWord, func(match string) string </span><span class="cov8" title="1">{ colored := "" for _, char := range match </span><span class="cov8" title="1">{ if char == '%' </span><span class="cov8" title="1">{ colored += "\033[35m" + string(char) + "\033[0m" }</span> else<span class="cov8" title="1"> { colored += "\033[34m" + string(char) + "\033[0m" }</span> } <span class="cov8" title="1">return colored</span> }) // tcpdump case strings.Contains(inputWordLower, "tcp"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "tcp", "\033[33m")</span> case strings.Contains(inputWordLower, "udp"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "udp", "\033[33m")</span> case strings.Contains(inputWordLower, "icmp"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "icmp", "\033[33m")</span> case strings.Contains(inputWordLower, "ip"):<span class="cov8" title="1"> words := []string{"ip4", "ipv4", "ip6", "ipv6", "ip"} for _, word := range words </span><span class="cov8" title="1">{ if strings.Contains(inputWordLower, word) </span><span class="cov8" title="1">{ coloredWord = app.replaceWordLower(inputWord, word, "\033[33m") break</span> } } // Update delimiter case strings.Contains(inputWord, "⎯"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "⎯", "\033[32m")</span> // Исключения case strings.Contains(inputWordLower, "not"):<span class="cov8" title="1"> coloredWord = app.replaceWordLower(inputWord, "not", "\033[31m")</span> } <span class="cov8" title="1">return coloredWord</span> } // ---------------------------------------- Log output ---------------------------------------- // Функция для обновления вывода журнала (параметр для прокрутки в самый вниз) func (app *App) updateLogsView(lowerDown bool) <span class="cov8" title="1">{ // Получаем доступ к выводу журнала v, err := app.gui.View("logs") if err != nil </span><span class="cov0" title="0">{ return }</span> // Очищаем окно для отображения новых строк <span class="cov8" title="1">v.Clear() // Получаем ширину и высоту окна viewWidth, viewHeight := v.Size() // Опускаем в самый низ, только если это не ручной скролл (отключается параметром) if lowerDown </span><span class="cov8" title="1">{ // Если количество строк больше высоты окна, опускаем в самый низ if len(app.filteredLogLines) > viewHeight-1 </span><span class="cov0" title="0">{ app.logScrollPos = len(app.filteredLogLines) - viewHeight - 1 }</span> else<span class="cov8" title="1"> { app.logScrollPos = 0 }</span> } // Определяем количество строк для отображения, начиная с позиции logScrollPos <span class="cov8" title="1">startLine := app.logScrollPos endLine := startLine + viewHeight if endLine > len(app.filteredLogLines) </span><span class="cov8" title="1">{ endLine = len(app.filteredLogLines) }</span> // Учитываем auto wrap (только в конце лога) <span class="cov8" title="1">if app.logScrollPos == len(app.filteredLogLines)-viewHeight-1 </span><span class="cov0" title="0">{ var viewLines int = 0 // количество строк для вывода var viewCounter int = 0 // обратный счетчик видимых строк для остановки var viewIndex int = len(app.filteredLogLines) - 1 // начальный индекс для строк с конца for </span><span class="cov0" title="0">{ // Фиксируем текущую входную строку и счетчик viewLines += 1 viewCounter += 1 // Получаем длинну видимых символов в строке с конца var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) lengthLine := len([]rune(ansiEscape.ReplaceAllString(app.filteredLogLines[viewIndex], ""))) // Если длинна строки больше ширины окна, получаем количество дополнительных строк if lengthLine > viewWidth </span><span class="cov0" title="0">{ // Увеличивая счетчик и пропускаем строки viewCounter += lengthLine / viewWidth }</span> // Если счетчик привысил количество видимых строк, вычетаем последнюю строку из видимости <span class="cov0" title="0">if viewCounter > viewHeight </span><span class="cov0" title="0">{ viewLines -= 1 }</span> <span class="cov0" title="0">if viewCounter >= viewHeight </span><span class="cov0" title="0">{ break</span> } // Уменьшаем индекс <span class="cov0" title="0">viewIndex -= 1</span> } <span class="cov0" title="0">for i := len(app.filteredLogLines) - viewLines - 1; i < endLine; i++ </span><span class="cov0" title="0">{ fmt.Fprintln(v, app.filteredLogLines[i]) }</span> } else<span class="cov8" title="1"> { // Проходим по отфильтрованным строкам и выводим их for i := startLine; i < endLine; i++ </span><span class="cov8" title="1">{ fmt.Fprintln(v, app.filteredLogLines[i]) }</span> } // Вычисляем процент прокрутки и обновляем заголовок <span class="cov8" title="1">var percentage int = 0 if len(app.filteredLogLines) > 0 </span><span class="cov8" title="1">{ // Стартовая позиция + размер текущего вывода логов и округляем в большую сторону (math) percentage = int(math.Ceil(float64((startLine+viewHeight)*100) / float64(len(app.filteredLogLines)))) if percentage > 100 </span><span class="cov8" title="1">{ v.Title = fmt.Sprintf("Logs: 100%% (%d) ["+app.debugLoadTime+"]", len(app.filteredLogLines)) // "Logs: 100%% (%d) [Max lines: "+app.logViewCount+"/Load time: "+app.debugLoadTime+"]" }</span> else<span class="cov0" title="0"> { v.Title = fmt.Sprintf("Logs: %d%% (%d/%d) ["+app.debugLoadTime+"]", percentage, startLine+1+viewHeight, len(app.filteredLogLines)) }</span> } else<span class="cov8" title="1"> { v.Title = "Logs: 0% (0) [" + app.debugLoadTime + "]" }</span> <span class="cov8" title="1">app.viewScrollLogs(percentage)</span> } // Функция для обновления интерфейса скроллинга func (app *App) viewScrollLogs(percentage int) <span class="cov8" title="1">{ vScroll, _ := app.gui.View("scrollLogs") vScroll.Clear() // Определяем высоту окна _, viewHeight := vScroll.Size() // Заполняем скролл пробелами, если вывод пустой или не выходит за пределы окна if percentage == 0 || percentage > 100 </span><span class="cov8" title="1">{ fmt.Fprintln(vScroll, "▲") for i := 1; i < viewHeight-1; i++ </span><span class="cov8" title="1">{ fmt.Fprintln(vScroll, " ") }</span> <span class="cov8" title="1">fmt.Fprintln(vScroll, "▼")</span> } else<span class="cov0" title="0"> { // Рассчитываем позицию курсора (корректируем процент на размер скролла и верхней стрелки) scrollPosition := (viewHeight*percentage)/100 - 3 - 1 fmt.Fprintln(vScroll, "▲") // Выводим строки с пробелами и символом █ for_scroll: for i := 1; i < viewHeight-3; i++ </span><span class="cov0" title="0">{ // Проверяем текущую поизицию switch </span>{ case i == scrollPosition:<span class="cov0" title="0"> // Выводим скролл fmt.Fprintln(vScroll, "███")</span> case scrollPosition <= 0 || app.logScrollPos == 0:<span class="cov0" title="0"> // Если вышли за пределы окна или текст находится в самом начале, устанавливаем курсор в начало fmt.Fprintln(vScroll, "███") // Остальное заполняем пробелами с учетом стрелки и курсора (-4) до последней стрелки (-1) for i := 4; i < viewHeight-1; i++ </span><span class="cov0" title="0">{ fmt.Fprintln(vScroll, " ") }</span> <span class="cov0" title="0">break for_scroll</span> default:<span class="cov0" title="0"> // Пробелы на остальных строках fmt.Fprintln(vScroll, " ")</span> } } <span class="cov0" title="0">fmt.Fprintln(vScroll, "▼")</span> } } // Функция для скроллинга вниз func (app *App) scrollDownLogs(step int) error <span class="cov8" title="1">{ v, err := app.gui.View("logs") if err != nil </span><span class="cov0" title="0">{ return err }</span> // Получаем высоту окна, что бы не опускать лог с пустыми строками <span class="cov8" title="1">_, viewHeight := v.Size() // Проверяем, что размер журнала больше размера окна if len(app.filteredLogLines) > viewHeight </span><span class="cov0" title="0">{ // Увеличиваем позицию прокрутки app.logScrollPos += step // Если достигнут конец списка, останавливаем на максимальной длинне с учетом высоты окна if app.logScrollPos > len(app.filteredLogLines)-1-viewHeight </span><span class="cov0" title="0">{ app.logScrollPos = len(app.filteredLogLines) - 1 - viewHeight // Включаем автоскролл app.autoScroll = true }</span> // Вызываем функцию для обновления отображения журнала <span class="cov0" title="0">app.updateLogsView(false)</span> } <span class="cov8" title="1">return nil</span> } // Функция для скроллинга вверх func (app *App) scrollUpLogs(step int) error <span class="cov8" title="1">{ app.logScrollPos -= step if app.logScrollPos < 0 </span><span class="cov8" title="1">{ app.logScrollPos = 0 }</span> // Отключаем автоскролл <span class="cov8" title="1">app.autoScroll = false app.updateLogsView(false) return nil</span> } // Функция для переход к началу журнала func (app *App) pageUpLogs() <span class="cov8" title="1">{ app.logScrollPos = 0 app.autoScroll = false app.updateLogsView(false) }</span> // Функция для очистки поля ввода фильтра func (app *App) clearFilterEditor(g *gocui.Gui) <span class="cov8" title="1">{ v, _ := g.View("filter") // Очищаем содержимое View v.Clear() // Устанавливаем курсор на начальную позицию if err := v.SetCursor(0, 0); err != nil </span><span class="cov0" title="0">{ return }</span> // Очищаем буфер фильтра <span class="cov8" title="1">app.filterText = "" app.applyFilter(false)</span> } // Функция для обновления последнего выбранного вывода лога func (app *App) updateLogOutput(seconds int) <span class="cov8" title="1">{ for </span><span class="cov8" title="1">{ // Выполняем обновление интерфейса через метод Update для иницилизации перерисовки интерфейса app.gui.Update(func(g *gocui.Gui) error </span><span class="cov8" title="1">{ // Сбрасываем автоскролл, если это ручное обновление, что бы опустить журнал вниз if seconds == 0 </span><span class="cov8" title="1">{ app.autoScroll = true }</span> <span class="cov8" title="1">switch app.lastWindow </span>{ case "services":<span class="cov8" title="1"> app.loadJournalLogs(app.lastSelected, false)</span> case "varLogs":<span class="cov8" title="1"> app.loadFileLogs(app.lastSelected, false)</span> case "docker":<span class="cov8" title="1"> app.loadDockerLogs(app.lastSelected, false)</span> } <span class="cov8" title="1">return nil</span> }) <span class="cov8" title="1">if seconds == 0 </span><span class="cov8" title="1">{ break</span> } <span class="cov8" title="1">time.Sleep(time.Duration(seconds) * time.Second)</span> } } // Функция для обновления вывода при изменение размера окна func (app *App) updateWindowSize(seconds int) <span class="cov8" title="1">{ for </span><span class="cov8" title="1">{ app.gui.Update(func(g *gocui.Gui) error </span><span class="cov8" title="1">{ v, err := g.View("logs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">windowWidth, windowHeight := v.Size() if windowWidth != app.windowWidth || windowHeight != app.windowHeight </span><span class="cov8" title="1">{ app.windowWidth, app.windowHeight = windowWidth, windowHeight app.updateLogsView(true) if v, err := g.View("services"); err == nil </span><span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleServices = viewHeight }</span> <span class="cov8" title="1">if v, err := g.View("varLogs"); err == nil </span><span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleFiles = viewHeight }</span> <span class="cov8" title="1">if v, err := g.View("docker"); err == nil </span><span class="cov8" title="1">{ _, viewHeight := v.Size() app.maxVisibleDockerContainers = viewHeight }</span> <span class="cov8" title="1">app.applyFilterList()</span> } <span class="cov8" title="1">return nil</span> }) <span class="cov8" title="1">time.Sleep(time.Duration(seconds) * time.Second)</span> } } // Функция для фиксации места загрузки журнала с помощью делиметра func (app *App) updateDelimiter(newUpdate bool) <span class="cov8" title="1">{ // Фиксируем текущую длинну массива (индекс) для вставки строки обновления, если это ручной выбор из списка if newUpdate </span><span class="cov8" title="1">{ app.newUpdateIndex = len(app.currentLogLines) - 1 // Сбрасываем автоскролл app.autoScroll = true // Фиксируем время загрузки журнала app.updateTime = time.Now().Format("15:04:05") }</span> // Проверяем, что массив не пустой и уже привысил длинну новых сообщений <span class="cov8" title="1">if app.newUpdateIndex > 0 && len(app.currentLogLines)-1 > app.newUpdateIndex </span><span class="cov0" title="0">{ // Формируем длинну делимитра v, _ := app.gui.View("logs") width, _ := v.Size() lengthDelimiter := width/2 - 5 delimiter1 := strings.Repeat("⎯", lengthDelimiter) delimiter2 := delimiter1 if width > lengthDelimiter+lengthDelimiter+10 </span><span class="cov0" title="0">{ delimiter2 = strings.Repeat("⎯", lengthDelimiter+1) }</span> <span class="cov0" title="0">var delimiterString string = delimiter1 + " " + app.updateTime + " " + delimiter2 // Вставляем новую строку после указанного индекса, сдвигая остальные строки массива app.currentLogLines = append(app.currentLogLines[:app.newUpdateIndex], append([]string{delimiterString}, app.currentLogLines[app.newUpdateIndex:]...)...)</span> } } // ---------------------------------------- Key Binding ---------------------------------------- // Функция для биндинга клавиш func (app *App) setupKeybindings() error <span class="cov8" title="1">{ // Ctrl+C для выхода из приложения if err := app.gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil </span><span class="cov0" title="0">{ return err }</span> // Tab для переключения между окнами <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyTab, gocui.ModNone, app.nextView); err != nil </span><span class="cov0" title="0">{ return err }</span> // Shift+Tab для переключения между окнами в обратном порядке <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyBacktab, gocui.ModNone, app.backView); err != nil </span><span class="cov0" title="0">{ return err }</span> // Enter для выбора службы и загрузки журналов <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyEnter, gocui.ModNone, app.selectService); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyEnter, gocui.ModNone, app.selectFile); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyEnter, gocui.ModNone, app.selectDocker); err != nil </span><span class="cov0" title="0">{ return err }</span> // Перемещение вниз к следующей службе (функция nextService), файлу (nextFileName) или контейнеру (nextDockerContainer) <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Быстрое пролистывание вниз через 10 записей (Shift+Down) <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Alt+Down 100 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Shift+D на 10 для macOS <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", 'D', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", 'D', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", 'D', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Ctrl+D на 100 для macOS <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyCtrlD, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyCtrlD, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyCtrlD, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Pgdn 1 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Shift+Pgdn 10 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Alt+Pgdn 100 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextService(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextFileName(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.nextDockerContainer(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // ^^^ // Пролистывание вверх <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Shift+Up 10 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Alt+Up 100 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Shift+U на 10 для macOS <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", 'U', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", 'U', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", 'U', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Ctrl+U на 100 для macOS <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyCtrlU, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyCtrlU, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyCtrlU, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Pgup 1 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Shift+Pgup 10 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Alt+Pgup 100 <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevService(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevFileName(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.prevDockerContainer(v, 100) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Переключение выбора журналов для systemd/journald и отключаем для Windows <span class="cov8" title="1">if app.getOS != "windows" </span><span class="cov8" title="1">{ if err := app.gui.SetKeybinding("services", gocui.KeyArrowRight, gocui.ModNone, app.setUnitListRight); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("services", gocui.KeyArrowLeft, gocui.ModNone, app.setUnitListLeft); err != nil </span><span class="cov0" title="0">{ return err }</span> } // Переключение выбора журналов для File System <span class="cov8" title="1">if app.keybindingsEnabled </span><span class="cov8" title="1">{ // Установка привязок if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowRight, gocui.ModNone, app.setLogFilesListRight); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("varLogs", gocui.KeyArrowLeft, gocui.ModNone, app.setLogFilesListLeft); err != nil </span><span class="cov0" title="0">{ return err }</span> } else<span class="cov8" title="1"> { // Удаление привязок if err := app.gui.DeleteKeybinding("varLogs", gocui.KeyArrowRight, gocui.ModNone); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.DeleteKeybinding("varLogs", gocui.KeyArrowLeft, gocui.ModNone); err != nil </span><span class="cov0" title="0">{ return err }</span> } // Переключение выбора журналов для Containerization System <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowRight, gocui.ModNone, app.setContainersListRight); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("docker", gocui.KeyArrowLeft, gocui.ModNone, app.setContainersListLeft); err != nil </span><span class="cov0" title="0">{ return err }</span> // Переключение между режимами фильтрации через Up/Down для выбранного окна (filter) <span class="cov8" title="1">if err := app.gui.SetKeybinding("filter", gocui.KeyArrowUp, gocui.ModNone, app.setFilterModeRight); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("filter", gocui.KeyArrowDown, gocui.ModNone, app.setFilterModeLeft); err != nil </span><span class="cov0" title="0">{ return err }</span> // PgUp/PgDn Filter <span class="cov8" title="1">if err := app.gui.SetKeybinding("filter", gocui.KeyPgup, gocui.ModNone, app.setFilterModeRight); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("filter", gocui.KeyPgdn, gocui.ModNone, app.setFilterModeLeft); err != nil </span><span class="cov0" title="0">{ return err }</span> // Переключение для количества выводимых строк через Left/Right для выбранного окна (logs) <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowLeft, gocui.ModNone, app.setCountLogViewDown); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowRight, gocui.ModNone, app.setCountLogViewUp); err != nil </span><span class="cov0" title="0">{ return err }</span> // >>> Logs // Пролистывание вывода журнала через 1/10/500 записей вниз <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowDown, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowDown, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(500) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Shift+D 10 <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", 'D', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Ctrl+D 500 <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyCtrlD, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(500) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Up Logs 1/10/500 <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowUp, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyArrowUp, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(500) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Shift+U 500 <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", 'U', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Ctrl+U 10 <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyCtrlU, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(500) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // KeyPgdn Logs 1/10/500 <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyPgdn, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyPgdn, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyPgdn, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollDownLogs(500) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // KeyPgup Logs 1/10/500 <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyPgup, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(1) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyPgup, gocui.ModShift, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(10) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("logs", gocui.KeyPgup, gocui.ModAlt, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ return app.scrollUpLogs(500) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Перемещение к концу журнала (Ctrl+E or End) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyCtrlE, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ app.updateLogsView(true) return nil }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyEnd, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ app.updateLogsView(true) return nil }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Перемещение к началу журнала (Ctrl+A or Home) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyCtrlA, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ app.pageUpLogs() return nil }</span>); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyHome, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ app.pageUpLogs() return nil }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Очистка поля ввода для фильтра (Ctrl+W) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyCtrlW, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ app.clearFilterEditor(g) return nil }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Обновить все текущие списки журналов вручную (Ctrl+R) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ if app.getOS != "windows" </span><span class="cov0" title="0">{ app.loadServices(app.selectUnits) app.loadFiles(app.selectPath) }</span> else<span class="cov0" title="0"> { app.loadWinFiles(app.selectPath) }</span> <span class="cov0" title="0">app.loadDockerContainer(app.selectContainerizationSystem) return nil</span> }); err != nil <span class="cov0" title="0">{ return err }</span> // Выключение/включение встроенной покраски ключевых слов (Ctrl+Q) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyCtrlQ, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ if app.colorMode </span><span class="cov0" title="0">{ app.colorMode = false }</span> else<span class="cov0" title="0"> { app.colorMode = true }</span> <span class="cov0" title="0">if len(app.currentLogLines) != 0 </span><span class="cov0" title="0">{ app.updateLogsView(true) app.applyFilter(false) app.updateLogOutput(0) }</span> <span class="cov0" title="0">return nil</span> }); err != nil <span class="cov0" title="0">{ return err }</span> // Включение/выключение режима покраски через tailspin (Ctrl+S) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyCtrlS, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ if app.tailSpinMode </span><span class="cov0" title="0">{ app.tailSpinMode = false }</span> else<span class="cov0" title="0"> { // Проверяем, что tailspin установлен в системе tsCommands := []string{"tailspin", "tspin"} for _, ts := range tsCommands </span><span class="cov0" title="0">{ cmd := exec.Command(ts, "--version") _, err := cmd.Output() if err == nil </span><span class="cov0" title="0">{ app.tailSpinMode = true }</span> } } <span class="cov0" title="0">if len(app.currentLogLines) != 0 </span><span class="cov0" title="0">{ app.updateLogsView(true) app.applyFilter(false) app.updateLogOutput(0) }</span> <span class="cov0" title="0">return nil</span> }); err != nil <span class="cov0" title="0">{ return err }</span> // Отключить окно справки (F1) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyF1, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ app.showInterfaceHelp(g) return nil }</span>); err != nil <span class="cov0" title="0">{ return err }</span> // Закрыть окно справки (Esc) <span class="cov8" title="1">if err := app.gui.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error </span><span class="cov0" title="0">{ if err := app.closeHelp(g); err != nil </span><span class="cov0" title="0">{ return nil }</span> <span class="cov0" title="0">return nil</span> }); err != nil <span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">return nil</span> } func (app *App) showInterfaceHelp(g *gocui.Gui) <span class="cov8" title="1">{ // Получаем размеры терминала maxX, maxY := g.Size() // Размеры окна help width, height := 104, 29 // Вычисляем координаты для центрального расположения 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) </span><span class="cov0" title="0">{ return }</span> <span class="cov8" title="1">helpView.Title = " Help " helpView.Autoscroll = true helpView.Wrap = true helpView.FrameColor = gocui.ColorGreen helpView.TitleColor = gocui.ColorGreen helpView.Clear() fmt.Fprintln(helpView, "\n \033[33mlazyjournal\033[0m - terminal user interface for reading logs from journalctl, file system, Docker and") fmt.Fprintln(helpView, " Podman containers, as well Kubernetes pods.") fmt.Fprintln(helpView, "\n Version: \033[36m"+programVersion+"\033[0m") fmt.Fprintln(helpView, "\n Hotkeys:") fmt.Fprintln(helpView, "\n \033[32mTab\033[0m - switch between windows.") fmt.Fprintln(helpView, " \033[32mShift+Tab\033[0m - return to previous window.") fmt.Fprintln(helpView, " \033[32mLeft/Right\033[0m - switch between journal lists in the selected window.") fmt.Fprintln(helpView, " \033[32mEnter\033[0m - selection a journal from the list to display log output.") fmt.Fprintln(helpView, " \033[32m<Up/PgUp>\033[0m and \033[32m<Down/PgDown>\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[32m<Shift/Alt>+<Up/Down>\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[32m<Shift/Ctrl>+<U/D>\033[0m - quickly move up and down (alternative for macOS).") fmt.Fprintln(helpView, " \033[32mCtrl+A\033[0m or \033[32mHome\033[0m - go to top of log.") fmt.Fprintln(helpView, " \033[32mCtrl+E\033[0m or \033[32mEnd\033[0m - go to the end of the log.") fmt.Fprintln(helpView, " \033[32mCtrl+Q\033[0m - enable or disable built-in output coloring.") fmt.Fprintln(helpView, " \033[32mCtrl+S\033[0m - enable or disable coloring via tailspin.") fmt.Fprintln(helpView, " \033[32mCtrl+R\033[0m - update all log lists.") fmt.Fprintln(helpView, " \033[32mCtrl+W\033[0m - clear text input field for filter to quickly update current log output without filtering.") fmt.Fprintln(helpView, " \033[32mCtrl+C\033[0m - exit.") fmt.Fprintln(helpView, " \033[32mEscape\033[0m - close help.") fmt.Fprintln(helpView, "\n Source code: \033[35mhttps://github.com/Lifailon/lazyjournal\033[0m")</span> } func (app *App) closeHelp(g *gocui.Gui) error <span class="cov8" title="1">{ if err := g.DeleteView("help"); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">return nil</span> } // Функции для переключения количества строк для вывода логов func (app *App) setCountLogViewUp(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ switch app.logViewCount </span>{ case "5000":<span class="cov8" title="1"> app.logViewCount = "10000"</span> case "10000":<span class="cov8" title="1"> app.logViewCount = "50000"</span> case "50000":<span class="cov8" title="1"> app.logViewCount = "100000"</span> case "100000":<span class="cov8" title="1"> app.logViewCount = "200000"</span> case "200000":<span class="cov8" title="1"> app.logViewCount = "300000"</span> case "300000":<span class="cov8" title="1"> app.logViewCount = "300000"</span> } <span class="cov8" title="1">app.applyFilter(false) app.updateLogOutput(0) return nil</span> } func (app *App) setCountLogViewDown(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ switch app.logViewCount </span>{ case "300000":<span class="cov8" title="1"> app.logViewCount = "200000"</span> case "200000":<span class="cov8" title="1"> app.logViewCount = "100000"</span> case "100000":<span class="cov8" title="1"> app.logViewCount = "50000"</span> case "50000":<span class="cov8" title="1"> app.logViewCount = "10000"</span> case "10000":<span class="cov8" title="1"> app.logViewCount = "5000"</span> case "5000":<span class="cov8" title="1"> app.logViewCount = "5000"</span> } <span class="cov8" title="1">app.applyFilter(false) app.updateLogOutput(0) return nil</span> } // Функции для переключения режима фильтрации func (app *App) setFilterModeRight(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedFilter, err := g.View("filter") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">switch selectedFilter.Title </span>{ case "Filter (Default)":<span class="cov8" title="1"> selectedFilter.Title = "Filter (Fuzzy)" app.selectFilterMode = "fuzzy"</span> case "Filter (Fuzzy)":<span class="cov8" title="1"> selectedFilter.Title = "Filter (Regex)" app.selectFilterMode = "regex"</span> case "Filter (Regex)":<span class="cov8" title="1"> selectedFilter.Title = "Filter (Default)" app.selectFilterMode = "default"</span> } <span class="cov8" title="1">app.applyFilter(false) return nil</span> } func (app *App) setFilterModeLeft(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedFilter, err := g.View("filter") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">switch selectedFilter.Title </span>{ case "Filter (Default)":<span class="cov8" title="1"> selectedFilter.Title = "Filter (Regex)" app.selectFilterMode = "regex"</span> case "Filter (Regex)":<span class="cov8" title="1"> selectedFilter.Title = "Filter (Fuzzy)" app.selectFilterMode = "fuzzy"</span> case "Filter (Fuzzy)":<span class="cov8" title="1"> selectedFilter.Title = "Filter (Default)" app.selectFilterMode = "default"</span> } <span class="cov8" title="1">app.applyFilter(false) return nil</span> } // Функции для переключения выбора журналов из journalctl func (app *App) setUnitListRight(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedServices, err := g.View("services") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> // Сбрасываем содержимое массива и положение курсора <span class="cov8" title="1">app.journals = app.journals[:0] app.startServices = 0 app.selectedJournal = 0 // Меняем журнал и обновляем список switch app.selectUnits </span>{ case "services":<span class="cov8" title="1"> app.selectUnits = "UNIT" selectedServices.Title = " < System journals (0) > " app.loadServices(app.selectUnits)</span> case "UNIT":<span class="cov8" title="1"> app.selectUnits = "USER_UNIT" selectedServices.Title = " < User journals (0) > " app.loadServices(app.selectUnits)</span> case "USER_UNIT":<span class="cov8" title="1"> app.selectUnits = "kernel" selectedServices.Title = " < Kernel boot (0) > " app.loadServices(app.selectUnits)</span> case "kernel":<span class="cov8" title="1"> app.selectUnits = "services" selectedServices.Title = " < Unit list (0) > " app.loadServices(app.selectUnits)</span> } <span class="cov8" title="1">return nil</span> } func (app *App) setUnitListLeft(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedServices, err := g.View("services") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">app.journals = app.journals[:0] app.startServices = 0 app.selectedJournal = 0 switch app.selectUnits </span>{ case "services":<span class="cov8" title="1"> app.selectUnits = "kernel" selectedServices.Title = " < Kernel boot (0) > " app.loadServices(app.selectUnits)</span> case "kernel":<span class="cov8" title="1"> app.selectUnits = "USER_UNIT" selectedServices.Title = " < User journals (0) > " app.loadServices(app.selectUnits)</span> case "USER_UNIT":<span class="cov8" title="1"> app.selectUnits = "UNIT" selectedServices.Title = " < System journals (0) > " app.loadServices(app.selectUnits)</span> case "UNIT":<span class="cov8" title="1"> app.selectUnits = "services" selectedServices.Title = " < Unit list (0) > " app.loadServices(app.selectUnits)</span> } <span class="cov8" title="1">return nil</span> } // Функция для переключения выбора журналов файловой системы func (app *App) setLogFilesListRight(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedVarLog, err := g.View("varLogs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> // Добавляем сообщение о загрузке журнала <span class="cov8" title="1">g.Update(func(g *gocui.Gui) error </span><span class="cov8" title="1">{ selectedVarLog.Clear() fmt.Fprintln(selectedVarLog, "Searching log files...") selectedVarLog.Highlight = false return nil }</span>) // Отключаем переключение списков <span class="cov8" title="1">app.keybindingsEnabled = false if err := app.setupKeybindings(); err != nil </span><span class="cov0" title="0">{ log.Panicln("Error key bindings", err) }</span> // Полсекундная задержка, для корректного обновления интерфейса после выполнения функции <span class="cov8" title="1">time.Sleep(500 * time.Millisecond) app.logfiles = app.logfiles[:0] app.startFiles = 0 app.selectedFile = 0 // Запускаем функцию загрузки журнала в горутине if app.getOS == "windows" </span><span class="cov0" title="0">{ go func() </span><span class="cov0" title="0">{ switch app.selectPath </span>{ case "ProgramFiles":<span class="cov0" title="0"> app.selectPath = "ProgramFiles86" selectedVarLog.Title = " < Program Files x86 (0) > " app.loadWinFiles(app.selectPath)</span> case "ProgramFiles86":<span class="cov0" title="0"> app.selectPath = "ProgramData" selectedVarLog.Title = " < ProgramData (0) > " app.loadWinFiles(app.selectPath)</span> case "ProgramData":<span class="cov0" title="0"> app.selectPath = "AppDataLocal" selectedVarLog.Title = " < AppData Local (0) > " app.loadWinFiles(app.selectPath)</span> case "AppDataLocal":<span class="cov0" title="0"> app.selectPath = "AppDataRoaming" selectedVarLog.Title = " < AppData Roaming (0) > " app.loadWinFiles(app.selectPath)</span> case "AppDataRoaming":<span class="cov0" title="0"> app.selectPath = "ProgramFiles" selectedVarLog.Title = " < Program Files (0) > " app.loadWinFiles(app.selectPath)</span> } // Включаем переключение списков <span class="cov0" title="0">app.keybindingsEnabled = true if err := app.setupKeybindings(); err != nil </span><span class="cov0" title="0">{ log.Panicln("Error key bindings", err) }</span> }() } else<span class="cov8" title="1"> { go func() </span><span class="cov8" title="1">{ switch app.selectPath </span>{ case "/var/log/":<span class="cov8" title="1"> app.selectPath = "/opt/" selectedVarLog.Title = " < Optional package logs (0) > " app.loadFiles(app.selectPath)</span> case "/opt/":<span class="cov8" title="1"> app.selectPath = "/home/" selectedVarLog.Title = " < Users home logs (0) > " app.loadFiles(app.selectPath)</span> case "/home/":<span class="cov8" title="1"> app.selectPath = "descriptor" selectedVarLog.Title = " < Process descriptor logs (0) > " app.loadFiles(app.selectPath)</span> case "descriptor":<span class="cov8" title="1"> app.selectPath = "/var/log/" selectedVarLog.Title = " < System var logs (0) > " app.loadFiles(app.selectPath)</span> } // Включаем переключение списков <span class="cov8" title="1">app.keybindingsEnabled = true if err := app.setupKeybindings(); err != nil </span><span class="cov0" title="0">{ log.Panicln("Error key bindings", err) }</span> }() } <span class="cov8" title="1">return nil</span> } func (app *App) setLogFilesListLeft(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedVarLog, err := g.View("varLogs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">g.Update(func(g *gocui.Gui) error </span><span class="cov8" title="1">{ selectedVarLog.Clear() fmt.Fprintln(selectedVarLog, "Searching log files...") selectedVarLog.Highlight = false return nil }</span>) <span class="cov8" title="1">app.keybindingsEnabled = false if err := app.setupKeybindings(); err != nil </span><span class="cov0" title="0">{ log.Panicln("Error key bindings", err) }</span> <span class="cov8" title="1">time.Sleep(500 * time.Millisecond) app.logfiles = app.logfiles[:0] app.startFiles = 0 app.selectedFile = 0 if app.getOS == "windows" </span><span class="cov0" title="0">{ go func() </span><span class="cov0" title="0">{ switch app.selectPath </span>{ case "ProgramFiles":<span class="cov0" title="0"> app.selectPath = "AppDataRoaming" selectedVarLog.Title = " < AppData Roaming (0) > " app.loadWinFiles(app.selectPath)</span> case "AppDataRoaming":<span class="cov0" title="0"> app.selectPath = "AppDataLocal" selectedVarLog.Title = " < AppData Local (0) > " app.loadWinFiles(app.selectPath)</span> case "AppDataLocal":<span class="cov0" title="0"> app.selectPath = "ProgramData" selectedVarLog.Title = " < ProgramData (0) > " app.loadWinFiles(app.selectPath)</span> case "ProgramData":<span class="cov0" title="0"> app.selectPath = "ProgramFiles86" selectedVarLog.Title = " < Program Files x86 (0) > " app.loadWinFiles(app.selectPath)</span> case "ProgramFiles86":<span class="cov0" title="0"> app.selectPath = "ProgramFiles" selectedVarLog.Title = " < Program Files (0) > " app.loadWinFiles(app.selectPath)</span> } <span class="cov0" title="0">app.keybindingsEnabled = true if err := app.setupKeybindings(); err != nil </span><span class="cov0" title="0">{ log.Panicln("Error key bindings", err) }</span> }() } else<span class="cov8" title="1"> { go func() </span><span class="cov8" title="1">{ switch app.selectPath </span>{ case "/var/log/":<span class="cov8" title="1"> app.selectPath = "descriptor" selectedVarLog.Title = " < Process descriptor logs (0) > " app.loadFiles(app.selectPath)</span> case "descriptor":<span class="cov8" title="1"> app.selectPath = "/home/" selectedVarLog.Title = " < Users home logs (0) > " app.loadFiles(app.selectPath)</span> case "/home/":<span class="cov8" title="1"> app.selectPath = "/opt/" selectedVarLog.Title = " < Optional package logs (0) > " app.loadFiles(app.selectPath)</span> case "/opt/":<span class="cov8" title="1"> app.selectPath = "/var/log/" selectedVarLog.Title = " < System var logs (0) > " app.loadFiles(app.selectPath)</span> } <span class="cov8" title="1">app.keybindingsEnabled = true if err := app.setupKeybindings(); err != nil </span><span class="cov0" title="0">{ log.Panicln("Error key bindings", err) }</span> }() } <span class="cov8" title="1">return nil</span> } // Функция для переключения выбора системы контейнеризации (Docker/Podman) func (app *App) setContainersListRight(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedDocker, err := g.View("docker") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">app.dockerContainers = app.dockerContainers[:0] app.startDockerContainers = 0 app.selectedDockerContainer = 0 switch app.selectContainerizationSystem </span>{ case "docker":<span class="cov8" title="1"> app.selectContainerizationSystem = "podman" selectedDocker.Title = " < Podman containers (0) > " app.loadDockerContainer(app.selectContainerizationSystem)</span> case "podman":<span class="cov8" title="1"> app.selectContainerizationSystem = "kubectl" selectedDocker.Title = " < Kubernetes pods (0) > " app.loadDockerContainer(app.selectContainerizationSystem)</span> case "kubectl":<span class="cov8" title="1"> app.selectContainerizationSystem = "docker" selectedDocker.Title = " < Docker containers (0) > " app.loadDockerContainer(app.selectContainerizationSystem)</span> } <span class="cov8" title="1">return nil</span> } func (app *App) setContainersListLeft(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedDocker, err := g.View("docker") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">app.dockerContainers = app.dockerContainers[:0] app.startDockerContainers = 0 app.selectedDockerContainer = 0 switch app.selectContainerizationSystem </span>{ case "docker":<span class="cov8" title="1"> app.selectContainerizationSystem = "kubectl" selectedDocker.Title = " < Kubernetes pods (0) > " app.loadDockerContainer(app.selectContainerizationSystem)</span> case "kubectl":<span class="cov8" title="1"> app.selectContainerizationSystem = "podman" selectedDocker.Title = " < Podman containers (0) > " app.loadDockerContainer(app.selectContainerizationSystem)</span> case "podman":<span class="cov8" title="1"> app.selectContainerizationSystem = "docker" selectedDocker.Title = " < Docker containers (0) > " app.loadDockerContainer(app.selectContainerizationSystem)</span> } <span class="cov8" title="1">return nil</span> } // Функция для переключения окон через Tab func (app *App) nextView(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedFilterList, err := g.View("filterList") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedServices, err := g.View("services") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedVarLog, err := g.View("varLogs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedDocker, err := g.View("docker") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedFilter, err := g.View("filter") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedLogs, err := g.View("logs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedScrollLogs, err := g.View("scrollLogs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">currentView := g.CurrentView() var nextView string // Начальное окно if currentView == nil </span><span class="cov0" title="0">{ nextView = "services" }</span> else<span class="cov8" title="1"> { switch currentView.Name() </span>{ case "filterList":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "services":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "varLogs":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "docker":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "filter":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorGreen selectedScrollLogs.FrameColor = gocui.ColorGreen</span> case "logs":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> } } // Устанавливаем новое активное окно <span class="cov8" title="1">if _, err := g.SetCurrentView(nextView); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">return nil</span> } // Функция для переключения окон в обратном порядке через Shift+Tab func (app *App) backView(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ selectedFilterList, err := g.View("filterList") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedServices, err := g.View("services") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedVarLog, err := g.View("varLogs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedDocker, err := g.View("docker") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedFilter, err := g.View("filter") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedLogs, err := g.View("logs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">selectedScrollLogs, err := g.View("scrollLogs") if err != nil </span><span class="cov0" title="0">{ log.Panicln(err) }</span> <span class="cov8" title="1">currentView := g.CurrentView() var nextView string if currentView == nil </span><span class="cov0" title="0">{ nextView = "services" }</span> else<span class="cov8" title="1"> { switch currentView.Name() </span>{ case "filterList":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorGreen selectedScrollLogs.FrameColor = gocui.ColorGreen</span> case "services":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "logs":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "filter":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "docker":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> case "varLogs":<span class="cov8" title="1"> 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 selectedLogs.TitleColor = gocui.ColorDefault selectedScrollLogs.FrameColor = gocui.ColorDefault</span> } } <span class="cov8" title="1">if _, err := g.SetCurrentView(nextView); err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov8" title="1">return nil</span> } // Функция для выхода func quit(g *gocui.Gui, v *gocui.View) error <span class="cov8" title="1">{ return gocui.ErrQuit }</span> </pre> </div> </body> <script> (function() { var files = document.getElementById('files'); var visible; files.addEventListener('change', onChange, false); function select(part) { if (visible) visible.style.display = 'none'; visible = document.getElementById(part); if (!visible) return; files.value = part; visible.style.display = 'block'; location.hash = part; } function onChange() { select(files.value); window.scrollTo(0, 0); } if (location.hash != "") { select(location.hash.substr(1)); } if (!visible) { select("file0"); } })(); </script> </html>