github dheerajshenoy/lektra v0.7.1

latest release: v0.7.2
7 days ago

Features

  • Auto scroll on text selection mode to keep the selection in view when selecting text
    with the mouse or keyboard, which provides a smoother and more intuitive text selection experience,
    especially for longer documents where the selected text may go out of view.

  • Add optional lua scripting support (experimental, work in progress)

    • API overview

      • lektra.opt - for getting and setting config options
      • lektra.cmd - for command related stuff
      • lektra.ui - for UI related stuff (e.g. showing notifications, input dialogs, etc.)
      • lektra.tabs - for managing tabs
      • lektra.event - for subscribing to events (e.g. page change, file open, etc.)
      • lektra.keymap - for managing keybindings
      • lektra.mousemap - for managing mousebindings
      • lektra.utils - for utility functions
      • lektra.version - for version functions
      • lektra.capabilities - for querying about compiled options
      • lektra.bookmarks - for managing bookmarks
    • Check LUA-WIKI.md for more details and examples of the lua scripting support in LEKTRA.

  • Vim/Emacs like search hit indexing navigation if absolute_jump = false in [search]

  • Add bookmarks support with a bookmark picker to view and manage bookmarks. Bookmarks allow users to save specific locations
    in the document for quick access later.

  • New bookmark related commands:

    • bookmark_add to add a bookmark at the current location
    • bookmark_remove to remove a bookmark at the current location
    • bookmarks to open the bookmark picker with the list of bookmarks in the document
  • Add image rotation support

  • Add missing implementation for single_instance option. Now if single_instance is enabled, new files will use the
    existing instance of LEKTRA instead of opening a new instance, which allows for better management of
    multiple documents and prevents cluttering the taskbar with multiple instances of LEKTRA.

Bug Fixes

  • Update the layout menu item names
  • Fix crash (SEGV) on click selection in SINGLE layout mode — pageAtScenePos was
    guarding the outPageItem assignment with if (outPageItem), but the pointer is always
    nullptr at that point, so pageItem was never set and mapFromScene dereferenced null.
  • Fix wrong text selection after zoom in SINGLE layout mode — setZoomAnchored called
    repositionPages() (which scales the old item as a visual intermediate) but never
    triggered a re-render, leaving the page item permanently at a non-unity scale. Because
    mapFromScene divides by the item scale, selection coordinates were passed to
    computeTextSelectionQuad in the wrong zoom space. Fixed by calling renderPage() for
    SINGLE mode after repositionPages() in setZoomAnchored.
  • Fix fit mode not working if image files are opened
  • Fix memory leak in extractText function in Model class
  • Add UTF-8 text conversion for file paths on Windows to fix issues with opening files with non-ASCII characters in their paths on Windows.
  • Reset m_success = false at the start of every open in Model.cpp
  • [DocumentView.cpp] Made the future watcher connection single-shot and cleared stale connections before reconnecting and
    added a guard in handleOpenFileFinished() so it exits early if the model did not actually open the file.
  • Add missing page config section loading from the config
  • Fix visual line mode navigation to be more naturally
  • Fix context menu on tabs not working
  • Fix opening files in containers with already open file not loading.
  • Fix openSessionFromArray function loading files incorrectly and breaking
  • Fix segfault in buildPageCache because of double free of mupdf context
  • Add implementation for file_reload command
  • Fix synctex initialisation
  • Fix synctex optional macro in the source code HAS_SYNCTEX -> WITH_SYNCTEX
  • Fix image zoom anchoring
  • lua/Lektra.cpp: Fix stack leak in executeLuaCode — the message-handler function was
    pushed before luaL_loadstring but never popped, growing the Lua stack by one slot on
    every call. Replaced with lua_settop save/restore and removed the broken handler
    (which returned 0 instead of the required 1 value). Also fixed toStdString().c_str()
    to a stable QByteArray local.
  • lua/view.cpp: Fix open method reading lua_upvalueindex(1) with zero upvalues —
    caused undefined behaviour/crash. Now resolves the Lektra instance via
    qobject_cast<Lektra*>((*view)->window()).
  • lua/view.cpp: Fix fit, mode, and layout getter methods returning 0 (no values)
    after pushing a value onto the stack — callers received garbage. Changed to return 1.
  • lua/view.cpp: Fix goto_location applying a page-index offset twice: pageno was
    already converted to 0-based, then subtracted again before passing to GotoLocation,
    sending page 1 to index −1.
  • lua/view.cpp: Fix zoom containing unreachable return 0 after return 1 in both
    branches. Collapsed to a single push + return 1.
  • lua/view.cpp: Fix set_invert returning 1 in the success branch without pushing
    anything — now returns 0 (setter, no return value).
  • lua/view.cpp: Replace empty stub bodies in set_mode and save_as with
    luaL_error so callers get a clear error instead of silent no-ops.
  • lua/event.cpp: Fix unregister API mismatch — it expected (string, int) but
    register returns only an int handle. Unified to (EventType, handle) matching the
    register signature.
  • lua/event.cpp: Fix once taking a string event name while register takes an integer
    EventType. Both now use integer EventType for consistency.
  • lua/event.cpp: Fix COUNT sentinel exposed in the lektra.event.EventType Lua table
    due to an off-by-one <= in the population loop. Changed to <.
  • lua/event.cpp: Fix once callbacks never being removed — is_once flag was set but
    dispatchLuaEvent never checked it, so the callback would fire on every subsequent
    dispatch (pushing nil after the first unref). Moved cleanup into dispatchLuaEvent
    using an erase-remove pass after invocation.
  • lua/event.cpp: Fix iterator invalidation in dispatchLuaEvent — callbacks could call
    unregister during iteration. Now iterates over a local copy of the callback list.
  • lua/cmd.cpp: Fix Lua registry reference leak on command unregister — func_ref was
    captured in the action lambda but luaL_unref was never called when the command was
    removed. Wrapped in a shared_ptr guard whose destructor calls luaL_unref.
  • include/DispatchType.hpp: Fix duplicate OnPageChanged key in the dispatch map —
    the second entry silently overwrote the first in QHash.
  • include/DispatchType.hpp: Rename reserved identifier __dispatchEventMap (double
    leading underscore is reserved by the C++ standard) to s_dispatchEventMap.
  • include/DispatchType.hpp: Replace O(n) linear scan in dispatchTypeToString with an
    O(1) static array indexed by enum value.

