In a previous post, I mentioned a little bit of modeline hacking to display git changes and today’s commit count. In this article, I thought I would go through those snippets from my modeline code that accomplish this. At the bottom, I’ve also included a standalone minor mode for it.
Basically, this code will show you a format like “250/5” where 250 represents the number of uncommitted changes and 5 represents the number of commits made today.
If you’re interested in topics like these, particularly how you can leverage programs like git for technical and creative writing, I’d highly recommend my downloadable handbooks:
So let’s get into it.
Table of Contents
Git branch display
This section displays the current git branch name in brackets in the modeline. I like seeing the branch name just to make sure I’m in the right place.
(defun csm-modeline--git-branch () "Return the current git branch name, wrapped in brackets. If not in a git repository, return an empty string." (when buffer-file-name (let* ((default-directory (file-name-directory buffer-file-name)) (branch (string-trim (shell-command-to-string "git rev-parse --abbrev-ref HEAD 2>/dev/null")))) (if (and branch (not (string-empty-p branch))) (format "[%s]" branch) "")))) (defvar-local csm-modeline-directory '(:eval (when-let ((branch-name (csm-modeline--git-branch))) (propertize branch-name 'face 'magit-branch-local))) "Mode line construct to display the git branch name.") (put 'csm-modeline-directory 'risky-local-variable t)
csm-modeline--git-branchusesgit rev-parse --abbrev-ref HEADto catch the current branch name.- It checks if
buffer-file-nameexists to ensure we’re in a file-based buffer. - Sets
default-directoryto the file’s directory so git commands run in the correct location. - Redirects errors to
/dev/nullto silently handle cases in which we’re not in a git repository. - Formats the branch name in brackets like
[master]or[drafting](my preferred default branch). csm-modeline-directoryis the modeline variable that displays the branch withmagit-branch-localface styling.- The
risky-local-variableproperty is set totbecause it evaluates code dynamically.
Git changes and commit count display
This is the main functionality that shows uncommitted changes and today’s commit count.
Caching variables
(defvar csm-modeline-csmchange-cache nil "Cache for csmchange command output.") (defvar csm-modeline-csmchange-last-update 0 "Timestamp of last csmchange update.") (defvar csm-modeline-csmchange-update-interval 30 "Update interval in seconds for csmchange output.")
These variables implement a caching mechanism to avoid running shell commands too frequently:
csm-modeline-csmchange-cachestores the last result from thecsmchangecommandcsm-modeline-csmchange-last-updatetracks when the cache was last refreshed using a Unix timestamp.csm-modeline-csmchange-update-intervalsets how often to refresh (30 seconds by default)
This caching is crucial for performance since the modeline updates frequently, but we don’t need to run shell commands every time.
Getting Change Count
(defun csm-modeline--csmchange-output () "Return the output of csmchange command." (let ((output (shell-command-to-string "csmchange 2>/dev/null"))) (string-trim output))) (defun csm-modeline--cached-csmchange-output () "Return cached csmchange output, updating if necessary." (let ((now (float-time))) (when (or (null csm-modeline-csmchange-cache) (> (- now csm-modeline-csmchange-last-update) csm-modeline-csmchange-update-interval)) (setq csm-modeline-csmchange-cache (csm-modeline--csmchange-output) csm-modeline-csmchange-last-update now))) csm-modeline-csmchange-cache)
csm-modeline--csmchange-outputruns thecsmchangecommand (a custom shell script) and returns the trimmed output.csm-modeline--cached-csmchange-outputimplements the caching logic:- Gets the current time with
float-time. - Checks if the cache is null (first run) or if enough time has passed since the last update.
- If an update is needed, it runs
csmchangeand updates both the cache and timestamp. - Returns the cached value.
- Gets the current time with
Here is the csmchange shell script:
#!/bin/bash # Run the command in the current working directory git diff --word-diff=porcelain HEAD | grep -e '^+[^+]' -e '^-[^-]' | wc -w
Getting today’s commit Count
(defun csm-modeline--today-commits-count () "Return the number of commits made today." (when buffer-file-name (let* ((default-directory (file-name-directory buffer-file-name)) (count (string-trim (shell-command-to-string "git log --since='00:00:00' --oneline --no-merges 2>/dev/null | wc -l")))) (if (and count (not (string-empty-p count))) count "0"))))
This function counts commits made since midnight today:
- Checks
buffer-file-nameexists to ensure we’re in a file buffer. - Sets
default-directoryto the file’s directory for correct git context. - Uses
git log --since='00:00:00'to get commits since midnight. --onelineformats each commit as a single line for easy counting.--no-mergesexcludes merge commits to count only direct commits.- Pipes to
wc -lto count the number of lines (commits). - Returns “0” if the result is empty or invalid.
- Errors are silently discarded with
2>/dev/null.
Modeline display variable
(defvar-local csm-modeline-csmchange '(:eval (when (mode-line-window-selected-p) (let ((output (csm-modeline--cached-csmchange-output)) (commits (csm-modeline--today-commits-count))) (when (and output (not (string-empty-p output))) (let* ((change-count (string-to-number output)) (face (if (>= change-count 250) 'warning 'font-lock-string-face))) (concat " " (propertize output 'face face) (when commits (propertize (format "/%s" commits) 'face face)))))))) "Mode line construct to display csmchange output with today's commit count.") (put 'csm-modeline-csmchange 'risky-local-variable t)
This is the main modeline variable that brings it all together:
- Uses
:evalto dynamically evaluate the code each time the modeline updates. mode-line-window-selected-pensures the display only appears in the active window.- Retrieves both the cached change count and today’s commit count.
- Converts the change count to a number to compare against 250.
- Selects a face:
'warning(usually red/orange), depending on theme, if changes >~ 250, otherwise'font-lock-string-face(usually green/blue). - Formats the output as changes over commits (e.g. “250/5”).
- Setting
risky-local-variabletotallows the dynamic evaluation.
Simplified minor mode
On my GitHub, I have included a self-contained minor mode that implements only the git changes and commit count functionality. I figured this would be easier for individual analysis. If you have any suggestions on how to improve it please let me know. The csmchange shell script logic has been integrated directly into the minor mode, making it fully self-contained without requiring external commands.
Minor mode features
The simplified minor mode includes:
- Built-in Change Counting: The
csmchangeshell script logic is integrated directly into the package, so no external commands are required (though you can still use an external command if you prefer by settinggit-stats-modeline-use-builtin-difftonil). - Customizable Settings: Users can customize the update interval, warning threshold, whether to use built-in diff counting, and optionally the command to use for external counting.
- Same Core Logic: Uses the identical caching and counting logic from the original modeline.
- Easy Activation: Simply call
(git-stats-modeline-mode 1)to enable or(git-stats-modeline-mode 0)to disable. - Global Minor Mode: Affects all buffers, not just specific ones.
- Self-Contained: Can be distributed as a standalone functionality with proper headers and documentation.
How the minor could be used and modified:
;; Load the file (load-file "/path/to/git-stats-modeline.el") ;; Enable the mode (uses built-in change counting by default) (git-stats-modeline-mode 1) ;; Optional: customize settings (setq git-stats-modeline-warning-threshold 300) (setq git-stats-modeline-update-interval 60) ;; Optional: use external command instead of built-in counting (setq git-stats-modeline-use-builtin-diff nil) (setq git-stats-modeline-change-command "csmchange")
Leave a Reply