β‹…π•­π–†π–˜π–†π–‘π–™β‹…

Changelog

0.12.7 (Aug, 15 2026)#

Added#

Long source lines are shown raw (unwrapped) while editing, so the cursor could move past the right edge and disappear. Track a horizontal scroll offset and pan the document so the cursor stays visible, mirroring the existing vertical follow logic.

When the document pans, full-width fills must extend with it or they stop short of the right edge. Thread the scroll offset through the render path so code-block backgrounds, heading underlines, and the title rule span width + offset, covering every visible block, not just the one being edited.

Tables were parsed away as paragraphs and never drawn. The note editor now renders GFM tables as bordered boxes in reading view. Columns size to their content and, when the table is too wide to fit, share the spare width in proportion to each column's demand the way browsers lay out tables, so a long column takes the most slack without starving the others, and cell text wraps to fit.

While editing, a table renders as the same box but reveals the row under the cursor as raw markdown so its pipes stay editable, and every row including the delimiter is reachable. The box is only drawn while the buffer still parses as a table, so the moment the syntax breaks the block falls back to raw line by line editing and a half broken table never hides itself.

A thematic break is now kept as a plain paragraph instead of being dropped, because a table whose pipes are deleted degrades into a bare dash row that would otherwise vanish from the document.

Add vim-style visual selection to the note editor: v (charwise) and V (linewise) select text, y yanks the selection to the clipboard and esc cancels. A brief flash highlights the yanked range to acknowledge the copy.

The selection is highlighted per character and clipped to the editor viewport. Clipboard writes go through a native utility (pbcopy, wl-copy, xclip, xsel or clip) with an OSC 52 fallback for SSH and tmux, adding no new dependencies.

Render Obsidian and GitHub callouts (> [!note]) with a per-kind coloured bar and an icon + title header above the body, keeping the accent colour while editing.

Support the full Obsidian type set (note, abstract, info, todo, tip, success, question, warning, failure, danger, bug, example, quote) and their aliases, plus custom titles and fold markers ([!note]- Title), which pulldown-cmark does not recognise. Type names are case-insensitive and unknown types fall back to note, as in Obsidian.

Each type has a configurable symbol across the ascii, unicode and nerd font presets.

Open several notes at once in a bufferline-style tab bar and cycle through them. Selecting a note focuses its existing tab or opens a new one, so each note keeps its own cursor, scroll and unsaved edits.