Performance

  • lua/view.cpp: Implement view:outline() — returns the document table of contents as a
    recursive Lua table tree. Each entry has title, pageno (1-based, nil for external
    links), x, y, and a children array for nested headings.

  • Model.cpp / DocumentView.cpp: Overhaul animated image (GIF) playback performance:

    • Two-pass open: pingImages reads frame count and per-frame delays from headers only
      (no pixel I/O). A single [0] scene read then decodes frame 0 and emits
      openFileFinished immediately, so the image appears without waiting for the full decode.
      Remaining frames are decoded in a background thread via readImages + coalesceImages.
    • Remove dead getAnimatedFrame: the function re-read and re-coalesced the entire file
      from disk on every call just to produce one frame. It was never called; removed entirely.
    • Eliminate m_image_cache indirection for animated frames: setCurrentAnimFrame now
      only updates m_current_frame; requestImageRender reads directly from
      m_animated_frames[m_current_frame], removing a per-advance QImage copy.
    • Remove unconditional qDebug in setCurrentAnimFrame: the debug log fired on every
      frame advance (30+ times per second in all build configurations).
    • Fix cleanup_image not resetting animated state: m_animated_frames,
      m_frame_delays_ms, m_frame_count, and m_current_frame were left populated when
      switching away from an animated image.
    • Frame-skip during startup: the playback timer skips frames that have not been decoded
      yet (background decode still in progress) instead of displaying a blank frame.
    • Elapsed-time frame scheduling: the animation timer now subtracts actual render time
      from the next frame's nominal delay via QElapsedTimer, keeping the playback cadence
      on schedule even when individual frames render slowly.
    • Free raw Magick frames early: in the background decode path, the raw readImages
      vector is cleared immediately after coalesceImages so decoded-but-uncompressed Magick
      memory is released before the QImage conversion loop begins.
  • DocumentView.cpp: Replace QGraphicsItem::data(0).toString() tag checks with QSet<int>
    membership tests (m_placeholder_pages, m_preload_pages) eliminating repeated
    QVariantQString conversions in hot loops inside removeUnusedPageItems,
    repositionPages, and renderPages.

  • DocumentView.cpp: Split the single render queue into a visible-page queue and a preload
    queue so startNextRenderJob dequeues in O(1) instead of doing an O(n) linear scan with
    an O(n) removeAt to find the next visible page to render.

  • DocumentView.cpp: Eliminate QHash::keys() heap copies in removeUnusedPageItems,
    clearVisibleLinks, and clearVisibleAnnotations; replaced with direct iteration or a
    two-pass collect-then-delete approach.

  • DocumentView.cpp: Cache the last rendered search-hit state (index + page-item pointer)
    in updateCurrentHitHighlight so the path is only recomputed when the hit or page item
    actually changes, avoiding redundant QPainterPath rebuilds on every scroll event.

  • Model.cpp: Skip redundant line_length() traversal in find_closest_in_page for lines
    with no characters — the call always returned 0 so the idx increment was a no-op.

  • Model.cpp: Hoist m_inv_dpr scale constant out of the per-link and per-annotation render
    loops in renderPageWithExtrasAsync; add reserve on result.links and
    result.annotations before those loops to avoid reallocation churn on pages with many
    links or annotations.

  • Lektra.cpp: Hoist findChildren<QShortcut *>() out of the per-key loop in
    setupKeybinding — was performing a full child-object tree traversal once per key;
    now called once before the loop with deleted entries removed in-place to avoid dangling
    pointers.

  • Lektra.cpp: Replace the temporary QStringList() << "*.json" filter construction in
    getSessionFiles with a static const QStringList, eliminating a heap allocation on
    every call.

Breaking Changes

  • Remove placeholder_text option from [command_palette] section of the config.
  • Add prompt option in [picker] common to all pickers, which allows for a
    customizable prompt text shown next to the input field.
  • Rename always_open_in_new_window to single_instance.
  • C++ version requirement has been downgraded to C++20 (previously it was C++23) to allow
    for wider compatibility with different platforms and compilers, as C++20 is now widely
    supported by most modern compilers and platforms, while C++23 is still very new and not yet
    supported on many platforms.

Don't miss a new lektra release

NewReleases is sending notifications on new releases.