github dheerajshenoy/lektra v0.7.4

9 hours ago

New Features

  • Add narrow to region (Emacs-style). Invoke narrow_to_region (or View → Narrow to
    Region
    ) to enter rubber-band selection; the chosen rectangle becomes the entire viewport —
    scrolling is constrained to it, everything outside is painted over with the background
    colour, and all interactions (text selection, zoom, search, links) work normally within the
    region. wide_region (or View → Widen) restores the full document view. The narrow state
    survives zoom changes: the region is stored in normalised page-local coordinates and
    recomputed from the page item's current transform after each re-render. The narrow region is
    also accessible from the region-selection context menu ("Narrow to Region").
  • Expose view:narrow_to_region(), view:wide_region(), and view:is_narrowed() -> boolean
    in the Lua view API.
  • Add horizontal and vertical page flip (flip_horizontal / flip_vertical commands, default
    bindings | / _). Flip state is stored in Model alongside rotation and propagated
    through every coordinate-space transform (buildPageToDevMatrix / buildRenderTransform
    helpers) so that text selection, link hit-testing, annotation picking, and all other
    page-space operations remain correct when a document is flipped. All render backends are
    supported: MuPDF PDF/XPS/CBZ, static images, animated GIFs, and DjVu. Commands are
    exposed as view:flip_horizontal() and view:flip_vertical() in the Lua view API.
  • Add view:region_select(callback) Lua API. Switches the view into rubber-band
    selection mode; when the user draws a rectangle the callback receives
    { x, y, w, h } in scene coordinates and the view returns to normal. The default
    context menu is bypassed so scripts can use the region for arbitrary purposes.
  • Add "Copy Region as Image (Custom DPI)..." to the region-selection context menu.
    For PDF and other vector sources the selected region is re-rendered from the cached
    MuPDF display list at the requested DPI (72–1200, default 300), so the clipboard
    image is sharp regardless of the current view zoom. Only the selected sub-region is
    rasterized — not the whole page. Rotation and flip state are preserved in the new
    render transform. For raster sources (images, DjVu) the existing crop is upscaled
    using smooth transformation to approximate the requested resolution.
  • Add lektra.timer Lua module backed by QTimer. Timers are created with
    lektra.timer.new(interval_ms, callback [, single_shot]), support start, stop,
    set_interval, set_single_shot, is_active, is_single_shot, interval, and
    destroy. Timers are parented to the main window so they are cleaned up automatically
    on shutdown; the __gc metamethod ensures the QTimer and Lua callback reference are
    released as soon as the userdata is garbage-collected, making explicit destroy() calls
    optional rather than required.
  • Add view:rotate_clock(), view:rotate_anticlock(), view:flip_horizontal(), and
    view:flip_vertical() to the Lua view API (lektra.view methods on View userdata).
    Rotation methods were previously missing from the view API entirely.
  • Implement SyncTeX forward search (editor → lektra). --synctex-forward now calls
    synctex_display_query and jumps to the matching PDF location via GotoLocation,
    including deferred-render support. Previously the flag parsed the arguments but never
    performed the jump (was a TODO).
  • Add --socket <path> CLI flag. Starts the IPC server on the given socket path,
    allowing multiple lektra instances to be addressed independently (analogous to
    nvim --listen). The first invocation with a given socket listens; subsequent
    invocations with the same socket forward their message and exit.
  • Add --single-instance CLI flag. Forces single-instance mode for one invocation
    without requiring behavior.single_instance = true in the config file.
  • --synctex-forward now implicitly acts as single-instance: it always attempts to
    connect to a running instance via IPC regardless of the single_instance config
    setting, preventing duplicate windows when triggered from an editor.
  • SyncTeX forward search via IPC now reuses an already-open tab for the target PDF
    instead of opening a new one each time. The matching tab is brought to focus and
    the view jumps to the synctex position in-place.
  • Add split maximize (split_maximize command). Hides all split panes except the
    currently focused one, giving it the full tab area. Invoking the command again restores
    all panes at equal sizes. Focus navigation while maximized (split_focus_*) moves
    the maximized slot to the newly focused pane rather than switching the active pane
    within a hidden layout. Creating a new split or closing the maximized view
    automatically restores the layout first. A small corner badge (⊠) is drawn on the
    maximized view; controlled by split.maximize_indicator (bool, default true) and
    split.maximize_indicator_color (ARGB, default 0xCC2979FF). Both options are exposed
    in the Lua opt API as lektra.opt.split.maximize_indicator and
    lektra.opt.split.maximize_indicator_color.
  • Add focus border for the active split pane. Three new split config options:
    split.focus_border (bool, default false) enables the feature;
    split.focus_border_color (ARGB integer, default 0xFF4FC3F7) sets the border colour;
    split.focus_border_width (integer, default 2) sets the thickness in pixels. All three
    are exposed in the Lua opt API as lektra.opt.split.focus_border,
    lektra.opt.split.focus_border_color, and lektra.opt.split.focus_border_width.
  • Add portal.split config option ("vertical", "horizontal", or "smart").
    Previously portals always opened in a vertical split. "smart" automatically picks
    vertical when the view is wider than tall and horizontal otherwise.
  • Add FilePicker — an Emacs-style find-file picker (file_picker command, default
    binding Ctrl+Shift+o). The prompt label shows the current directory (abbreviated
    with ~); the search box filters entries in that directory. Typing a path with a /
    separator auto-navigates to the directory part. Tab completes the best match:
    directories are entered immediately, files are completed in the input. Backspace/Delete
    on an empty input navigates up one directory.

