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

Changelog

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 erik.kinn@gmail.com

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 erik.kinn@gmail.com

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 erik.kinn@gmail.com

Related to #424

Signed-off-by: Erik Kinnunen erik.kinn@gmail.com

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 erik.kinn@gmail.com

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 erik.kinn@gmail.com

Replaces hardcoded glyphs with configurable symbols.

Related to #424

Signed-off-by: Erik Kinnunen erik.kinn@gmail.com

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 erik.kinn@gmail.com

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

Signed-off-by: Erik Kinnunen erik.kinn@gmail.com

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 erik.kinn@gmail.com

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