diff --git a/.gitignore b/.gitignore index ba01f71..34a3924 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ +vim/doc/tags notesium diff --git a/CHANGELOG.md b/CHANGELOG.md index a5face2..d225c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,70 @@ +## 0.6.4 + +This release introduces the **finder** command, a built-in interactive +filtering and selection tool powered by the excellent **fzf engine**. It +enhances the CLI with flexible input handling and syntax-highlighted +previews, making it easier to search and navigate notes efficiently. The +finder works great as a standalone tool but becomes even more powerful +when integrated with other tools, such as Vim. + +Regarding Vim, Notesium now has a **Vim/Neovim plugin**, replacing the +previous example integration. The new plugin is easier to set up, +removes dependencies on `fzf`, `fzf.vim`, and `bat`, and seamlessly +integrates with the `finder` command. It is more robust, offers greater +flexibility, and enhances compatibility with both Vim and Neovim. + +This release **is backwards compatible**, but users transitioning from +the example Vim integration should review the notable changes below and +the updated documentation. + +Added: + +- Finder: Native interactive filter and selection TUI (embedded `fzf`). +- Finder: Input specified via end-of-options unix convention (`-- CMD [OPTS]`). +- Finder: Input supports List, Links, Links commands and their options. +- Finder: Input default `list --color --prefix=label --sort=alpha`. +- Finder: Preview with syntax highlighting and lineno support (`cat` command). +- Finder: Preview toggle with `Ctrl-/`. +- Finder: Configurable custom prompt. +- Completion: Updated to support Finder end-of-options. + +- Cat: Markdown syntax highlighting and concealment. +- Cat: Highlight - `header codeblock code blockquote plainlink listmarker`. +- Cat: Highlight and Concealment - `bold italic markdownlink`. +- Cat: Line number highlight. + +- Vim: Brand new Vim/Neovim plugin, installable via plugin managers. +- Vim: Integrates with Finder (removing dependency on `fzf`, `fzf.vim`, `bat`). +- Vim: Supports Neovim floating windows, with fallback for Vim without `term`. +- Vim: Auto enable/disable preview based on terminal width. +- Vim: Auto adjust window size for link-insertion based on terminal width. +- Vim: Documentation accessible via `:help notesium[-section]`. +- Vim: Settings - `g:notesium_(bin|mappings|weekstart|window|window_small)`. +- Vim: Commands - `Notesium(New|Daily|Weekly|List|Links|Lines|Web|InsertLink)`. +- Vim: Mappings - `(nn|nd|nw|nl|nm|nc|nk|ns|nW)`, `[[`. + +Changed: + +- Readme: Updated to include Finder command. +- Readme: Vim example integration replaced with new Vim/Neovim Plugin. +- Readme: Vim screenshots updated using new Vim/Neovim Plugin. + +**Vim plugin vs. Vim example integration**: + +- Fixed: Links filename enumeration, bang support. +- Fixed: InsertLink autocmd localized to buffer. +- Fixed: InsertLink auto adjust window size based on terminal width. +- Fixed: `nc` adheres to g:notesium_weekstart. +- Fixed: NotesiumWeb now supports custom and required arguments. +- Added: NotesiumWeb windows support via powershell. +- Added: g:notesium_bin to override hardcoded assumed `notesium`. +- Added: Callbacks for `editfile` and `insertlink`. +- Added: Plugin documentation accessible via `:help notesium[-section]`. +- Changed: NOTESIUM_WEEKSTART (int) replaced with g:notesium_weekstart (str). +- Changed: NotesiumSearch has been renamed to NotesiumLines. +- Removed: `nb` is not included in the plugin mappings. +- Removed: Dependencies on `fzf`, `fzf.vim` or a highlight tool such as `bat`. + ## 0.6.3 This release introduces a new **lines** `--filter` option with support diff --git a/README.md b/README.md index dc8417b..3d3b8ca 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,14 @@ It aspires and is designed to: - [Section folding](#section-folding) - [Syntax highlighting and concealment](#syntax-highlighting-and-concealment) - [Vim](#vim) - - [Example integration](#example-integration) + - [Setup](#setup) + - [Configuration](#configuration) + - [Commands](#commands) - [Keybindings](#keybindings-1) - - [Fzf search syntax](#fzf-search-syntax) + - [Finder search syntax](#finder-search-syntax) - [Related Vim settings](#related-vim-settings) - [Custom URI protocol](#custom-uri-protocol) - - [Example integration](#example-integration-1) + - [Example integration](#example-integration) - [Handler registration](#handler-registration) - [Design assumptions and rationale](#design-assumptions-and-rationale) - [Filenames are 8 hexidecimal digits](#filenames-are-8-hexidecimal-digits) @@ -105,6 +107,10 @@ It aspires and is designed to: - Supports line filtering queries with AND, OR, and NOT conditionals. - **Stats** - View counts of notes, labels, orphans, links, lines, words, etc. +- **Finder** + - Interactive filter selection TUI. + - Supports List, Links and Lines commands and their options as input. + - Optional preview window with syntax highlighting and concealment. - **Web** - Completely self-contained and runs locally. - Create and edit notes with web based editor (optional vim-mode). @@ -173,29 +179,30 @@ It aspires and is designed to:
*[Vim] List: prefixed with associated labels and sorted alphabetically* -![image: vim prefixed label](https://www.notesium.com/images/screenshot-1681733208.png) +![image: vim prefixed label](https://www.notesium.com/images/screenshot-1738853845.png)
*[Vim] List: prefixed with modification date and sorted per modification time* -![image: vim prefixed mtime](https://www.notesium.com/images/screenshot-1681733355.png) +![image: vim prefixed mtime](https://www.notesium.com/images/screenshot-1738854202.png)
*[Vim] Links: display all links* -![image: vim links all](https://www.notesium.com/images/screenshot-1681733482.png) +![image: vim links all](https://www.notesium.com/images/screenshot-1738854280.png)
*[Vim] Links: display links related to the current note* -![image: vim links related](https://www.notesium.com/images/screenshot-1681733712.png) +![image: vim links related](https://www.notesium.com/images/screenshot-1738854409.png)
*[Vim] Links: link insertion triggered by `[[`* -![image: vim link insertion](https://www.notesium.com/images/screenshot-1681734183.png) +![image: vim link insertion](https://www.notesium.com/images/screenshot-1738854615.png)
*[Vim] Lines: full text search (light theme)* -![image: vim full text search light](https://www.notesium.com/images/screenshot-1681734555.png) +![image: vim full text search light](https://www.notesium.com/images/screenshot-1738854816.png)
+ ## CLI Notesium is primarily tested and supported on Linux, with only @@ -289,6 +296,10 @@ Commands: stats Print statistics --color Color code using ansi escape sequences --table Format as table with whitespace delimited columns + finder Start finder (interactive filter selection TUI) + --preview Display note preview (toggle with ctrl-/) + --prompt=STR Set custom prompt text + -- CMD [OPTS] Input (default: list --color --prefix=label --sort=alpha) web Start web server --webroot=PATH Path to web root to serve (default: embedded webroot) --mount=DIR:URI Additional directory to serve under webroot (experimental) @@ -511,130 +522,82 @@ of the formatting characters is supported. ## Vim -Notesium does not supply a Vim plugin, it is up to the user to write -their own Vim commands and configure keybindings. That said, below are -some fairly generic commands, with preferences configured in the -keybindings. +Notesium provides a Vim/Neovim plugin that integrates with the Notesium +CLI, particularily the `finder` command providing and interactive filter +selection TUI with syntax highlighted preview. -- Dependencies: [fzf](https://github.com/junegunn/fzf) and [fzf.vim](https://github.com/junegunn/fzf.vim). -- Recommended: [bat](https://github.com/sharkdp/bat) for syntax highlighting in the preview. +- Depends: [notesium](#cli) (0.6.4 or above) - Recommended: [vim-markdown](https://github.com/preservim/vim-markdown) for general markdown goodness. -- Recommended: [goyo.vim](https://github.com/junegunn/goyo.vim) and [lightlight.vim](https://github.com/junegunn/limelight.vim) for distraction free writing. +- Recommended: [goyo.vim](https://github.com/junegunn/goyo.vim) and [limelight.vim](https://github.com/junegunn/limelight.vim) for distraction free writing. -### Example integration +### Setup -```vim -let $NOTESIUM_DIR = trim(system("notesium home")) -let $NOTESIUM_WEEKSTART = 1 "0 Sunday, 1 Monday, ... - -autocmd BufRead,BufNewFile $NOTESIUM_DIR/*.md inoremap [[ fzf#vim#complete({ - \ 'source': 'notesium list --sort=mtime', - \ 'options': '+s -d : --with-nth 3.. --prompt "NotesiumInsertLink> "', - \ 'reducer': {l->"[". split(l[0],':1: ')[1] ."](".split(l[0],':')[0].")"}, - \ 'window': {'width': 0.5, 'height': 0.5}}) - -command! -bang NotesiumNew - \ execute ":e" system("notesium new") - -command! -bang NotesiumWeb - \ let s:options = "--stop-on-idle --open-browser" | - \ execute ":silent !nohup notesium web ".s:options." > /dev/null 2>&1 &" - -command! -bang -nargs=* NotesiumList - \ let s:spec = {'dir': $NOTESIUM_DIR, 'options': '+s -d : --with-nth 3..'} | - \ call fzf#vim#grep( - \ 'notesium list '.join(map(split(), 'shellescape(v:val)'), ' '), 0, - \ &columns > 79 ? fzf#vim#with_preview(s:spec, 'right', 'ctrl-/') : s:spec, 0) - -command! -bang -nargs=* NotesiumLinks - \ let s:spec = {'dir': $NOTESIUM_DIR, 'options': '-d : --with-nth 3..'} | - \ call fzf#vim#grep( - \ 'notesium links '.join(map(split(), 'shellescape(v:val)'), ' '), 0, - \ &columns > 79 ? fzf#vim#with_preview(s:spec, 'right', 'ctrl-/') : s:spec, 0) - -command! -bang -nargs=* NotesiumSearch - \ let s:spec = {'dir': $NOTESIUM_DIR, 'options': '-d : --with-nth 3..'} | - \ call fzf#vim#grep( - \ 'notesium lines '.join(map(split(), 'shellescape(v:val)'), ' '), 0, - \ &columns > 79 ? fzf#vim#with_preview(s:spec, 'right', 'ctrl-/') : s:spec, 0) - -command! -bang -nargs=* NotesiumDaily - \ let s:cdate = empty() ? strftime('%Y-%m-%d') : | - \ let s:output = system('notesium new --verbose --ctime='.s:cdate.'T00:00:00') | - \ let s:filepath = matchstr(s:output, 'path:\zs[^\n]*') | - \ execute 'edit ' . s:filepath | - \ if getline(1) =~ '^\s*$' | - \ let s:epoch = matchstr(s:output, 'epoch:\zs[^\n]*') | - \ call setline(1, '# ' . strftime('%b %d, %Y (%A)', s:epoch)) | - \ endif - -command! -bang -nargs=* NotesiumWeekly - \ let s:date = empty() ? strftime('%Y-%m-%d') : | - \ let s:output = system('notesium new --verbose --ctime='.s:date.'T00:00:01') | - \ let s:epoch = str2nr(matchstr(s:output, 'epoch:\zs[^\n]*')) | - \ let s:day = strftime('%u', s:epoch) | - \ let s:startOfWeek = empty($NOTESIUM_WEEKSTART) ? 1 : $NOTESIUM_WEEKSTART | - \ let s:diff = (s:day - s:startOfWeek + 7) % 7 | - \ let s:weekBegEpoch = s:epoch - (s:diff * 86400) | - \ let s:weekBegDate = strftime('%Y-%m-%d', s:weekBegEpoch) | - \ let s:output = system('notesium new --verbose --ctime='.s:weekBegDate.'T00:00:01') | - \ let s:filepath = matchstr(s:output, 'path:\zs[^\n]*') | - \ execute 'edit ' . s:filepath | - \ if getline(1) =~ '^\s*$' | - \ let s:weekFmt = s:startOfWeek == 0 ? '%U' : '%V' | - \ let s:yearWeekStr = strftime('%G: Week' . s:weekFmt, s:weekBegEpoch) | - \ let s:weekBegStr = strftime('%a %b %d', s:weekBegEpoch) | - \ let s:weekEndStr = strftime('%a %b %d', s:weekBegEpoch + (6 * 86400)) | - \ let s:title = printf('# %s (%s - %s)', s:yearWeekStr, s:weekBegStr, s:weekEndStr) | - \ call setline(1, s:title) | - \ endif - -nnoremap nn :NotesiumNew -nnoremap nd :NotesiumDaily -nnoremap nw :NotesiumWeekly -nnoremap nl :NotesiumList --prefix=label --sort=alpha --color -nnoremap nm :NotesiumList --prefix=mtime --sort=mtime --color -nnoremap nc :NotesiumList --prefix=ctime --sort=ctime --color --date=2006-01 -nnoremap nb :NotesiumLinks --incoming =expand("%:t") -nnoremap nk :NotesiumLinks --color =expand("%:t") -nnoremap ns :NotesiumSearch --prefix=title --color -nnoremap nW :NotesiumWeb - -" overrides for journal -if $NOTESIUM_DIR =~ '**/journal/*' - nnoremap nl :NotesiumList --prefix=label --sort=mtime --color -endif +To install the plugin, add the repository to your plugin manager and +point its runtime path to the `'vim'` directory. For example: + +``` +" init.vim or .vimrc +Plug 'alonswartz/notesium', { 'rtp': 'vim' } + +-- init.lua +Plug('alonswartz/notesium', { ['rtp'] = 'vim' }) ``` +### Configuration + +| Setting | Comment | Default +| ------- | ------- | ------- +| `g:notesium_bin` | Binary name or path | `notesium` +| `g:notesium_mappings` | Enable(1) or disable(0) mappings | `1` +| `g:notesium_weekstart` | First day of the week | `monday` +| `g:notesium_window` | Finder Default | `{'width': 0.85, 'height': 0.85}` +| `g:notesium_window_small` | Finder InsertLink | `{'width': 0.50, 'height': 0.50}` + +Note: These settings should be set prior to the plugin being sourced. + +### Commands + +| Command | Comment +| ------- | ------- +| `:NotesiumNew` | Open new note for editing +| `:NotesiumDaily [YYYY-MM-DD]` | Open new or existing daily note +| `:NotesiumWeekly [YYYY-MM-DD]` | Open new or existing weekly note +| `:NotesiumList [LIST_OPTS]` | Open finder: list of notes +| `:NotesiumLines [LINES_OPTS]` | Open finder: lines of all notes +| `:NotesiumLinks [LINKS_OPTS]` | Open finder: links of all notes +| `:NotesiumLinks! [LINKS_OPTS]` | Open finder: links of the active note +| `:NotesiumInsertLink [LIST_OPTS]` | Open finder: insert selection as markdown link +| `:NotesiumWeb [WEB_OPTS]` | Start web server, open browser (stop on idle) + +Note: `NotesiumWeekly` depends on `g:notesium_weekstart`. + ### Keybindings -| Mode | Binding | Comment -| ---- | ------- | ------- -| insert | `[[` | Opens note list, insert selection as markdown formatted link -| normal | `nn` | Opens new note for editing -| normal | `nd` | Opens new or existing daily note -| normal | `nw` | Opens new or existing weekly note -| normal | `nl` | List with prefixed label, sorted alphabetically (mtime if journal) -| normal | `nm` | List with prefixed date modified, sorted by mtime -| normal | `nc` | List with prefixed date created in custom format, sorted by ctime -| normal | `nb` | List all notes linking to this note (backlinks) -| normal | `nk` | List all links related to this note -| normal | `ns` | Full text search -| normal | `nW` | Opens browser with web view (auto stop webserver on idle) -| fzf | `C-k` `C-j` | Move up and down in fzf window -| fzf | `Enter` | Open selection -| fzf | `C-t` `C-x` `C-v` | Open selection in new tab, split, vertical split -| fzf | `C-/` | Toggle preview -| fzf | `Shift-Tab` | Multiple selection -| normal | `ge` | Open the link under the cursor (vim-markdown) -| normal | `[[` `]]` | Jump back and forward between headings (vim-markdown) - -### Fzf search syntax +| Mode | Binding | Comment +| ---- | ------- | ------- +| insert | `[[` | Opens note list, insert selection as markdown formatted link +| normal | `nn` | Opens new note for editing +| normal | `nd` | Opens new or existing daily note +| normal | `nw` | Opens new or existing weekly note +| normal | `nl` | List with prefixed label, sorted alphabetically; mtime if journal +| normal | `nm` | List with prefixed date modified, sorted by mtime +| normal | `nc` | List with prefixed date created `(YYYY/WeekXX)`, sorted by ctime +| normal | `nk` | List all links related to active note (or all if none) +| normal | `ns` | Full text search with prefixed note title +| normal | `nW` | Opens browser with embedded web/app (auto stop webserver on idle) +| finder | `C-j` `↓` | Select next entry (down) +| finder | `C-k` `↑` | Select previous entry (up) +| finder | `C-/` | Toggle preview +| finder | `Enter` | Submit selected entry +| finder | `Esc` | Dismiss finder +| normal | `ge` | Open the link under the cursor (vim-markdown) +| normal | `[[` `]]` | Jump back and forward between headings (vim-markdown) + +### Finder search syntax | Token | Match Type | Comment | ----- | ---------- | ------- -| `sbtrkt` | fuzzy-match | Items that fuzzy match `sbtrkt` -| `'word` | exact-match | Items that include `word` +| `word` | exact-match | Items that include `word` | `^word` | prefix exact-match | Items that start with `word` | `word$` | suffix exact-match | Items that end with `word` | `!word` | inverse exact-match | Items that do not include `word` @@ -642,27 +605,11 @@ endif | `!word$` | inverse suffix exact-match | Items that do not end with `word` | `foo bar` | multiple exact match (AND) | Items that include both `foo` AND `bar` | `foo \| bar` | multiple exact match (OR) | Items that include either `foo` OR `bar` +| `'sbtrkt` | fuzzy-match | Items that fuzzy match `sbtrkt` ### Related Vim settings ```vim -" junegunn/fzf.vim -let $FZF_DEFAULT_OPTS="--reverse --filepath-word --no-separator --no-scrollbar " -let g:fzf_buffers_jump = 1 -let g:fzf_layout = { 'window': { 'width': 0.85, 'height': 0.85 } } -let g:fzf_colors = { - \ 'fg': ['fg', 'Normal'], - \ 'bg': ['bg', 'Normal'], - \ 'hl': ['fg', 'Comment'], - \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], - \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], - \ 'hl+': ['fg', 'Statement'], - \ 'info': ['fg', 'PreProc'], - \ 'pointer': ['fg', 'Exception'], - \ 'marker': ['fg', 'Keyword'], - \ 'spinner': ['fg', 'Label'], - \ 'header': ['fg', 'Comment'] } - " preservim/vim-markdown let g:vim_markdown_folding_style_pythonic = 1 let g:vim_markdown_folding_level = 2 @@ -671,9 +618,9 @@ let g:vim_markdown_auto_insert_bullets = 0 let g:vim_markdown_new_list_item_indent = 0 let g:vim_markdown_toc_autofit = 1 let g:vim_markdown_conceal_code_blocks = 0 - let g:markdown_fenced_languages = ['json', 'sh', 'shell=bash'] hi def link mkdHeading htmlH1 + autocmd FileType markdown setlocal conceallevel=2 ``` @@ -874,4 +821,4 @@ PAUSE=y bats tests/list.bats --tap The MIT License (MIT) -Copyright (c) 2023-2024 Alon Swartz +Copyright (c) 2023-2025 Alon Swartz diff --git a/completion.bash b/completion.bash index ad53820..dd2ed52 100644 --- a/completion.bash +++ b/completion.bash @@ -19,6 +19,20 @@ __notesium_complete() { # handle options with equals. COMP_WORDBREAKS is global. _get_comp_words_by_ref -n = cur prev + + if [[ "${COMP_WORDS[1]}" == "finder" ]]; then + if [[ "${prev}" == "--" ]]; then + words="$(echo -e "list\nlinks\nlines")" + else + for ((i = 1; i < ${#COMP_WORDS[@]} - 1; i++)); do + if [[ "${COMP_WORDS[i]}" == "--" ]]; then + words="$(__notesium_opts "${COMP_WORDS[i+1]}")" + break + fi + done + fi + fi + case ${cur} in --prefix=*|--sort=*) prev="${cur%%=*}=" diff --git a/finder.go b/finder.go new file mode 100644 index 0000000..cf69924 --- /dev/null +++ b/finder.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + + fzf "github.com/junegunn/fzf/src" +) + +type channelWriter struct { + ch chan string +} + +func (cw *channelWriter) Write(p []byte) (n int, err error) { + str := string(p) // Convert bytes to string + cw.ch <- str // Send to channel + return len(p), nil +} + +func runFinder(inputChan chan string, opts []string) ([]string, int, error) { + options, err := fzf.ParseOptions(false, opts) + if err != nil { + return nil, 2, fmt.Errorf("fzf error: %w", err) + } + + outputChan := make(chan string) + resultChan := make(chan struct { + code int + err error + }, 1) + + options.Input = inputChan + options.Output = outputChan + + go func() { + code, runErr := fzf.Run(options) + close(outputChan) + + resultChan <- struct { + code int + err error + }{code, runErr} + + close(resultChan) + }() + + var lines []string + for line := range outputChan { + lines = append(lines, line) + } + + result := <-resultChan + return lines, result.code, result.err +} + diff --git a/go.mod b/go.mod index a2aa4a2..eb63b8d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,18 @@ module github.com/alonswartz/notesium -go 1.16 +go 1.20 + +require ( + github.com/charlievieth/fastwalk v1.0.9 // indirect + github.com/gdamore/encoding v1.0.1 // indirect + github.com/gdamore/tcell/v2 v2.8.1 // indirect + github.com/junegunn/fzf v0.58.0 // indirect + github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b1edd03 --- /dev/null +++ b/go.sum @@ -0,0 +1,87 @@ +github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0= +github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI= +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= +github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= +github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= +github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/junegunn/fzf v0.58.0 h1:sT6lO4OTkHpEHpr8E1iZz6bvxZ6URHjTYl8/yhS8s1U= +github.com/junegunn/fzf v0.58.0/go.mod h1:IsDYaa3WFbMYYi8yp92fQFTqN10hs3nH4OMBiz/kJXo= +github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw= +github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/highlight.go b/highlight.go new file mode 100644 index 0000000..91f52fb --- /dev/null +++ b/highlight.go @@ -0,0 +1,167 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strings" +) + +const ( + ansiHeading = "\033[1m" + ansiBold = "\033[1m" + ansiItalic = "\033[3m" + ansiLink = "\033[34m" + ansiCodeBlock = "\033[33m" + ansiInlineCode = "\033[33m" + ansiBlockQuote = "\033[36m" + ansiListMarker = "\033[36m" + ansiLineBg = "\033[40m" + ansiReset = "\033[0m" +) +var ( + reBold = regexp.MustCompile(`\*\*(.*?)\*\*`) + reBoldAlt = regexp.MustCompile(`__(.*?)__`) + reItalic = regexp.MustCompile(`\*(.*?)\*`) + reItalicAlt = regexp.MustCompile(`_(.*?)_`) + reUnorderedList = regexp.MustCompile(`^(\s*[-+*]) `) + reOrderedList = regexp.MustCompile(`^(\s*\d+\.) `) + reInlineCode = regexp.MustCompile("`(.*?)`") + reLinkPlain = regexp.MustCompile(`(?:https?://|www\.)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/[^\s]*)?`) + reLinkMarkdown = regexp.MustCompile(`\[(.[^]]*?)\]\((.[^)]*?)\)`) + reAnsi = regexp.MustCompile(`\x1b\[[0-9;]*m`) + reReset = regexp.MustCompile(`\x1b\[0m`) +) + +func renderMarkdown(reader io.Reader, writer io.Writer, lineNumber int) { + inCodeBlock := false + scanner := bufio.NewScanner(reader) + + for lineIndex := 1; scanner.Scan(); lineIndex++ { + line := scanner.Text() + highlightedLine := highlightLine(line, &inCodeBlock) + if lineNumber > 0 && lineNumber == lineIndex { + highlightedLine = highlightLineWithBackground(highlightedLine) + } + fmt.Fprintln(writer, highlightedLine) + } + + if err := scanner.Err(); err != nil { + fmt.Fprintf(writer, "Error reading content: %v\n", err) + } +} + +func highlightLine(line string, inCodeBlock *bool) string { + // Code blocks + if strings.HasPrefix(line, "```") { + *inCodeBlock = !*inCodeBlock + return ansiCodeBlock + line + ansiReset + } + if *inCodeBlock { + return ansiCodeBlock + line + ansiReset + } + + // Headers + if strings.HasPrefix(line, "#") { + return ansiHeading + line + ansiReset + } + + // Blockquotes + if strings.HasPrefix(line, "> ") { + return ansiBlockQuote + line + ansiReset + } + + // Inline code + matches := reInlineCode.FindAllStringSubmatchIndex(line, -1) + if len(matches) > 0 { + return highlightLineWithInlineCode(line, matches) + } + + return highlightString(line) +} + +func highlightString(line string) string { + // Links + line = highlightLink(line, reLinkMarkdown, ansiLink) + line = highlightRegex(line, reLinkPlain, ansiLink, 0) + + // Bold (**text** or __text__) + line = highlightRegex(line, reBold, ansiBold, 2) + line = highlightRegex(line, reBoldAlt, ansiBold, 2) + + // Italic (*text* or _text_) + line = highlightRegex(line, reItalic, ansiItalic, 1) + line = highlightRegex(line, reItalicAlt, ansiItalic, 1) + + // List markers + line = highlightRegex(line, reUnorderedList, ansiListMarker, 0) + line = highlightRegex(line, reOrderedList, ansiListMarker, 0) + + return line +} + +func highlightRegex(line string, re *regexp.Regexp, ansiCode string, markerLength int) string { + return re.ReplaceAllStringFunc(line, func(match string) string { + inner := match[markerLength : len(match)-markerLength] + return ansiCode + inner + ansiReset + }) +} + +func highlightLink(line string, re *regexp.Regexp, ansiCode string) string { + return re.ReplaceAllStringFunc(line, func(match string) string { + matches := re.FindStringSubmatch(match) + if len(matches) >= 2 { + title := matches[1] + return ansiCode + title + ansiReset + } + return match + }) +} + +func highlightLineWithInlineCode(line string, matches [][]int) string { + var builder strings.Builder + prevIndex := 0 + + for _, match := range matches { + start, end := match[0], match[1] + groupStart, groupEnd := match[2], match[3] + + // Handle text before inline code + if start > prevIndex { + builder.WriteString(highlightString(line[prevIndex:start])) + } + + builder.WriteString(ansiInlineCode) + builder.WriteString(line[groupStart:groupEnd]) + builder.WriteString(ansiReset) + + prevIndex = end + } + + // Handle text after inline code + if prevIndex < len(line) { + builder.WriteString(highlightString(line[prevIndex:])) + } + + return builder.String() +} + + +func highlightLineWithBackground(highlightedLine string) string { + // apply bg after resets to handle segments + highlightedLine = reReset.ReplaceAllStringFunc(highlightedLine, func(reset string) string { + return reset + ansiLineBg + }) + + // apply padding + termWidth := 79 + visibleChars := len(reAnsi.ReplaceAllString(highlightedLine, "")) + requiredPadding := termWidth - visibleChars + if requiredPadding > 0 { + padding := strings.Repeat(" ", requiredPadding) + highlightedLine += ansiLineBg + padding + } + + return ansiLineBg + highlightedLine + ansiReset +} diff --git a/notesium.go b/notesium.go index cee4334..9bc46d2 100644 --- a/notesium.go +++ b/notesium.go @@ -58,6 +58,10 @@ func main() { notesiumLines(notesiumDir, cmd.Options.(linesOptions), os.Stdout) case "stats": notesiumStats(notesiumDir, cmd.Options.(statsOptions), os.Stdout) + case "finder": + notesiumFinder(notesiumDir, cmd.Options.(finderOptions)) + case "cat": + notesiumCat(notesiumDir, cmd.Options.(catOptions)) case "web": notesiumWeb(notesiumDir, cmd.Options.(webOptions)) case "extract": @@ -331,6 +335,83 @@ func notesiumStats(dir string, opts statsOptions, w io.Writer) { fmt.Fprintf(w, keyFormat+" %d\n", "chars", chars) } +func notesiumFinder(dir string, opts finderOptions) { + inputCmd, err := parseOptions(opts.input) + if err != nil { + log.Fatal(err) + } + + optsFzf := []string{ + "--ansi", + "--exact", + "--border", + "--reverse", + "--no-sort", + "--no-height", + "--no-scrollbar", + "--no-separator", + "--pointer=>", + "--delimiter=:", + "--with-nth=3..", + "--color=bg:8,bg+:0,fg:12,fg+:12,hl:11,hl+:3,pointer:9,info:3", + fmt.Sprintf("--prompt=%s> ", opts.prompt), + } + + if opts.preview { + executablePath, err := os.Executable() + if err != nil { + log.Fatalf("error resolving executable path: %v", err) + } + + executablePath, err = filepath.EvalSymlinks(executablePath) + if err != nil { + log.Fatalf("error resolving symlinked executable path: %v", err) + } + + optsFzf = append(optsFzf, + "--bind=ctrl-/:toggle-preview", + "--preview-window=+{2}-/2", + fmt.Sprintf("--preview=%s cat {}", executablePath), + ) + } + + inputChan := make(chan string) + go func() { + defer close(inputChan) + writer := &channelWriter{ch: inputChan} + + switch inputCmd.Name { + case "list": + notesiumList(dir, inputCmd.Options.(listOptions), writer) + case "links": + notesiumLinks(dir, inputCmd.Options.(linksOptions), writer) + case "lines": + notesiumLines(dir, inputCmd.Options.(linesOptions), writer) + default: + log.Fatal("input command not supported: ", inputCmd.Name) + } + }() + + results, code, err := runFinder(inputChan, optsFzf) + if code != 0 && code != 130 && err != nil { + fmt.Fprintf(os.Stderr, "Error running fzf: %v\n", err) + } + for _, line := range results { + fmt.Print(line) + } + os.Exit(code) +} + +func notesiumCat(dir string, opts catOptions) { + path := filepath.Join(dir, opts.filename) + file, err := os.Open(path) + if err != nil { + log.Fatalf("Error opening file: %v\n", err) + } + defer file.Close() + renderMarkdown(file, os.Stdout, opts.lineNumber) +} + func notesiumWeb(dir string, opts webOptions) { populateCache(dir) diff --git a/options.go b/options.go index b65c695..5edca00 100644 --- a/options.go +++ b/options.go @@ -35,6 +35,10 @@ Commands: stats Print statistics --color Color code using ansi escape sequences --table Format as table with whitespace delimited columns + finder Start finder (interactive filter selection TUI) + --preview Display note preview (toggle with ctrl-/) + --prompt=STR Set custom prompt text + -- CMD [OPTS] Input (default: list --color --prefix=label --sort=alpha) web Start web server --webroot=PATH Path to web root to serve (default: embedded webroot) --mount=DIR:URI Additional directory to serve under webroot (experimental) @@ -82,6 +86,17 @@ type linesOptions struct { filter string } +type finderOptions struct { + input []string + prompt string + preview bool +} + +type catOptions struct { + filename string + lineNumber int +} + type statsOptions struct { color Color table bool @@ -231,6 +246,29 @@ func parseOptions(args []string) (Command, error) { cmd.Options = opts return cmd, nil + case "finder": + opts := finderOptions{} + opts.input = []string{"list", "--color", "--prefix=label", "--sort=alpha"} + for i, opt := range args[1:] { + if opt == "--" { + opts.input = args[i+2:] + if len(opts.input) == 0 { + return Command{}, fmt.Errorf("input command not specified") + } + break + } + switch { + case opt == "--preview": + opts.preview = true + case strings.HasPrefix(opt, "--prompt="): + opts.prompt = strings.TrimPrefix(opt, "--prompt=") + default: + return Command{}, fmt.Errorf("unrecognized option: %s", opt) + } + } + cmd.Options = opts + return cmd, nil + case "stats": opts := statsOptions{} for _, opt := range args[1:] { @@ -297,6 +335,24 @@ func parseOptions(args []string) (Command, error) { cmd.Options = opts return cmd, nil + case "cat": + opts := catOptions{} + opts.lineNumber = 0 + if len(args) != 2 { + return Command{}, fmt.Errorf("filename not specified or too many arguments") + } + + parts := strings.Split(args[1], ":") + opts.filename = parts[0] + if len(parts) > 1 { + if num, err := strconv.Atoi(parts[1]); err == nil && num > 0 { + opts.lineNumber = num + } + } + + cmd.Options = opts + return cmd, nil + case "version": opts := versionOptions{} for _, opt := range args[1:] { diff --git a/vim/doc/notesium.txt b/vim/doc/notesium.txt new file mode 100644 index 0000000..5221ef7 --- /dev/null +++ b/vim/doc/notesium.txt @@ -0,0 +1,123 @@ +notesium.txt Notesium Vim Plugin Last change: Feb 06 2025 +TABLE OF CONTENTS *notesium* *notesium-toc* +============================================================================== + + Notesium |notesium| + Setup |notesium-setup| + Configuration |notesium-config| + Commands |notesium-commands| + Mappings |notesium-mappings| + Finder |notesium-finder| + License |notesium-license| + +NOTESIUM *notesium* +============================================================================== + +Notesium - A simple yet powerful system for networked thought. +> + Writing does not make intellectual endeavours easier, it makes them + possible. Deepen understanding, insight, and allow for structure to emerge + organically by linking notes. +< +See the {Website}{1} and {GitHub}{2} repository for more details. + + {1} https://www.notesium.com + {2} https://github.com/alonswartz/notesium + +SETUP *notesium-setup* +============================================================================== + +The Notesium Vim plugin provides an interface for interacting with Notesium +from within Vim/NeoVim, and therefore requires the `notesium` binary to be +installed. + +To install the plugin, add the repository to your plugin manager and +point its runtime path to the `'vim'` directory. For example: +> + " init.vim or .vimrc + Plug 'alonswartz/notesium', { 'rtp': 'vim' } + + -- init.lua + Plug('alonswartz/notesium', { ['rtp'] = 'vim' }) +< +Note: The plugin depends on notesium 0.6.4 or above. + +CONFIGURATION *notesium-config* +============================================================================== + +`g:notesium_bin` Binary name or path `notesium` +`g:notesium_mappings` Enable(1) or disable(0) mappings `1` +`g:notesium_weekstart` First day of the week `monday` +`g:notesium_window` Finder Default `{'width': 0.85, 'height': 0.85}` +`g:notesium_window_small` Finder InsertLink `{'width': 0.50, 'height': 0.50}` + +Note: These settings should be set prior to the plugin being sourced. + +COMMANDS *notesium-commands* +============================================================================== + +`:NotesiumNew` Open new `note` for editing +`:NotesiumDaily [YYYY-MM-DD]` Open new or existing daily `note` +`:NotesiumWeekly [YYYY-MM-DD]` Open new or existing weekly `note` +`:NotesiumList [LIST_OPTS]` Open finder: list of notes +`:NotesiumLines [LINES_OPTS]` Open finder: lines of all notes +`:NotesiumLinks [LINKS_OPTS]` Open finder: links of all notes +`:NotesiumLinks! [LINKS_OPTS]` Open finder: links of the active `note` +`:NotesiumInsertLink [LIST_OPTS]` Open finder: insert selection as markdown link +`:NotesiumWeb [WEB_OPTS]` Start web server, open browser (stop on idle) + +Note: `NotesiumWeekly` depends on `g:notesium_weekstart`. + +MAPPINGS *notesium-mappings* +============================================================================== + +INSERT MODE + +`[[` Opens `note` list, insert selection as markdown formatted link + +NORMAL MODE + +`nn` Opens new `note` for editing +`nd` Opens new or existing daily `note` +`nw` Opens new or existing weekly `note` +`nl` List with prefixed label, sorted alphabetically; mtime if journal +`nm` List with prefixed date modified, sorted by mtime +`nc` List with prefixed date created `(YYYY/WeekXX)`, sorted by ctime +`nk` List all links related to active `note` (or all if none) +`ns` Full text search with prefixed `note` title +`nW` Opens browser with embedded web/app (auto stop webserver on idle) + +Note: The mappings can be enabled/disabled via `g:notesium_mappings`. + +FINDER *notesium-finder* +============================================================================== + +KEYBINDINGS + +`C-j` Select next entry (down) +`C-k` Select previous entry (up) +`C-/` Toggle preview +`Enter` Submit selected entry +`Esc` Dismiss finder + +SEARCH SYNTAX + +`word` Exact-match Items that include `word` +`^word` Prefix exact-match Items that start with `word` +`word$` Suffix exact-match Items that end with `word` +`!word` Inverse exact-match Items that do not include `word` +`!^word` Inverse prefix exact-match Items that do not start with `word` +`!word$` Inverse suffix exact-match Items that do not end with `word` +`foo bar` Multiple exact match (AND) Items that include both `foo` AND `bar` +`foo | bar` Multiple exact match (OR) Items that include either `foo` OR `bar` +`'sbtrkt` Fuzzy-match Items that fuzzy match `sbtrkt` + +LICENSE *notesium-license* +============================================================================== + +The MIT License (MIT) + +Copyright (c) 2023-2025 Alon Swartz + +============================================================================== +vim:tw=78:sw=2:ts=2:ft=help:nowrap: diff --git a/vim/plugin/notesium.vim b/vim/plugin/notesium.vim new file mode 100644 index 0000000..5fcf97a --- /dev/null +++ b/vim/plugin/notesium.vim @@ -0,0 +1,251 @@ +" vim:foldmethod=marker + +" Notesium configuration {{{1 +" ---------------------------------------------------------------------------- + +if !exists('g:notesium_bin') || empty(g:notesium_bin) + let g:notesium_bin = 'notesium' +endif + +if !exists('g:notesium_mappings') || empty(g:notesium_mappings) + let g:notesium_mappings = 1 +endif + +if !exists('g:notesium_weekstart') || empty(g:notesium_weekstart) + let g:notesium_weekstart = 'monday' +endif + +if !exists('g:notesium_window') || empty(g:notesium_window) + let g:notesium_window = {'width': 0.85, 'height': 0.85} +endif + +if !exists('g:notesium_window_small') || empty(g:notesium_window_small) + let g:notesium_window_small = {'width': 0.5, 'height': 0.5} +endif + +if !executable(g:notesium_bin) + echoerr "notesium_bin not found: " . g:notesium_bin + finish +endif + +function! notesium#get_notesium_dir() abort + let l:output = systemlist(g:notesium_bin . ' home') + if empty(l:output) || v:shell_error + echoerr "Failed to get NOTESIUM_DIR - " . join(l:output, "\n") + return '' + endif + return l:output[0] +endfunction + +let $NOTESIUM_DIR = notesium#get_notesium_dir() + +" Notesium finder {{{1 +" ---------------------------------------------------------------------------- + +if has('nvim') + + function! notesium#finder(config) abort + " Prepare command + let l:cmd = g:notesium_bin . ' finder ' . get(a:config, 'options', '') + let l:cmd .= ' -- ' . get(a:config, 'input', '') + + " Set window dimensions + let l:width = float2nr(&columns * get(a:config['window'], 'width', 1)) + let l:height = float2nr(&lines * get(a:config['window'], 'height', 1)) + let l:opts = { + \ 'relative': 'editor', + \ 'style': 'minimal', + \ 'row': (&lines - l:height) / 2, + \ 'col': (&columns - l:width) / 2, + \ 'width': l:width, + \ 'height': l:height } + + " Create buffer and floating window + highlight link NormalFloat Normal + let l:buf = nvim_create_buf(v:false, v:true) + let l:win = nvim_open_win(l:buf, v:true, l:opts) + + " Start the finder + call termopen(l:cmd, { + \ 'on_exit': { + \ job_id, exit_code, _ -> + \ notesium#finder_finalize(exit_code, l:buf, a:config['callback']) }}) + + " Focus the terminal and switch to insert mode + call nvim_set_current_win(l:win) + call feedkeys('i', 'n') + endfunction + + function! notesium#finder_finalize(exit_code, buf, callback) abort + " Capture buffer output, cleanup and validate + let l:output = trim(join(getbufline(a:buf, 1, '$'), "\n")) + if bufexists(a:buf) + execute 'bwipeout!' a:buf + endif + if empty(l:output) || a:exit_code == 130 + return + endif + if a:exit_code != 0 + echoerr printf("Finder error (%d): %s", a:exit_code, l:output) + return + endif + + " Parse output (filename:linenumber: text) and pass to callback + let l:parts = split(l:output, ':') + if len(l:parts) < 3 + echoerr "Invalid finder output: " . l:output + return + endif + let l:text = trim(join(l:parts[2:], ':')) + call a:callback(l:parts[0], l:parts[1], l:text) + endfunction + +else + + function! notesium#finder(config) abort + " Prepare the command + let l:cmd = g:notesium_bin . ' finder ' . get(a:config, 'options', '') + let l:cmd .= ' -- ' . get(a:config, 'input', '') + + " Start the finder + let l:output = system(l:cmd) + redraw! + if empty(l:output) || v:shell_error + return + endif + + " Parse output (filename:linenumber: text) and pass to callback + let l:parts = split(l:output, ':') + if len(l:parts) < 3 + echoerr "Invalid finder output: " . l:output + return + endif + let l:text = join(l:parts[2:], ':') + let l:text = substitute(l:text, '^\_s\+\|\_s\+$', '', 'g') + silent! call a:config['callback'](l:parts[0], l:parts[1], l:text) + endfunction + +endif + +" Notesium finder callbacks {{{1 +" ---------------------------------------------------------------------------- + +function! notesium#finder_callback_editfile(filename, linenumber, text) abort + let l:file_path = fnamemodify($NOTESIUM_DIR, ':p') . a:filename + execute 'edit' fnameescape(l:file_path) + execute a:linenumber . 'normal! zz' +endfunction + +function! notesium#finder_callback_insertlink(filename, linenumber, text) abort + let l:link = printf("[%s](%s)", a:text, a:filename) + call feedkeys((mode() == 'i' ? '' : 'a') . l:link, 'n') +endfunction + +" Notesium commands {{{1 +" ---------------------------------------------------------------------------- + +command! NotesiumNew + \ execute ":e" system(g:notesium_bin . ' new') + +command! -nargs=* NotesiumInsertLink + \ call notesium#finder({ + \ 'input': 'list ' . join(map(split(), 'shellescape(v:val)'), ' '), + \ 'options': '--prompt=NotesiumInsertLink', + \ 'callback': function('notesium#finder_callback_insertlink'), + \ 'window': (&columns > 79 ? g:notesium_window_small : g:notesium_window) }) + +command! -nargs=* NotesiumList + \ call notesium#finder({ + \ 'input': 'list ' . join(map(split(), 'shellescape(v:val)'), ' '), + \ 'options': '--prompt=NotesiumList' . (&columns > 79 ? ' --preview' : ''), + \ 'callback': function('notesium#finder_callback_editfile'), + \ 'window': g:notesium_window }) + +command! -bang -nargs=* NotesiumLinks + \ let s:is_note = expand("%:t") =~# '^[0-9a-f]\{8\}\.md$' | + \ let s:filename = ("" == "!" && s:is_note) ? expand("%:t") : '' | + \ let s:args = . (!empty(s:filename) ? ' ' . s:filename : '') | + \ call notesium#finder({ + \ 'input': 'links ' . join(map(split(s:args), 'shellescape(v:val)'), ' '), + \ 'options': '--prompt=NotesiumLinks' . (&columns > 79 ? ' --preview' : ''), + \ 'callback': function('notesium#finder_callback_editfile'), + \ 'window': g:notesium_window }) + +command! -nargs=* NotesiumLines + \ call notesium#finder({ + \ 'input': 'lines ' . join(map(split(), 'shellescape(v:val)'), ' '), + \ 'options': '--prompt=NotesiumLines' . (&columns > 79 ? ' --preview' : ''), + \ 'callback': function('notesium#finder_callback_editfile'), + \ 'window': g:notesium_window }) + +command! -nargs=* NotesiumDaily + \ let s:cdate = empty() ? strftime('%Y-%m-%d') : | + \ let s:output = system(g:notesium_bin.' new --verbose --ctime='.s:cdate.'T00:00:00') | + \ let s:filepath = matchstr(s:output, 'path:\zs[^\n]*') | + \ execute 'edit' fnameescape(s:filepath) | + \ if getline(1) =~ '^\s*$' | + \ let s:epoch = matchstr(s:output, 'epoch:\zs[^\n]*') | + \ call setline(1, '# ' . strftime('%b %d, %Y (%A)', s:epoch)) | + \ endif + +command! -nargs=* NotesiumWeekly + \ let s:daysMap = {'sunday': 0, 'monday': 1, 'tuesday': 2,'wednesday': 3, 'thursday': 4, 'friday': 5, 'saturday': 6} | + \ let s:startOfWeek = get(s:daysMap, g:notesium_weekstart, -1) | + \ if s:startOfWeek == -1 | + \ throw "Invalid g:notesium_weekstart: " . g:notesium_weekstart | + \ endif | + \ let s:date = empty() ? strftime('%Y-%m-%d') : | + \ let s:output = system(g:notesium_bin.' new --verbose --ctime='.s:date.'T00:00:01') | + \ let s:epoch = str2nr(matchstr(s:output, 'epoch:\zs[^\n]*')) | + \ let s:day = strftime('%u', s:epoch) | + \ let s:diff = (s:day - s:startOfWeek + 7) % 7 | + \ let s:weekBegEpoch = s:epoch - (s:diff * 86400) | + \ let s:weekBegDate = strftime('%Y-%m-%d', s:weekBegEpoch) | + \ let s:output = system('notesium new --verbose --ctime='.s:weekBegDate.'T00:00:01') | + \ let s:filepath = matchstr(s:output, 'path:\zs[^\n]*') | + \ execute 'edit' fnameescape(s:filepath) | + \ if getline(1) =~ '^\s*$' | + \ let s:weekFmt = s:startOfWeek == 0 ? '%U' : '%V' | + \ let s:yearWeekStr = strftime('%G: Week' . s:weekFmt, s:weekBegEpoch) | + \ let s:weekBegStr = strftime('%a %b %d', s:weekBegEpoch) | + \ let s:weekEndStr = strftime('%a %b %d', s:weekBegEpoch + (6 * 86400)) | + \ let s:title = printf('# %s (%s - %s)', s:yearWeekStr, s:weekBegStr, s:weekEndStr) | + \ call setline(1, s:title) | + \ endif + +command! -nargs=* NotesiumWeb + \ let s:r_args = ["--stop-on-idle", "--open-browser"] | + \ let s:q_args = filter(split(), 'index(s:r_args, v:val) == -1') + s:r_args | + \ let s:args = join(map(s:q_args, 'shellescape(v:val)'), ' ') | + \ if has('unix') | + \ execute ":silent !nohup ".g:notesium_bin." web ".s:args." > /dev/null 2>&1 &" | + \ elseif has('win32') || has('win64') | + \ execute ":silent !powershell -Command \"Start-Process -NoNewWindow ".g:notesium_bin." -ArgumentList 'web ".s:args."'\"" | + \ else | + \ throw "Unsupported platform" | + \ endif + +" Notesium mappings {{{1 +" ---------------------------------------------------------------------------- + +if g:notesium_mappings + autocmd BufRead,BufNewFile $NOTESIUM_DIR/*.md inoremap [[ :NotesiumInsertLink --sort=mtime + nnoremap nn :NotesiumNew + nnoremap nd :NotesiumDaily + nnoremap nw :NotesiumWeekly + nnoremap nl :NotesiumList --prefix=label --sort=alpha --color + nnoremap nm :NotesiumList --prefix=mtime --sort=mtime --color + nnoremap nc :NotesiumList --prefix=ctime --sort=ctime --color --date=2006/Week%V + nnoremap nk :NotesiumLinks! --color + nnoremap ns :NotesiumLines --prefix=title --color + nnoremap nW :NotesiumWeb + + " overrides + if g:notesium_weekstart ==# 'sunday' + nnoremap nc :NotesiumList --prefix=ctime --sort=ctime --color --date=2006/Week%U + endif + + if $NOTESIUM_DIR =~ '**/journal/*' + nnoremap nl :NotesiumList --prefix=label --sort=mtime --color + endif +endif