Bug Fixes

  • Fix highlight annotation hover effect triggering on parts of a line before the
    annotation starts. HighlightAnnotation now overrides shape() to return the
    union of its individual segment rects instead of the full bounding rect, so Qt's
    hover hit-testing only fires when the cursor is actually over a highlighted segment.
  • Fix background colour being overridden by the system palette colour on scroll
    or zoom. initGui was setting the background brush only on m_gscene
    (QGraphicsScene), whose drawBackground() only fills the scene rect.
    Viewport areas that fall outside the scene rect (visible when zoomed out or
    near document edges) were painted with the system palette window colour
    instead of the configured background. The brush is now set on both m_gview
    (QGraphicsView) — which fills the entire viewport — and m_gscene so that
    the narrow-clip strip painting in GraphicsView::paintEvent continues to read
    the correct colour from scene()->backgroundBrush().
  • Fix thumbnail panel defaulting to single-page layout instead of vertical. handleOpenFileFinished unconditionally called
    setLayoutMode(m_config.layout.mode) on every file open, overwriting the vertical layout set during thumbnail view construction.
    The call is now skipped when in thumbnail mode.
  • Fix text-selection quads persisting on screen after navigating to a different page via the thumbnail panel.
    GotoPage now calls ClearTextSelection() before rendering in single-page layout mode, where the entire page is replaced.
  • Fix thumbnail page highlight disappearing after the page item is re-rendered (e.g. after a zoom change).
    renderPageFromImage now saves the isHighlighted() state of the old item before deleting it and restores it on the newly created item.
  • Fix thumbnail page highlight disappearing when a highlighted page scrolls off-screen and its item is deleted.
    The highlight state is now re-applied in renderPageFromImage whenever an existing highlighted item is replaced,
    covering both the re-render and the scroll-back-into-view paths.
  • Fix GoForwardHistory always returning immediately due to a malformed guard condition. The expression
    m_loc_history_index + static_cast<int>(m_loc_history.size()) (a large positive sum, always truthy) was missing a comparison operator;
    corrected to m_loc_history_index + 1 >= static_cast<int>(m_loc_history.size()).
  • Add behavior.cache_password config option (default true). When
    auto-reloading a password-protected document, the password entered at open
    time is reused automatically. Set to false to prevent the password from
    persisting in memory beyond the initial unlock; auto-reload will then fail
    with an explanatory message for encrypted files.
  • Fix auto-reload from disk being unreliable. Three bugs: (1) the file-stability
    check compared two QFileInfo size readings taken back-to-back with no delay,
    so it always returned "stable" even while the file was still being written (e.g.
    latexmk truncates the PDF to 0 bytes before rewriting it); size is now compared
    across two 100 ms timer ticks. (2) QFileSystemWatcher::fileChanged can fire
    multiple times for a single atomic file replace, spawning concurrent reload
    chains that caused double reloads; a m_reload_pending guard now prevents this.
    (3) The watcher path was only re-added after a successful reload, so a
    transient corrupt file would permanently stop watching for future saves; the
    re-add is now unconditional.
  • Fix --single-instance CLI flag not triggering IPC forwarding. The probe condition
    used readSingleInstanceFromConfig() (reads the TOML file) and ignored the in-memory
    flag set by --single-instance, so the flag only started a server but never forwarded
    to an existing instance. Both sources are now combined before the probe runs.
  • Fix SyncTeX IPC tab reuse only searching the root view of each container, missing PDFs
    open in split panes. Now uses getAllViews() to search all views in the container.
  • Fix window title showing "Argument missing" warning when title_format used {}
    placeholder in the default value ("{} - lektra"), which is incompatible with
    QString::arg(). Default changed to "%1 - lektra" to match the existing TOML-loading
    path that already performs the {}%1 substitution.
  • Fix crash on exit caused by Lektra::~Lektra() calling lua_close(m_L) before
    m_command_manager was destroyed. Commands registered from Lua hold LuaRefGuard
    shared_ptrs whose destructor calls luaL_unref — which requires the Lua state to still
    be alive. m_command_manager is now explicitly reset before lua_close so those
    destructors fire in the correct order.