Cycle with ctrl+n and ctrl+p, the bufferline-style L and H, or the unimpaired ]b and [b, and close the active tab with ctrl+w. All bindings are global so they work from any pane. Cycling moves the explorer selection to the active note, expanding any collapsed folder in its path so the note becomes visible.

To free the global L for tab cycling, the debug log overlay's level-cycle key moves from L to l.

The bar renders one uniform-width tab per note, shrinking them from a maximum toward a minimum as more open and only ever showing whole tabs. Names are truncated with an ellipsis and tabs that share a name are disambiguated by their parent directory. The active tab is a reverse-video highlight so it contrasts on any theme.

Bindings can be written against a <leader> prefix instead of a fixed key, so a whole set of them can be moved to another prefix by changing one line. The leader is set at the top level of the configuration file and defaults to <space>.

A binding string now parses into a KeySpec, a sequence of elements that are either a literal keystroke or the <leader> placeholder. Resolving a spec against the leader yields the concrete Key the runtime already dispatches on, so the existing chord machinery is untouched. Key keeps its own deserializer and rejects <leader>, which makes a self referential leader = "<leader>" a config error.

Resolution happens once the layers are known rather than at parse time. The leader is read from the user config alone and applied to the bundled config.toml and vim.toml as well, otherwise a <leader> binding shipped with basalt would answer to a different key than the user's own.

The leader may be a modified key or itself a sequence, and it may appear more than once in a binding.

Grow the experimental editor's Normal mode from a few cursor moves into a full vim editing grammar.

Motions run over the whole document through a pure motion engine (motion.rs) and a jump_to_offset primitive that crosses blocks: 0 ^ $, w W b B e E, { } %, gg G, and f F t T with ; , to repeat. A count prefixes any motion (3w, 5j, 10G).

Operators compose with motions via an operator-pending state machine: d c y over any motion, dd cc yy, x D C s, and p/P through an unnamed register. Text objects (ci", da(, ciw ...), r to replace a character, and u / ctrl+r undo built on content snapshots round it out; visual-mode d/c/y reuse the same path.

Semantic keys stay rebindable in vim.toml. Only the inputs no static binding can express -- count digits and the character after f/t/r or a text object -- take a raw-key path, mirroring insert mode.

Introduce a theme system that maps roles to colours, with per-pane border and section styling. Add Basalt's own theme Causeway and a bundled set (gruvbox, everforest, catppuccin and minimal), plus a theme picker that saves the choice to the user config.

The --theme flag overrides the configured theme.

Breaking#

Move off MIT to a split model that protects against proprietary forks while keeping the libraries reusable.

  • basalt-tui: MIT -> GPL-3.0-or-later (copyleft and patent grant)
  • basalt-core and basalt-widgets: MIT -> Apache-2.0 (permissive, reusable, with a patent grant)

Add a lightweight Contributor License Agreement so future GPL contributions can still be relicensed, including under a commercial license, without chasing past contributors for sign-off. Contributors keep their copyright and agree by opening a pull request.

Document the split in the README and CONTRIBUTING, and add a pull request template that acknowledges the CLA.

Already-published versions remain MIT for anyone who has them; the new terms apply from the next release onward.

The vault selector, debug log overlay, external editor and Obsidian commands were on ctrl+g, g<, ctrl+alt+e and ctrl+alt+o. They are now v, d, e and o, which frees the awkward chords and lets all four follow a user's chosen leader. Quit, help and the tab bindings are unchanged.

The old keys stop working, so muscle memory for ctrl+g in particular needs relearning. Rebinding them in the user config restores them.

Note that is now a live prefix in every non editing pane, so a bare space waits for the next keystroke. If that key completes no sequence it is retried on its own, and insert mode and the input modal bypass sequence handling entirely, so typing is unaffected.

The debug log overlay carried the old key in its own title, so the hint in the top right now reads Leader d.

The vault-selector and debug-log tapes send the new keys and both gifs are regenerated in each theme.

Fixed#

While editing, typing a word that filled the line to the viewport width panned the viewport right by one column, hiding the first column of every line. The pan stuck even after the word wrapped to the next visual line.

Wrapped text never exceeds the viewport width, so it needs no horizontal pan. Reset the pan to zero for lines that fit, and keep panning only for lines that truly overflow, such as code blocks and long unbroken words.

Rendered list markers (and other leading synthetic glyphs like quote and heading prefixes) carry no source range, so the selection highlight skipped them and a selected line's marker was left blank. Highlight a synthetic span when the content it precedes is selected, so the marker is part of the selection.

Selecting a heading in the outline jumped the cursor to it but scrolled the minimum amount needed to make it visible, so a heading below the viewport landed on the last line with none of its content shown.

It now scrolls the target line to the top of the viewport, clamped so the last line never rises above the bottom, keeping as much content below the heading visible as possible.

The debounced vault watcher fired a rescan roughly 250ms after picking a note, which rebuilt the tree and snapped the explorer cursor back to the active note. Moving the cursor right after selecting caused it to jump back once loading finished.

Split the rebuild into an intentional-select path (used on create and rename) and a new refresh_entries that captures the cursor and the picked note by path before rebuilding and restores each independently, so a background rescan snaps neither.

0.12.6 (Jun, 21 2026)#

Added#

Treat <...> as a grouping delimiter in binding strings so a named or modified key acts as a single keystroke inside a sequence, enabling Helix-style leader bindings like f and .

The group contents are parsed exactly like a standalone key, and a group must resolve to one keystroke (multi-key, unterminated, and empty groups error). < and > are reserved for the syntax; bind the literals via and .

Closes #516

A toggleable in-TUI debug log overlay (β€Ήg<β€Ί) shows tracing output across all levels (trace, debug, info, warn, error) on top of the application without interfering with normal use. A bounded ring buffer captures events through a tracing layer; the overlay supports scrolling, clearing and a minimum-level filter. It can also be opened on startup with --debug and filtered with --log-level. Relevant logging was added across the app.

The Explorer now watches the vault root recursively and refreshes when markdown files or directories change on disk, so external moves, additions and deletions are reflected without a user-initiated refresh. The watcher is rebuilt when the active vault changes and avoids stealing focus from the editor.

Changed#

Editing used to flip the whole block to raw on entry, and rendering it from the AST during edits drifted from the source, so nested lists and structural edits were unreliable.

Render the active block with a line decorator that maps 1:1 to source place. This keeps indentation, blank lines and code blocks faithful to the source and makes editing predictable.

Read mode and non-active blocks keep the AST renderer; unifying the two is tracked in #533.

Fixed#

In Edit mode the visual rendering now mirrors the source's blank-line structure. Empty lines between loose list items render as empty rows, and block spacing matches the source byte gap instead of a fixed "one empty line per block". Adjacent blocks with no source empty lines now render with no visual gap, so deleting at a block's start merges into the previous block without a misleading empty row in between.

0.12.5 (May, 16 2026)#

Added#

Allows opening an arbitrary directory as a vault by setting the env var, skipping the splash screen and going straight to the explorer.

The scrollbar is now visible in both splash screen and the vault selector modal. It's not optimal, but I realized that vault selection needs a rewrite, thus I didn't bother to do this properly for example the scrollbar is always shown if we have more than 4 items regardless if we have space to show all items and not needing a scrollbar.

Initial CLI layer with --version and --help flags. The version output is in a "standard" format similar to cargo version output 0.12.5 (abc123def 2026-05-15).

The values are populated by a build phase (compile time) using env vars.

Changed#

The rename input in explorer pane was fixed to left border of explorer pane, and forced vision to jump in deeply nested fields. This commit adds indentation to the floating rename input which is calculated by the depth of the explorer item. Additionally added depth field to items.

CreateUntitledNote and CreateUntitledFolder now resolve a target directory from the explorer's current item (the directory itself, a file's parent, or the vault root as fallback) instead of always creating at the vault root. After creation the explorer expands that directory so the new item is visible.

Add an explorer Open message that always expands a directory rather than toggling, backed by a new ExplorerState::open. The existing toggle behavior moves to a Select message, and the ExplorerOpen command and shell-return path now map to Select. toggle_item_in_tree takes an always_open flag to support both.

Bump basalt-core to 0.9.0 version in basalt`

Dependencies#

datasourcepackagefromto
crateindoc2.0.72.0.7
crateinsta1.46.31.43.2
cratetempfile3.26.03.23.0
datasourcepackagefromto
crateinsta1.43.21.47.2

Fixed#

Backspace at the start of a block's text buffer used to do nothing. It now merges the current block into the previous one, preserving in-progress edits. The merge is a no-op when previous and current block indices coincide, so backspace at the start of the first block does not collapse the only AST node.

Supporting changes:

  • Add previous_block_idx, rename current_block to current_block_idx. Both return owned usize.
  • Add TextBuffer::insert_at_start, which prepends bytes and moves both the live and original source range starts so a later commit overwrites the joined region.
  • Walk children recursively in shift_nodes via a new ast::Node::children_as_mut. Without this the cursor lands on a stale child range after editing a paragraph above a list.
  • commit_text_buffer re-parses on buffer.modified rather than on content change, since the merge mutates ast_nodes in place and a buffer that round-trips needs a re-parse.
  • In update_layout's deferred init, pick the editing block by cursor source offset, not by a stale virtual_row lookup.
  • Rename insertion_offset to source_pos.

A heading's source range can include a paragraph that follows it when the user types a newline mid-edit. The raw renderer treated the whole range as one heading line, so the new content was invisible until exit. Split on the first newline in raw mode and render the rest with render_raw.

Also drop the synthetic empty trailer in raw mode so paragraphs don't gain a stray blank row when entering edit mode.

ensure_cursor_visible was comparing cursor.virtual_row against viewport.top() and a viewport.bottom() shrunk by meta_len. The meta rows live above the content, so the cursor's screen row is virtual_row + meta_len. The old comparison scrolled the editor up too early when the meta header was visible, hiding the first content rows.

Add meta_len to the cursor's screen position before comparing against the viewport bounds, thread it through CursorWidget so it draws at the same offset, and add Viewport::scroll_up so vertical motion that consumes more rows than the viewport contains scrolls the document.

map_to_item handled VaultEntry::File via Item::from, which hardcodes depth: 0. Files still rendered correctly, but the rename input modal positions from Item::depth(), placing it flush-left for nested files.

Toggling a folder open or closed re-emitted SelectNote for the currently selected note, which rebuilt the note editor and reset its viewport scroll position.

Explorer state open() and select() now report (bool) whether a note was actually selected, and the explorer only emits SelectNote in that case, which then effectively retains the scroll position of the viewport.

Pass the configured symbol preset into StatusBar so the active component badge falls back to ASCII-safe glyphs when the Ascii preset is selected.

0.12.4 (Apr, 09 2026)#

Added#

Introduces symbols and symbol presets as the foundation for configurable UI symbols. Each preset provides a complete set of defaults for all visual glyphs used across the interface. Users pick a preset in [symbols] and optionally override individual fields on top of it.

Also added derives PartialEq and serde::Deserialize on FontStyle so heading and title font styles can be configured from toml.

Related to #424

Previously config parsing errors were silently swallowed. Now we return warnings alongside the config so they can be shown to the user. Invalid config files produce an InvalidConfig error with the TOML parser's message. The UserConfigNotFound case is still silently ignored since most users won't have a config file.

Additionally toasts now support word-wrapped multi-line messages via textwrap and compute their height dynamically instead of using a fixed constant. This lets longer error messages display fully rather than getting truncated. Toast stacking in render_toasts accounts for variable heights.

Signed-off-by: Erik Kinnunen [email protected]

Extend Preset with an Auto variant that detects terminal capabilities at startup by inspecting TERM, LC_ALL, LC_CTYPE and LANG environment variables to choose between Unicode and Ascii presets.

Signed-off-by: Erik Kinnunen [email protected]

Changed#

Setting rust-version lets Cargo emit a clear error when someone tries to build with a toolchain older than 1.91.0 and enables version-aware dependency resolver behavior.

Signed-off-by: Erik Kinnunen [email protected]

Related to #424

Signed-off-by: Erik Kinnunen [email protected]

All render functions now take &Symbols parameter so application symbols are read from config instead of being hardcoded.

List markers cycle through the configured list based on nesting depth. VirtualDocument stores a Symbols instance and uses it for the title font style and horizontal rule.

Additionally add support for cycling through list markers so different depth levels of list indentation have different markers. Can define n-amount of symbols.

Related to #424

Signed-off-by: Erik Kinnunen [email protected]

Replaces hardcoded symbols in the explorer with configurable ones. Unselected files also render symbols.unselected instead of blank space so the collapsed view is consistent across presets.

Added more emphasis on selected note with bold and underline styles.

Related to #424

Signed-off-by: Erik Kinnunen [email protected]

Replaces hardcoded glyphs with configurable symbols.

Related to #424

Signed-off-by: Erik Kinnunen [email protected]

Config is now loaded once in App::start and passed into App::new so that config.symbols is available when constructing component state. The default config sets preset = "unicode" in config.toml.

Related to #424

Signed-off-by: Erik Kinnunen [email protected]

Previously border style was hardcoded in code, however, this is now also controllable from the symbols map.

Signed-off-by: Erik Kinnunen [email protected]

Toast icons were hardcoded. This commit adds toast_success, toast_info, toast_error and toast_warning fields to the symbol config so each preset can define appropriate icons. ASCII uses simple text characters, Unicode keeps the existing icons, and NerdFont uses its own glyph variants. The icon is resolved at render time from the active symbols config.

Signed-off-by: Erik Kinnunen [email protected]

0.12.3 (Mar, 10 2026)#

Added#

This change adds support for a sequence (or chord) of keys like 'gg' or 'bn' or 'gth'.

The keys maps are separated into Single keys (which can contain modifiers) or a 'sequence' of keys called Chord.

Additionally added support for uppercased chars in key bindings, which means that users can now use shift + char to run commands.

Organized and structured the key code parsing, so it's more readable and the 'special' cases are clearly represented.

This commit adds key sequence handling to basalt, which means that users can create key bindings with a key sequence like gg or ciw.

  • Replace single-key lookup with pending_keys: Vec<Keystroke> on AppState, handle_event and handle_key_event take &mut AppState to drive accumulation and prefix/exact matching
  • Add ConfigSection::sequence_to_message and is_sequence_prefix
  • Remove old key_to_message / handle_active_component_event, editing bypass is now inlined in handle_key_event
  • handle_editing_event in input and note_editor now take KeyEvent by value for consistency

Related to #400 Related to #212

Adds ScrollToTop/ScrollToBottom to the note editor and explorer message enums, wired through new command variants: note_editor_scroll_to_top, note_editor_scroll_to_bottom, explorer_scroll_to_top, explorer_scroll_to_bottom.

Note editor ScrollToBottom uses cursor_jump to the last block rather than cursor_down(usize::MAX), which silently does nothing because saturating_add clamps to usize::MAX and skip(lines.len()) exhausts the iterator.

When vim_mode = true in the user config, a vim.toml preset is merged between the base config and the user config, so users can still override individual bindings. The preset adds:

  • ctrl+f / ctrl+b for half-page scrolling in note editor, explorer, and help modal (replacing ctrl+d / ctrl+u)
  • gg / G for jump to top/bottom in note editor, explorer, and outline

Add Normal/Insert sub-modes within EDIT mode. i enters Insert mode, Esc returns to Normal for hjkl navigation, Esc again exits to READ.

Fix block navigation in edit mode: track editing_block explicitly to prevent layout oscillation, commit text_buffer before switching blocks, fix modified() after block switch, and use AST source ranges for correct cursor positioning when entering code blocks.

Bind n to create a new untitled note and N to create a new untitled folder in the explorer. Both commands select the newly created item in the explorer after creation.

Changed#

Instead of merging vim.toml bindings on top of the defaults, vim mode now replaces the entire key_bindings set for each section it defines. This ensures vim-style bindings like gg and G are the only way to scroll to top/bottom, without leftover default bindings like ctrl+shift+up/down.

Some terminals send 'g'+SHIFT instead of 'G'+SHIFT. Normalize this in Keystroke::from so key bindings match regardless of terminal behavior. Also fix From<&KeyEvent> to go through the normalization path.

Fixed#

Fix inverted vim_mode condition that loaded vim config when disabled...

Add w/b word motion bindings for input modal in vim.toml and rename VIM_CONFIG_STR to VIM_CONFIGURATION_STR for consistency

When moving up/down, if the target line has no content (e.g. a synthetic line in a visual code block), search in the opposite direction as a fallback. This fixes ScrollToTop not working when the first element is a code block, and ScrollToBottom not reaching the last line.

When entering edit mode before layout, enter_insert couldn't find blocks and created a spurious empty paragraph node, hiding the real first line. Guard the empty-node fallback to only fire for genuinely empty files.

Also commit the text buffer when exiting vim insert mode so that save writes the updated content.

When you had items with number suffixes like 1, 2, 3, 100, the standard rust comparison and ordering would not work as expected, as users are usually expecting natural ordering. What happened was that we received: "item 1, item 10, item 2" instead of "item 1, item 2, item 10".

Added natord crate for natural sort ordering, and now explorer sorts the items in expected order "item 1, item 2, and item 10".

In some conditions after rename the state would be left in "limbo" and "frozen" state, which meant that users could not navigate normally anymore. This happened because we didn't correctly change to Explorer pane after exiting from rename input.

When using vim mode with NORMAL and INSERT modes, when creating writing new nodes for example a heading and then exiting would result in visual bug that appeared as if the current node merged with the next one, e.g. a paragraph after our newly created heading looked liked it was merged into it.

Fixed the visual bug by accessing the ast_nodes directly, since virtual_document in this case would have "stale" data.

0.12.2 (Feb, 21 2026)#

Added#

Introduces toast module with level-based notifications (info, warn, error, success) that auto-expire via a 250ms tick loop. Toasts render in the top-right corner with colored borders and icons per level.

Includes snapshot tests for all toast variants and unit tests for expiry behavior.

Related to #83

Enables multiple app messages to be dispatched and fully processed in sequence, which is needed to combine actions like saving a note and showing a toast notification in a single update cycle.

Related to #83

Show a success toast when a modified file is saved and an error toast when saving fails, replacing the previous silent no-op error handling.

Fixes #83

Fixed#

Call ensure_cursor_visible after cursor movement and text editing operations (insert_char, delete_char, cursor_left, cursor_right, cursor_word_forward, cursor_word_backward) so the viewport follows the cursor. These calls were already present in cursor_up and cursor_down but missing from the other methods.

Fixes #316

Account for the list scroll offset when calculating the input modal y-position in ToggleInputRename. Previously, the position used the absolute selected index, which placed the modal outside the visible area when the list was scrolled.

Smart punctuation transforms characters like ' and " to curly variants like β€˜ and β€œ. The latter variants have different byte lengths. This had an effect that made the source offset not match with the rendered offset causing issues like inability to move the cursor downwards if the current paragraph contained characters that were transformed into 'smart' variants due to overlapping offsets it the length difference caused.

The fix was to disable the smart punctuation, and come back to it at a later date and do the change holistically. This needs most likely some architectural change to allow smart punctuation to work. The smart punctuation should be treated as virtual variant that only has an effect in the rendering part of the text.

This fixes: #371

When opening an empty file and entering edit mode, no text or cursor was visible until pressing ESC. This was caused by three issues:

  • No AST nodes existed for empty files, so layout produced no virtual lines and typed text was invisible
  • Cursor rendering was gated on non-empty content, which is only updated on exit from edit mode
  • render_raw produced no content lines for empty content, leaving the cursor with no valid position

Fix by creating a placeholder paragraph node when entering insert mode on an empty document, allowing cursor rendering in edit mode, and producing a content line in render_raw for the empty content case.

0.12.1 (Jan, 26 2026)#

Changed#

The previous πŒ† tetragram for centre symbol had multiple issues between different terminal emulators and recently the update of unicode-width that changed classification on some symbols making the width differ, from the previous version.

Without this change I cannot update to newer version of unicode-width.

Obsolete feature as the 0. major version should tell enough about the instability of this application.

Fixed#

The cursor now correctly handles multi-byte characters (emojis, unicode symbols) when moving left/right and when calculating visual positions. Previously, the editor assumed 1 byte per character, causing the cursor to land in the middle of multi-byte sequences.

Key changes:

  • Use byte lengths instead of character counts for source range tracking
  • Convert between byte offsets and character boundaries properly
  • Update insert_char and delete_char to account for variable byte widths
  • Fix source_offset_to_virtual_column to use byte indices instead of char indices

Fixes #314

The cursor could not move upwards past empty lines in code blocks in both edit and read modes.

In edit mode, virtual_position_to_source_offset incorrectly returned source_range.end for empty lines because cur_col included synthetic span widths. Added content_col to track only content character widths.

In read mode, the Visual rendering of code blocks didn't account for newlines when calculating source ranges, causing empty lines to have empty ranges (e.g., 5..5). Now uses line_range() which properly adds 1 for newlines.

Fixes #321

Now all wiki-links will be updated after renaming a note in basalt. We call update_wiki_links in the RefreshVault message handler to automatically update links across the vault when a note is renamed.

Also refreshed the note editor content after rename to reflect any wiki-link changes in the currently open note, otherwise the content would not be refreshed properly.

Fixes #307

0.12.0 (Jan, 18 2026)#

Added#

[!CAUTION] BEWARE! This rename implementation does not cover updating the wiki-links. If you use the rename on notes that are referenced as wiki-linksβ€”these links will be broken after and needs to be manually corrected.

Add an input modal that provides dynamic modal text editing. The modal features a text input widget and cursor navigation, which supports character-by-character and word-based movement. The vim-like modes are limited. Only aforementioned movement and text editing.

The modal is integrated with the explorer pane, and is available by pressing 'r' (default key binding) on the selected item. The rename operation leverages the rename_note and rename_dir functions added in affec53 and a347325, the vault is 'reloaded' after rename.

Dependencies#

datasourcepackagefromto
crateratatui0.29.00.30.0

Fixed#

Fixes #69. Previously, when user sorted the vault items the folders would also be sorted according to the same rules as notes, however, this is not how obsidian sorts. Obsidian sorts only files by default, not directories. Directories are initially sorted A-z, and then kept in that order when sorting files.

0.11.2 (Dec, 21 2025)#

Changed#

Use direct definitions in the respective crates instead of using workspace dependencies for basalt-core and basalt-widgets.

Fixed#

The parser now correctly nests subtasks within their parent task nodes rather than treating them as siblings. The task_kind field changed from Option to Vec to track nested task states, similar to item_kind.

Nested task lists are now properly rendered following the same implementation as in the list items code.

The sticky key effect was visible for example with task lists when tasks were intended with tabs in the source. These tab characters would never replace the existing symbols from the buffer. The sticky symbols issue was fixed by replacing the tab characters with two spaces.

0.11.1 (Dec, 08 2025)#

Fixed#

basalt-core 0.6.3 fixes vault json deserializer for ts field and sets it as optional. It was previously set as required. If the ts field was missing it would crash basalt.

0.11.0 (Nov, 30 2025)#

Basalt author and maintainer here! Wanted to write a few words before the regular changelog.

Phew, this took longer than expected, but here we are. The editor feature does not offer feature parity with the tui-textarea that was being used previously, however, it can properly wrap the text while writing, which frankly, I find quite pleasing.

If you encounter any bugs or additional strangeness please open an issue! The editor was made by me and most likely contains errors. Use with caution! :)

I'm taking a small break from basalt for the advent of code puzzles! So expect slower development during December.

This release fixes the following issues: #105, #104, #95

Demo:

basalt demo of new 0.11.0 version editor capabilities

Added#

I decided to implement a virtual document, which is essentially the virtually rendered version of the markdown document. This virtual document is a collection of virtual blocks, virtual lines and virtual spans.

Virtual lines and spans are turned into Ratatui variants to render them in terminal.

Virtual spans are separated into two concepts, synthetic and content.

  • Synthetic spans are elements that are not calculated as part of the markdown source.
  • Content spans on the other hand are elements that are calculated as part of the markdown source.

This separation enables more fluid use cases and easier management codewise for more rendered content, like text wrapping symbols, additional emphasize lines or spans, etc.

Rendering is a collection of functions that are turned into virtual blocks. These virtual blocks map directly into top level markdown nodes.

All rendered functions wrap the text with the given max width. The text wrapping is a generalized wrapping function that can be now run for "any" markdown node that is defined in the ast module.

Cursor module is responsible for keeping up with the cursor state. Cursor can be switched between two modes, read and edit.

Each mode behaves a bit differently, read only considers the virtual elements, and edit mode considers the source content.

For now only the read mode variant is properly implemented.

The rendering is handled with a separate stateful CursorWidget component that takes the cursor state as an input and draws the cursor accordingly.

In read mode the cursor is drawn as a full-width line cursor.

Also add empty_line() helper function to TextSegment struct. This creates a new empty line with "\n" as the content.

This empty line can then be split in the render functions, but still keep the content inside a single markdown node (e.g. paragraph).

The text wrapper module exposes wrap_preserve_trailing, which, as name implies, keeps the trailing whitespace.

I'm using the textwrap::WordSeparator to find and iterate over the words in the text, and then determine if the word fits in the current line by using the passed max width variable. The textwrap crate itself did not ship with a premade wrapping utility that would have preserved the whitespace, at least, I did not find such utility.

The viewport abstraction wraps the ratatui layout structure Rect and uses additional layout data structures like Size and Offset.

These helper methods will allow easier access to the underlying chars and their byte indices.

Changed#

The old note editor variant was hard to maintain and was lacking proper structure. Adding new ast nodes or elements was a cumbersome process.

The new variant uses logical structures like virtual document and separate rendering functions to achieve a more cohesive end result.

The editor now requires to have a viewport in order to render anything properly. This is a requirement for example to decide the correct wrapping width for text elements.

This change also introduces fix for scrolling. Now scrolling works properly and cursor is always visible in the viewport. This fixes the issue #104.

The rendering is simplified drastically due to the use of more logical structures.

In this commit, only read mode is enabled and the edit mode support is missing.

No reason to not return a reference, and we avoid allocation.

Previously only content width was taken into account, however, this does not work as intended as the synthetic width needs to be calculated as well to find for example the correct offset for cursor column.

Also simplified and improved the existing methods. For example virtual spans now retuns a slice instead of owned Vec.

This custom implementation replaces tui-textarea with proper text wrapping and better WYSIWYG experience. Also the custom implementation allows for more granular control over how elements are, rendered and positioned.

I tried to mimic the previous functionality in a way so as little is lost as possible feature wise, obviously, since this is made from scratch the feature parity is far off still.

Additionally there is some known issues, like cursor positioning is incorrect with unicode symbols in source content. This can be observed by wrong end position when moving the cursor to the right most end. The cursor appears as if it is stuck, but that is due to wrong count somewhere, which should be fixed, but in a different commit.

For now the implementation is very limited and implements only a restricted set of features: Editing markdown nodes, saving changes to file, moving by words and scrolling by half pages.

This commit has quite many changes, and, unfortunately the nature of how this refactor was introduced, was difficult to separate the commits atomically and cleanly to smaller pieces.

But the most notable ones are:

  • Cursor changes, which include the cursor movement by columns and words and proper cursor positioning from the source offset location.

  • Editor state changes to allow insertion, deletion and saving of files, and source range shifting, which is related to the editor functionality, which essentially shifts the end of the source range, if the source ranges are not shifted properly and if the text buffer exceeds the range start of next node the text buffer would be replicated and rendered in-place of the next node.

  • Render changes, which introduce a new consolidated text wrapping and handling for newline characters in both visual and raw rendering modes. The new consolidated text wrapping uses the new whitespace preserving text wrap function. Additionally source offset for rendered virtual lines were fixed. Also added unicode-width dependency for accurate unicode character width calculations in render.

Dependencies#

datasourcepackagefromto
crateetcetera0.10.00.11.0

New Contributors#

Full Changelog: https://github.com/erikjuhani/basalt/compare/basalt/v0.10.4...basalt/0.11.0

0.11.0 (Unreleased)#

This release adds new improved note editor with proper text wrapping for all markdown elements (excluding code blocks).

The new improved and refactored editor code should enable faster feature creation.

Added#

Changed#

0.10.4 (Oct, 6 2025)#

This release adds note name support in the editor pane. Follows a similar approach as in the Obsidian app itself. The note name can not be changed yet in basalt.

Additionally contains some fixes and slight changes to headings to make them work better with the new note name implementation.

Added#

Fixed#

Changed#

0.10.3 (Sep, 15 2025)#

This release adds the support to easily hide and expand the explorer pane (file tree). Expanding and hiding is done with h, l and arrow left, and arrow right.

When explorer is expanded a ⟹ symbol is shown for clarity of the current state.

Added#

Fixed#

0.10.2 (Sep, 13 2025)#

Deprecated the following config commands:

Use these instead:

Changed#

0.10.1 (2025-08-31)#

Added#

Changed#

0.10.0 (2025-08-21)#

Added#

Changed#

0.9.0 (2025-07-30)#

Added#

0.8.0 (2025-06-25)#

Added#

Fixed#

0.7.0 (2025-06-15)#

Changed#

Added#

0.6.1 (2025-06-07)#

Fixed#

0.6.0 (2025-06-01)#

Added#

Fixed#

0.5.0 (2025-05-25)#

Fixed#

Changed#

0.4.1 (2025-05-25)#

Changed#

0.4.0 (2025-05-25)#

Fixed#

Check basalt-core CHANGELOG here.

0.3.7 (2025-05-22)#

Added#

Fixed#

0.3.6 (2025-05-21)#

Fixed#

0.3.5#

Fixed#

0.3.4#

Added#

Changed#

0.3.1#

Fixed#

0.3.0#

Added#