;;; elogt.el --- Portage Emerge log browser -*- lexical-binding: t -*- ;; Copyright 2023 Gentoo Authors ;; This file is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 2 of the License, or ;; (at your option) any later version. ;; This file is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;; Authors: Maciej Barć ;; Maintainer: Gentoo Emacs project ;; Created: 03 Feb 2023 ;; Version: 0.0.0 ;; Keywords: convenience, files ;; Homepage: https://gitweb.gentoo.org/proj/emacs-elogt.git ;; Package-Requires: ((emacs "27.1")) ;; SPDX-License-Identifier: GPL-2.0-or-later ;;; Commentary: ;; Portage Emerge log browser for GNU Emacs. ;; ElogT displays the Portage logs in a convenient table and allows log file ;; browsing and deletion. ;; TODO: Clean files older than X. ;; TODO: Time narrowing NEWER or OLDER, now uses regexp. ;; TODO: Rewrite `elogt--make-log-table-contents'? ;; Maybe Use `dolist' or `do'? ;;; Code: (require 'ansi-color) (require 'easymenu) (defconst elogt-version "0.0.0" "ElogT version.") (defconst elogt--portage-log-stars '((" \e[31;01m*\e[0m" . error) (" \e[32m*\e[0m" . info) (" \e[33;01m*\e[0m" . warn))) (defconst elogt--priority-levels `("Error" "Info" "Warn" "None")) (defconst elogt--priority-level-indicators `((error . ,(propertize "Error" 'font-lock-face '(:foreground "red"))) (info . ,(propertize "Info" 'font-lock-face '(:foreground "green"))) (warn . ,(propertize "Warn" 'font-lock-face '(:foreground "orange"))) (none . ,(propertize "None" 'font-lock-face '(:foreground "purple"))))) (defconst elogt--index-priority 0 "Position index of the priority field inside the ElogT table.") (defconst elogt--index-category 1 "Position index of the category field inside the ElogT table.") (defconst elogt--index-package 2 "Position index of the package field inside the ElogT table.") (defconst elogt--index-time 3 "Position index of the time field inside the ElogT table.") ;; Customization (defgroup elogt nil "Customization for ElogT, Portage Emerge log browser." :group 'ebuild) (defcustom elogt-portage-log-dir "/var/log/portage" "Portage log directory location." :safe 'stringp :type 'file :group 'elogt) (defcustom elogt-check-priority t "Check priority of each logfile. If set to nil entries will have a phony Info level." :type 'boolean :group 'elogt) ;; Log file processing (defun elogt--gather-portage-logs () "Gather logs from ‘elogt-log-dir’." (directory-files elogt-portage-log-dir t "\\.log")) (defun elogt--logfile-find-stars (file-name) "Find special info indicators in FILE-NAME." (let ((buffer (find-file-noselect file-name 'nowarn 'rawfile)) (found-stars nil)) (with-current-buffer buffer (dolist (star elogt--portage-log-stars) (save-excursion (goto-char (point-min)) (let ((found (search-forward (car star) nil 'noerror))) (when found (push (cdr star) found-stars)))))) (kill-buffer buffer) found-stars)) (defun elogt--priority-level-property (key) "Rerun a value referenced by KEY of ‘elogt--priority-level-indicators’." (cdr (assoc key elogt--priority-level-indicators))) (defun elogt--logfile-priority-level (file-name) "Return a FILE-NAME priority level. A logfile priority level is one of: None, Info, Warn, Error." (let ((found-stars (elogt--logfile-find-stars file-name))) (cond ((not elogt-check-priority) (elogt--priority-level-property 'info)) ((null found-stars) (elogt--priority-level-property 'none)) ((member 'error found-stars) (elogt--priority-level-property 'error)) ((member 'warn found-stars) (elogt--priority-level-property 'warn)) (t (elogt--priority-level-property 'info))))) (defun elogt--file-size (file-path) "Return size of a given file at FILE-PATH." (file-size-human-readable-iec (file-attribute-size (file-attributes file-path)))) (defun elogt--logfile-properties (file-name) "Extract the properties form given FILE-NAME." (let ((splitted-file-name (split-string (file-name-base file-name) ":"))) (cond ((= (length splitted-file-name) 3) (let ((category (car splitted-file-name)) (package (cadr splitted-file-name)) (time (caddr splitted-file-name))) (vector (elogt--logfile-priority-level file-name) category (propertize package 'face 'bold) (propertize time 'face 'italic) (elogt--file-size file-name)))) (t (make-vector 5 ""))))) (defun elogt--make-log-table-contents () "Make ElogT table contents." (let ((index 0)) (mapcar (lambda (log-file-path) (let ((entry (list index (elogt--logfile-properties log-file-path)))) (setq index (+ index 1)) entry)) (elogt--gather-portage-logs)))) ;; Table interaction (defun elogt--entry-logfile-path (table-entry) "Return a logfile path of a given ElogT TABLE-ENTRY." (format "%s/%s:%s:%s.log" elogt-portage-log-dir (aref table-entry elogt--index-category) (substring-no-properties (aref table-entry elogt--index-package)) (substring-no-properties (aref table-entry elogt--index-time)))) (defun elogt--get-table-entry-logfile-path () "Return a logfile path of current ElogT table entry." (elogt--entry-logfile-path (tabulated-list-get-entry))) (defun elogt--open-entry-file () "Open a ElogT table entry at point. Return opened buffer (done via `find-file')." (find-file (elogt--get-table-entry-logfile-path))) (defun elogt-open-entry () "Open specified ElogT table entry and put it in a mode for viewing only." (interactive) (let ((buffer (elogt--open-entry-file))) (with-current-buffer buffer (fundamental-mode) (ansi-color-apply-on-region (point-min) (point-max) 'preserve-sequences) (read-only-mode) (view-mode)))) (defun elogt--delete-entry-file () "Delete a logfile of a entry at point." (delete-file (elogt--get-table-entry-logfile-path))) (defun elogt-delete-entry () "Delete specified ElogT table entry." (interactive) (elogt--delete-entry-file) (tabulated-list-delete-entry)) (defun elogt-refresh-table () "Refresh the ElogT table." (interactive) (message "Refreshing the ElogT table, please wait...") (setq tabulated-list-entries (elogt--make-log-table-contents)) (tabulated-list-init-header) (tabulated-list-print 'remember-pos) (message "The ElogT table is ready.")) (defun elogt-sort-package () "Sort the ElogT table by package, ascending." (interactive) (tabulated-list-sort elogt--index-package)) (defun elogt-sort-category () "Sort the ElogT table by category, ascending." (interactive) (tabulated-list-sort elogt--index-category)) (defun elogt-sort-priority () "Sort the ElogT table by priority, ascending." (interactive) (tabulated-list-sort elogt--index-priority)) (defun elogt-sort-time () "Sort the ElogT table by time, descending." (interactive) (tabulated-list-sort elogt--index-time)) ;; Result narrowing (defun elogt--narrow (column-index column-value) "Narrow the ElogT table to a selected COLUMN-INDEX of COLUMN-VALUE. Table record values of the selected column are compared by regex, using the `string-match', function to the wanted COLUMN-VALUE." (setq tabulated-list-entries (seq-filter (lambda (entry) (string-match column-value (substring-no-properties (aref (cadr entry) column-index)))) tabulated-list-entries)) (tabulated-list-print 'remember-pos 'update)) (defun elogt-narrow-priority (selected) "Narrow the ElogT table to a SELECTED Priority. Uses the `elogt--narrow' function." (interactive (list (completing-read "Priority: " elogt--priority-levels nil 'require-match))) (elogt--narrow elogt--index-priority selected)) (defun elogt-narrow-category (selected) "Narrow the ElogT table to a SELECTED Category. Uses the `elogt--narrow' function." (interactive "sCategory: ") (elogt--narrow elogt--index-category selected)) (defun elogt-narrow-package (selected) "Narrow the ElogT table to a SELECTED Package. Uses the `elogt--narrow' function." (interactive "sPackage: ") (elogt--narrow elogt--index-package selected)) (defun elogt-narrow-time (selected) "Narrow the ElogT table to a SELECTED Time. Uses the `elogt--narrow' function." (interactive "sTime: ") (elogt--narrow elogt--index-time selected)) ;; Major mode (defvar elogt-mode-hook nil "Hook for the `elogt-mode'.") (defvar elogt-mode-map (let ((elogt-mode-map (make-keymap))) ;; Standard key bindings. (define-key elogt-mode-map (kbd "o") #'elogt-open-entry) (define-key elogt-mode-map (kbd "d") #'elogt-delete-entry) (define-key elogt-mode-map (kbd "g") #'elogt-refresh-table) (define-key elogt-mode-map (kbd "n l") #'elogt-narrow-priority) (define-key elogt-mode-map (kbd "n c") #'elogt-narrow-category) (define-key elogt-mode-map (kbd "n p") #'elogt-narrow-package) (define-key elogt-mode-map (kbd "n t") #'elogt-narrow-time) ;; Elogv-mimicking key bindings. (define-key elogt-mode-map (kbd "A") #'elogt-sort-package) (define-key elogt-mode-map (kbd "a") #'elogt-sort-category) (define-key elogt-mode-map (kbd "c") #'elogt-sort-priority) (define-key elogt-mode-map (kbd "t") #'elogt-sort-time) ;; Miscellaneous key bindings. (define-key elogt-mode-map (kbd "/") #'isearch-forward) elogt-mode-map) "Key map for `elogt-mode'.") (easy-menu-define elogt-mode-menu elogt-mode-map "Menu for `elogt-mode'." '("ElogT" ;; Menu-bar: standard. ["Open current entry" elogt-open-entry] ["Delete current entry" elogt-delete-entry] ["Refresh the table" elogt-refresh-table] ;; Menu-bar: narrow. ("Narrow" ["Narrow to Priority" elogt-narrow-priority] ["Narrow to Category" elogt-narrow-category] ["Narrow to Package" elogt-narrow-package] ["Narrow to Time" elogt-narrow-time]) ;; Menu-bar: sort. ("Sort" ["Sort by Priority" elogt-sort-priority] ["Sort by Category" elogt-sort-category] ["Sort by Package" elogt-sort-package] ["Sort by Time" elogt-sort-time]))) (define-derived-mode elogt-mode tabulated-list-mode "ElogT" "Major mode for ElogT, browser of Portage log files." (setq tabulated-list-format [("Priority" 10 t) ("Category" 20 t) ("Package" 30 t) ("Time" 20 t) ("Size" 10 t)]) (setq tabulated-list-sort-key '("Time" . t)) (run-hooks 'elogt-mode-hook) (use-local-map elogt-mode-map)) ;; Main provided features ;;;###autoload (defun elogt () "Browse Portage log files." (interactive) (let ((buffer (get-buffer-create "*ElogT*"))) (with-current-buffer buffer (elogt-mode) (elogt-refresh-table)) (display-buffer buffer))) (provide 'elogt) ;;; elogt.el ends here