Improvements

  • Change cursor to a crosshair (Qt::CrossCursor) when in text-highlight mode, switching
    to the I-beam only while actively dragging a selection. The default arrow is restored on
    mode exit.
  • Fix BrowseLinkItem hover highlight rendering as nearly-black instead of yellow due to
    QColor being constructed with float literals (1.0, 1.0, 0.0) that were implicitly
    truncated to integers (1, 1, 0). Corrected to (255, 255, 0, 125).
  • Fix internal links targeting page 0 (the first page) being silently ignored. The guard
    if (_pageno) evaluated to false for page 0; corrected to if (_pageno >= 0).
  • Fix float-to-int truncation in highlightAnnotColor and DeleteAnnotationsCommand::undo
    where static_cast<int>(x * 255) could produce off-by-one values (e.g. 254 instead of
    255). Now uses qRound().
  • Remove duplicate non-const Model::DPI() overload that shadowed the canonical
    [[nodiscard]] const version and caused the [[nodiscard]] attribute to be bypassed on
    non-const Model objects.
  • supports_save(), supports_encryption(), supports_decryption(), and isImage() in
    Model were not marked const noexcept despite being pure queries with no side effects.
  • Z-value and zoom-limit constants in DocumentView were defined as preprocessor macros
    (#define); replaced with typed static constexpr values.
  • m_spacing in DocumentView was declared double but initialized with a float
    literal (10.0f); corrected to int.
  • BrowseLinkItem::_uri was a raw char* with no ownership contract, risking dangling
    pointer access when MuPDF frees the underlying string. Changed to QString with a
    const QString & setter.

Bug Fixes (Model / DocumentView)

  • Fix highlightAnnotColor in Model using static_cast<int>(x * 255) which could
    produce off-by-one color values; corrected to qRound().
  • Fix removeAnnotComment declared in Model.hpp but never implemented; added the
    missing definition in Model.cpp.
  • Fix duplicate "Animated" entry being pushed twice into the properties list for image
    files in Model::properties(); removed the redundant line.
  • Fix get_obj_num_at_rect calling pdf_load_page without any fz_try/fz_catch
    guard, which could crash on a malformed PDF or out-of-range page number. Wrapped in
    fz_try/fz_always/fz_catch with proper page cleanup.
  • Fix getFirstCharPos using return inside an fz_try block (bypassing fz_always
    cleanup) and manually dropping page and stext_page before returning, causing a
    double-free when combined with the fz_always block. Replaced with a found flag that
    exits the nested loops normally so fz_always performs the single correct cleanup.
  • Fix ScrollDown_HalfPage and ScrollUp_HalfPage using m_page_items_hash[m_pageno]
    which silently inserts a null entry and immediately dereferences it, causing a crash when
    the current page is not yet rendered. Changed to .value(m_pageno, nullptr) with a null
    guard.
  • Fix renderAnnotations and renderLinks using m_page_items_hash[pageno] (inserting
    null on miss) instead of .value(pageno, nullptr).
  • Fix annotColorChangeRequested lambda in renderAnnotations querying
    m_model->getAnnotColor(m_pageno, ...) using the current page instead of the captured
    pageno, returning the wrong color for annotations on non-current pages.
  • Fix renderLinks early-return guard using && across all three conditions, meaning an
    unsupported-links model only skipped rendering when the other two conditions also held.
    Split into two independent guards.
  • Fix clearVisiblePages removing scene items without deleting them, leaking every
    GraphicsImageItem on document close or reload. Added delete item before
    removeItem, consistent with clearVisibleLinks and clearVisibleAnnotations.
  • Fix ensureSearchItemForPage returning a cached item only when text search is not
    supported — the condition was inverted. Corrected to if (supports_text_search() && ...).
  • Fix Copy_page_image calling pageAtScenePos with a widget-space QPoint from
    viewport()->rect().center() instead of a scene-space coordinate; the result was
    immediately overwritten by the correct call. Removed the dead first call.

Bug Fixes (DocumentContainer)

  • Fix focusView() skipping assignment of m_current_view when it was nullptr — the
    guard if (m_current_view && m_current_view != view) required a non-null current view,
    so the first focus call after construction never set m_current_view or emitted
    currentViewChanged. Corrected to if (m_current_view != view) with a separate null
    check before deactivating the old view.
  • Fix closeView() emitting viewClosed before m_current_view was updated, so any slot
    responding to the signal would observe a stale (already deleted) current view. Moved the
    emit viewClosed(view) to after the m_current_view reassignment block in both branches.
  • Fix closeThumbnailView() and focusThumbnailView() being empty stubs that only checked
    for a null m_thumbnail_view and returned. Replaced with inline implementations delegating
    to closeView(m_thumbnail_view) and focusView(m_thumbnail_view) respectively.
  • Fix createThumbnailView() connecting the viewClosed lambda without
    Qt::UniqueConnection, causing the lambda to accumulate duplicate connections on repeated
    calls. Added Qt::UniqueConnection to the connect call.

Performance / Code Quality (DocumentContainer)

  • thumbSize = totalSize * 0.15 in equalizeStretch silently truncated a double result
    to int; changed to static_cast<int>(totalSize * 0.15).
  • The QSplitter handle stylesheet string "QSplitter::handle { background-color: palette(mid); }"
    was duplicated across five call sites in DocumentContainer.cpp; extracted to a
    static const char *const SPLITTER_STYLESHEET at file scope.

Performance / Code Quality (Model / DocumentView)

  • HSCROLL_STEP and VSCROLL_STEP in DocumentView.cpp were preprocessor macros;
    replaced with static constexpr int.
  • switch ((int)m_model->rotation()) used a C-style cast; changed to static_cast<int>.
  • img.save(fileName, format.toStdString().c_str()) created a temporary std::string
    to obtain a const char*; changed to format.toLatin1().constData().
  • PageDimensionCache::reset() assigned integer 0 to a vector<bool>; corrected to
    false. C-style casts (int) in set(), getOrDefault(), and get() replaced with
    static_cast<int>.
  • Redundant reserve() calls before copy-assignment of links and annotations vectors
    in renderPageWithExtrasAsync removed; copy-assign allocates its own storage.

Performance / Code Quality

  • LRUCache::put unconditionally called remove(key) before every insert, incurring a
    redundant map lookup for the common new-key path. Inlined the existence check to avoid
    the extra traversal.

  • trim_ws in utils.hpp trimmed leading whitespace with a per-character erase loop
    (O(n²)); replaced with a single erase(begin, find_if_not(...)) call.

  • GraphicsImageItem::height(), quad_y_center(), and charEqual() were missing
    noexcept despite being trivially non-throwing; added for consistency with surrounding
    functions.

  • Show_highlight_search() and Show_annot_comment_search() used && instead of ||
    in their null-guard (!m_doc && !m_doc->model()->...), causing a null pointer
    dereference when m_doc was null. Corrected to ||.

  • Tab_goto bounds check used || instead of && (index > 0 || index < count),
    making the condition almost always true and allowing out-of-range indices to pass.
    Corrected to index >= 1 && index <= count.

  • ShowAbout leaked an AboutDialog instance on every call since the dialog was
    heap-allocated but never freed. Added WA_DeleteOnClose so each dialog self-destructs
    when closed.

  • OpenFilesInNewTab warning message claimed extra files would be processed with no
    callback, but the function returned immediately. Message updated to accurately state
    that the operation is aborted.

  • std::move was called on a const QStringList & parameter in OpenFilesInNewTab,
    OpenFilesInVSplit, and OpenFilesInHSplit, silently falling back to a copy.
    Corrected to plain assignment; the lambda captures in VSplit/HSplit now move qfiles
    correctly.

  • Fix n/N search navigation skipping hits on the current page and jumping directly to
    the next/previous page. getClosestHitIndex now steps by flat hit index when the current
    hit is on the visible page, falling back to page-level anchoring only when the user has
    scrolled to a different page.

  • Scrollbars are kept visible while search hit markers are drawn on them
    (scrollbars.search_hits = true). The auto-hide timer and mouse-leave events no longer
    dismiss the scrollbar during an active search; normal auto-hide resumes once the search
    is cancelled or cleared.

  • Fix jump marker rendering at the wrong position after a zoom change. The marker's
    location is now stored as a PageLocation (page + document-space coordinates) instead
    of a scene-space point, so Reshow_jump_marker recomputes the correct scene position
    at call time regardless of zoom level.

Don't miss a new lektra release

NewReleases is sending notifications on new releases.