|
1 | | ---- |
2 | | -last_modified: 2025-11-03 17:39:38 |
3 | | ---- |
4 | | - |
5 | 1 | # headup.nvim |
6 | 2 |
|
7 | | - |
| 3 | +<p align="center"> |
| 4 | + |
| 5 | +*Automatically updates file metadata on save.* |
| 6 | + |
| 7 | +</p> |
8 | 8 |
|
9 | | -A Neovim plugin that automatically updates file header metadata when files are saved. |
| 9 | +## Demo |
| 10 | + |
| 11 | +https://github.com/Fro-Q/headup.nvim/raw/main/assets/headup_demo.mp4 |
10 | 12 |
|
11 | 13 | ## Features |
12 | 14 |
|
13 | | -- **Automatic Updates**: Automatically updates metadata in file headers when saving |
14 | | -- **Respects Manual Edits**: Won't overwrite user manual changes to metadata |
15 | | -- **Configurable**: Support multiple file types with different patterns |
| 15 | +- Automatic updates for header metadata on write |
| 16 | +- Respects manual edits (won’t overwrite user changes) |
| 17 | +- Per-filetype rules with globs, early-stop scanning, and exclusions |
| 18 | + |
| 19 | +## Built-in supported content |
16 | 20 |
|
17 | | -## Supported Content |
| 21 | +- `current_time`: Current timestamp with customizable format |
| 22 | +- `file_size`: Humanized file size (B / KB / MB / GB / TB) |
| 23 | +- `line_count`: Number of lines in the buffer |
| 24 | +- `file_name`: Base filename |
| 25 | +- `file_path`: Path relative to CWD |
| 26 | +- `file_path_abs`: Absolute path |
18 | 27 |
|
19 | | -- `current_time`: Current timestamp |
20 | | -- `file_size`: File size with automatic unit conversion (B, KB, MB, GB, TB) |
21 | | -- `line_count`: Number of lines in the file |
22 | | -- `file_name`: The base filename of the buffer |
23 | | -- `file_path`: File path relative to current working directory |
24 | | -- `file_path_abs`: Absolute file path |
| 28 | +See also: `:help Utils.valid_contents`. |
25 | 29 |
|
26 | | -## Installation |
| 30 | +## Install |
27 | 31 |
|
28 | | -### Using [lazy.nvim](https://github.com/folke/lazy.nvim) |
| 32 | +Using lazy.nvim |
29 | 33 |
|
30 | 34 | ```lua |
31 | 35 | { |
32 | 36 | "Fro-Q/headup.nvim", |
33 | 37 | config = function() |
34 | | - require("headup").setup({ |
35 | | - -- your configuration here |
36 | | - }) |
| 38 | + require("headup").setup() |
37 | 39 | end, |
38 | 40 | } |
39 | 41 | ``` |
40 | 42 |
|
41 | | -### Using [packer.nvim](https://github.com/wbthomason/packer.nvim) |
| 43 | +Using packer.nvim |
42 | 44 |
|
43 | 45 | ```lua |
44 | 46 | use { |
45 | 47 | "Fro-Q/headup.nvim", |
46 | 48 | config = function() |
47 | | - require("headup").setup({ |
48 | | - -- your configuration here |
49 | | - }) |
| 49 | + require("headup").setup() |
50 | 50 | end |
51 | 51 | } |
52 | 52 | ``` |
53 | 53 |
|
54 | | -## Configuration |
55 | | - |
56 | | -headup.nvim uses a simplified configuration format where you pass global settings and configuration items directly in the setup table, without needing a `configs` wrapper key. |
| 54 | +## Quick start |
57 | 55 |
|
58 | | -### Basic Structure |
| 56 | +Minimal setup that updates something like `-- Last Modified: x` in Lua with current time in the specified format, stopping at the first empty line. |
59 | 57 |
|
60 | 58 | ```lua |
61 | 59 | require("headup").setup({ |
62 | | - -- Global settings |
63 | 60 | enabled = true, |
64 | 61 | silent = true, |
65 | | - |
66 | | - -- Configuration items (array elements) |
67 | | - { |
68 | | - pattern = "*.md", -- file name glob(s) for autocmd |
69 | | - match_pattern = "...", -- Lua pattern to capture value in file content |
70 | | - content = "current_time", -- what to write |
71 | | - -- other options... |
72 | | - }, |
| 62 | + time_format = "%Y-%m-%d %H:%M:%S", |
| 63 | + max_lines = 20, |
| 64 | + end_pattern = "^%s*$", -- stop at first empty line |
| 65 | + exclude_pattern = "", |
73 | 66 | { |
74 | | - pattern = {"*.py", "*.pyi"}, |
75 | | - match_pattern = "...", |
| 67 | + pattern = "*.lua", |
| 68 | + match_pattern = "^%s*%-%-%s*[Ll]ast[%s_%-][Mm]odified:%s(.-)%s*$", |
76 | 69 | content = "current_time", |
77 | | - -- other options... |
78 | 70 | }, |
79 | | - -- Add more configuration items as needed... |
80 | 71 | }) |
81 | 72 | ``` |
82 | 73 |
|
83 | | -### Default Configuration |
| 74 | +## Configuration overview |
84 | 75 |
|
85 | | -```lua |
86 | | -require("headup").setup({ |
87 | | - enabled = true, |
88 | | - silent = true, -- Set to false to show notifications when updating |
| 76 | +- Global options (apply to all items unless overridden): |
| 77 | + - `enabled` (boolean, default `true`) |
| 78 | + - `silent` (boolean, default `true`) |
| 79 | + - `time_format` (string|"inherit") |
| 80 | + - `max_lines` (number) |
| 81 | + - `end_pattern` (string, Lua pattern; early stop when matched) |
| 82 | + - `exclude_pattern` (string|string[]; file globs to skip) |
89 | 83 |
|
90 | | - -- Global fallbacks (used when an item doesn't set its own value) |
91 | | - time_format = "inherit", |
92 | | - max_lines = 20, |
93 | | - end_pattern = "^---%s*$", -- Stop scanning at end of YAML front matter |
94 | | - -- exclude_pattern = "*/archive/*", -- optional global exclude |
| 84 | +- Per-item options (each element is a rule): |
| 85 | + - `pattern` (string|string[]; file globs for autocmd) |
| 86 | + - `match_pattern` (string; Lua pattern to capture the value to replace) |
| 87 | + - `content` (string; one of supported content types) |
| 88 | + - `time_format` (string|"inherit") |
| 89 | + - `max_lines` (number) |
| 90 | + - `end_pattern` (string) |
| 91 | + - `exclude_pattern` (string|string[]) |
95 | 92 |
|
96 | | - { |
97 | | - pattern = "*.md", |
98 | | - match_pattern = "last_modified:%s*(.-)%s*$", |
99 | | - content = "current_time", |
100 | | - -- This item will use the global fallbacks above |
101 | | - }, |
102 | | -}) |
103 | | -``` |
| 93 | +Note: items inherit unset values from global options (fallbacks). |
104 | 94 |
|
105 | | -### Configuration Options |
| 95 | +## Commands and API |
106 | 96 |
|
107 | | -#### Global Options |
108 | | -- `enabled` (boolean): Whether the plugin is enabled globally (default: `true`) |
109 | | -- `silent` (boolean): Whether to suppress notification messages (default: `true`) |
110 | | -- `time_format` (string|"inherit"): Global fallback for time format when `content = "current_time"` |
111 | | -- `max_lines` (number): Global fallback for maximum number of lines to scan from the beginning |
112 | | -- `end_pattern` (string): Global fallback Lua pattern; stop scanning when matched (prevents over-scanning) |
113 | | -- `exclude_pattern` (string|string[]): Global fallback filename glob(s) to exclude from processing |
| 97 | +- `:HeadupEnable` / `:HeadupDisable` / `:HeadupToggle` |
| 98 | +- `:HeadupUpdate` – force update current buffer (ignores previous cache and buffer state) |
| 99 | +- `:HeadupClearCache` – clear internal cache |
114 | 100 |
|
115 | | -#### Per-Config Options (as array items) |
116 | | -Each configuration item should be a table with the following fields: |
117 | | -- `pattern` (string|string[]): File name glob(s) for autocmd (e.g., `"*.md"` or `{ "*.md", "*.markdown" }`) |
118 | | -- `match_pattern` (string): Lua pattern to capture the content to update |
119 | | -- `content` (string): What to write (`"current_time"`, `"file_size"`, `"line_count"`, `"file_name"`, `"file_path"`, `"file_path_abs"`) |
120 | | -- `time_format` (string): Time format string for `current_time`, use `"inherit"` to keep original format |
121 | | -- `max_lines` (number): Maximum number of lines to search from the beginning (default: 20) |
122 | | -- `end_pattern` (string, optional): Lua pattern that, when matched, stops scanning further lines (prevents over-scanning) |
123 | | -- `exclude_pattern` (string|string[], optional): File name glob(s) to exclude from processing |
| 101 | +Lua API (see :help Headup): |
124 | 102 |
|
125 | | -Note: For `time_format`, `max_lines`, `end_pattern`, and `exclude_pattern`, if an item doesn't provide a value, it will fall back to the corresponding global value when set. |
| 103 | +- Some commands have equivalent Lua functions: |
| 104 | + - `require('headup').enable()` / `disable()` / `toggle()` |
| 105 | + - `require('headup').update_current_buffer()` |
| 106 | + - `require('headup').clear_cache()` |
| 107 | +- You can register custom content generators: |
| 108 | + - `require('headup.func').register_generator(name, func)` |
126 | 109 |
|
127 | | -### Example Configurations |
| 110 | +See more in `:help headup.nvim`. |
128 | 111 |
|
129 | | -#### Markdown with YAML Front Matter |
| 112 | +## Extend: custom content generators |
| 113 | + |
| 114 | +You can add your own content type by registering a generator. A generator is |
| 115 | +`fun(bufnr: integer, ctx?: { time_format?: string, old_content?: string }): string`. |
| 116 | + |
| 117 | +Register: |
130 | 118 |
|
131 | 119 | ```lua |
132 | | -{ |
133 | | - pattern = "*.md", |
134 | | - match_pattern = "last_modified:%s*(.-)%s*$", |
135 | | - content = "current_time", |
136 | | - time_format = "inherit", |
137 | | - max_lines = 20, |
138 | | - end_pattern = "^---%s*$", |
139 | | -} |
| 120 | +local func = require('headup.func') |
| 121 | + |
| 122 | +func.register('my_branch', function(bufnr) |
| 123 | + local file = vim.api.nvim_buf_get_name(bufnr) |
| 124 | + local dir = file ~= '' and vim.fn.fnamemodify(file, ':h') or vim.fn.getcwd() |
| 125 | + local out = vim.fn.systemlist({ 'git', '-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD' }) |
| 126 | + local branch = (out and out[1]) or 'unknown' |
| 127 | + return (branch or ''):gsub('%s+', '') |
| 128 | +end) |
140 | 129 | ``` |
141 | 130 |
|
142 | | -#### Multiple File Types with Different Patterns |
| 131 | +Use in config: |
143 | 132 |
|
144 | 133 | ```lua |
145 | | -require("headup").setup({ |
146 | | - enabled = true, |
147 | | - silent = false, -- Show notifications |
148 | | - |
149 | | - -- Markdown files |
150 | | - { |
151 | | - pattern = {"*.md", "*.markdown"}, |
152 | | - match_pattern = "last_modified:%s*(.-)%s*$", |
153 | | - content = "current_time", |
154 | | - time_format = "%Y-%m-%d %H:%M:%S", |
155 | | - max_lines = 20, |
156 | | - end_pattern = "^---%s*$", |
157 | | - exclude_pattern = "*/archive/*", -- skip archived notes |
158 | | - }, |
159 | | - |
160 | | - -- Text files with file size |
161 | | - { |
162 | | - pattern = "*.txt", |
163 | | - match_pattern = "Size:%s*(.-)%s*$", |
164 | | - content = "file_size", |
165 | | - max_lines = 10, |
166 | | - }, |
167 | | - |
168 | | - -- Any file type with line count |
| 134 | +require('headup').setup({ |
169 | 135 | { |
170 | | - pattern = "*", |
171 | | - match_pattern = "Lines:%s*(.-)%s*$", |
172 | | - content = "line_count", |
173 | | - max_lines = 15, |
| 136 | + pattern = '*.md', |
| 137 | + match_pattern = 'branch:%s*(.-)%s*$', |
| 138 | + content = 'my_branch', |
174 | 139 | }, |
175 | 140 | }) |
176 | 141 | ``` |
177 | 142 |
|
178 | | -## Commands |
179 | | - |
180 | | -- `:HeadupEnable` / `:HeadupDisable` / `:HeadupToggle` — control plugin state |
181 | | -- `:HeadupUpdate` — manually update current buffer |
182 | | -- `:HeadupClearCache` — clear internal cache |
183 | | -- `:HeadupShowConfig` — show current effective configuration (formatted table) |
184 | | - |
185 | | -The plugin provides the following commands: |
186 | | - |
187 | | -- `:HeadupEnable` - Enable the plugin |
188 | | -- `:HeadupDisable` - Disable the plugin |
189 | | -- `:HeadupToggle` - Toggle plugin on/off |
190 | | -- `:HeadupUpdate` - Manually update metadata in current buffer |
191 | | -- `:HeadupClearCache` - Clear internal cache |
192 | | - |
193 | | -## How It Works |
194 | | - |
195 | | -1. **File Loading**: When a file is opened, the plugin scans for patterns and caches the matched content |
196 | | -2. **Content Detection**: On save, it checks if the file content has actually changed |
197 | | -3. **Manual Edit Respect**: If you manually edited the metadata, the plugin won't overwrite it |
198 | | -4. **Smart Update**: Only updates metadata when file content changed but metadata wasn't manually modified |
199 | | -5. **Cache Update**: Updates the internal cache after successful updates |
| 143 | +More templates: |
200 | 144 |
|
201 | | -## Example Usage |
202 | | - |
203 | | -For a Markdown file with YAML front matter: |
204 | | - |
205 | | -```markdown |
206 | | ---- |
207 | | -title: My Document |
208 | | -last_modified: 2024-01-01 12:00:00 |
209 | | ---- |
210 | | - |
211 | | -# My Document |
212 | | - |
213 | | -Content here... |
| 145 | +```lua |
| 146 | +-- Uppercase previous value |
| 147 | +func.register('uppercase', function(_, ctx) |
| 148 | + return ((ctx and ctx.old_content) or ''):upper() |
| 149 | +end) |
| 150 | + |
| 151 | +-- SHA256 of current buffer |
| 152 | +func.register('file_sha256', function(bufnr) |
| 153 | + local text = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), '\n') |
| 154 | + return vim.fn.sha256(text) |
| 155 | +end) |
214 | 156 | ``` |
215 | 157 |
|
216 | | -When you edit and save the file, `last_modified` will automatically update to the current timestamp while preserving the original time format. |
217 | | - |
218 | | -## Silent Mode |
| 158 | +And share your useful generators in [Discussions](https://github.com/Fro-Q/headup.nvim/discussions)! |
219 | 159 |
|
220 | | -By default, the plugin operates silently. To see notifications when metadata is updated, set `silent = false` in your configuration: |
221 | | - |
222 | | -```lua |
223 | | -require("headup").setup({ |
224 | | - silent = false, -- Show notifications |
225 | | - -- other config... |
226 | | -}) |
227 | | -``` |
| 160 | +## Help |
228 | 161 |
|
229 | | -When `silent = false`, you'll see notifications like: |
230 | | -- "headup.nvim: Auto-updated timestamp to: 2024-11-02 15:30:45" |
231 | | -- "headup.nvim: Updated file size to: 2.1 KB" |
| 162 | +There is a most detailed help file included with the plugin. See in `:help headup.nvim`. |
232 | 163 |
|
233 | 164 | ## License |
234 | 165 |
|
235 | | -MIT License - see [LICENSE](LICENSE) file for details. |
| 166 | +<p align="center"> |
| 167 | +MIT - Copyright (c) 2025 Fro-Q |
| 168 | +</p> |
0 commit comments