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(), andview:is_narrowed() -> boolean
in the Lua view API. - Add horizontal and vertical page flip (
flip_horizontal/flip_verticalcommands, default
bindings|/_). Flip state is stored inModelalongside 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 asview:flip_horizontal()andview: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.timerLua module backed byQTimer. Timers are created with
lektra.timer.new(interval_ms, callback [, single_shot]), supportstart,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__gcmetamethod ensures theQTimerand Lua callback reference are
released as soon as the userdata is garbage-collected, making explicitdestroy()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.viewmethods onViewuserdata).
Rotation methods were previously missing from the view API entirely. - Implement SyncTeX forward search (editor → lektra).
--synctex-forwardnow calls
synctex_display_queryand jumps to the matching PDF location viaGotoLocation,
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-instanceCLI flag. Forces single-instance mode for one invocation
without requiringbehavior.single_instance = truein the config file. --synctex-forwardnow implicitly acts as single-instance: it always attempts to
connect to a running instance via IPC regardless of thesingle_instanceconfig
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_maximizecommand). 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 bysplit.maximize_indicator(bool, defaulttrue) and
split.maximize_indicator_color(ARGB, default0xCC2979FF). Both options are exposed
in the Lua opt API aslektra.opt.split.maximize_indicatorand
lektra.opt.split.maximize_indicator_color. - Add focus border for the active split pane. Three new
splitconfig options:
split.focus_border(bool, defaultfalse) enables the feature;
split.focus_border_color(ARGB integer, default0xFF4FC3F7) sets the border colour;
split.focus_border_width(integer, default2) sets the thickness in pixels. All three
are exposed in the Lua opt API aslektra.opt.split.focus_border,
lektra.opt.split.focus_border_color, andlektra.opt.split.focus_border_width. - Add
portal.splitconfig 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_pickercommand, default
bindingCtrl+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.HighlightAnnotationnow overridesshape()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.initGuiwas setting the background brush only onm_gscene
(QGraphicsScene), whosedrawBackground()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 bothm_gview
(QGraphicsView) — which fills the entire viewport — andm_gsceneso that
the narrow-clip strip painting inGraphicsView::paintEventcontinues to read
the correct colour fromscene()->backgroundBrush(). - Fix thumbnail panel defaulting to single-page layout instead of vertical.
handleOpenFileFinishedunconditionally 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.
GotoPagenow callsClearTextSelection()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).
renderPageFromImagenow saves theisHighlighted()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 inrenderPageFromImagewhenever an existing highlighted item is replaced,
covering both the re-render and the scroll-back-into-view paths. - Fix
GoForwardHistoryalways 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 tom_loc_history_index + 1 >= static_cast<int>(m_loc_history.size()). - Add
behavior.cache_passwordconfig option (defaulttrue). When
auto-reloading a password-protected document, the password entered at open
time is reused automatically. Set tofalseto 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 twoQFileInfosize 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::fileChangedcan fire
multiple times for a single atomic file replace, spawning concurrent reload
chains that caused double reloads; am_reload_pendingguard 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-instanceCLI flag not triggering IPC forwarding. The probe condition
usedreadSingleInstanceFromConfig()(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 usesgetAllViews()to search all views in the container. - Fix window title showing
"Argument missing"warning whentitle_formatused{}
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{}→%1substitution. - Fix crash on exit caused by
Lektra::~Lektra()callinglua_close(m_L)before
m_command_managerwas destroyed. Commands registered from Lua holdLuaRefGuard
shared_ptrs whose destructor callsluaL_unref— which requires the Lua state to still
be alive.m_command_manageris now explicitly reset beforelua_closeso 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
BrowseLinkItemhover highlight rendering as nearly-black instead of yellow due to
QColorbeing 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 toif (_pageno >= 0). - Fix float-to-int truncation in
highlightAnnotColorandDeleteAnnotationsCommand::undo
wherestatic_cast<int>(x * 255)could produce off-by-one values (e.g. 254 instead of
255). Now usesqRound(). - Remove duplicate non-const
Model::DPI()overload that shadowed the canonical
[[nodiscard]] constversion and caused the[[nodiscard]]attribute to be bypassed on
non-constModelobjects. supports_save(),supports_encryption(),supports_decryption(), andisImage()in
Modelwere not markedconst noexceptdespite being pure queries with no side effects.- Z-value and zoom-limit constants in
DocumentViewwere defined as preprocessor macros
(#define); replaced with typedstatic constexprvalues. m_spacinginDocumentViewwas declareddoublebut initialized with afloat
literal (10.0f); corrected toint.BrowseLinkItem::_uriwas a rawchar*with no ownership contract, risking dangling
pointer access when MuPDF frees the underlying string. Changed toQStringwith a
const QString &setter.
Bug Fixes (Model / DocumentView)
- Fix
highlightAnnotColorinModelusingstatic_cast<int>(x * 255)which could
produce off-by-one color values; corrected toqRound(). - Fix
removeAnnotCommentdeclared inModel.hppbut never implemented; added the
missing definition inModel.cpp. - Fix duplicate
"Animated"entry being pushed twice into the properties list for image
files inModel::properties(); removed the redundant line. - Fix
get_obj_num_at_rectcallingpdf_load_pagewithout anyfz_try/fz_catch
guard, which could crash on a malformed PDF or out-of-range page number. Wrapped in
fz_try/fz_always/fz_catchwith proper page cleanup. - Fix
getFirstCharPosusingreturninside anfz_tryblock (bypassingfz_always
cleanup) and manually droppingpageandstext_pagebefore returning, causing a
double-free when combined with thefz_alwaysblock. Replaced with afoundflag that
exits the nested loops normally sofz_alwaysperforms the single correct cleanup. - Fix
ScrollDown_HalfPageandScrollUp_HalfPageusingm_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
renderAnnotationsandrenderLinksusingm_page_items_hash[pageno](inserting
null on miss) instead of.value(pageno, nullptr). - Fix
annotColorChangeRequestedlambda inrenderAnnotationsquerying
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
renderLinksearly-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
clearVisiblePagesremoving scene items without deleting them, leaking every
GraphicsImageItemon document close or reload. Addeddelete itembefore
removeItem, consistent withclearVisibleLinksandclearVisibleAnnotations. - Fix
ensureSearchItemForPagereturning a cached item only when text search is not
supported — the condition was inverted. Corrected toif (supports_text_search() && ...). - Fix
Copy_page_imagecallingpageAtScenePoswith a widget-spaceQPointfrom
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 ofm_current_viewwhen it wasnullptr— the
guardif (m_current_view && m_current_view != view)required a non-null current view,
so the first focus call after construction never setm_current_viewor emitted
currentViewChanged. Corrected toif (m_current_view != view)with a separate null
check before deactivating the old view. - Fix
closeView()emittingviewClosedbeforem_current_viewwas updated, so any slot
responding to the signal would observe a stale (already deleted) current view. Moved the
emit viewClosed(view)to after them_current_viewreassignment block in both branches. - Fix
closeThumbnailView()andfocusThumbnailView()being empty stubs that only checked
for a nullm_thumbnail_viewand returned. Replaced with inline implementations delegating
tocloseView(m_thumbnail_view)andfocusView(m_thumbnail_view)respectively. - Fix
createThumbnailView()connecting theviewClosedlambda without
Qt::UniqueConnection, causing the lambda to accumulate duplicate connections on repeated
calls. AddedQt::UniqueConnectionto theconnectcall.
Performance / Code Quality (DocumentContainer)
thumbSize = totalSize * 0.15inequalizeStretchsilently truncated adoubleresult
toint; changed tostatic_cast<int>(totalSize * 0.15).- The QSplitter handle stylesheet string
"QSplitter::handle { background-color: palette(mid); }"
was duplicated across five call sites inDocumentContainer.cpp; extracted to a
static const char *const SPLITTER_STYLESHEETat file scope.
Performance / Code Quality (Model / DocumentView)
HSCROLL_STEPandVSCROLL_STEPinDocumentView.cppwere preprocessor macros;
replaced withstatic constexpr int.switch ((int)m_model->rotation())used a C-style cast; changed tostatic_cast<int>.img.save(fileName, format.toStdString().c_str())created a temporarystd::string
to obtain aconst char*; changed toformat.toLatin1().constData().PageDimensionCache::reset()assigned integer0to avector<bool>; corrected to
false. C-style casts(int)inset(),getOrDefault(), andget()replaced with
static_cast<int>.- Redundant
reserve()calls before copy-assignment oflinksandannotationsvectors
inrenderPageWithExtrasAsyncremoved; copy-assign allocates its own storage.
Performance / Code Quality
-
LRUCache::putunconditionally calledremove(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_wsinutils.hpptrimmed leading whitespace with a per-charactereraseloop
(O(n²)); replaced with a singleerase(begin, find_if_not(...))call. -
GraphicsImageItem::height(),quad_y_center(), andcharEqual()were missing
noexceptdespite being trivially non-throwing; added for consistency with surrounding
functions. -
Show_highlight_search()andShow_annot_comment_search()used&&instead of||
in their null-guard (!m_doc && !m_doc->model()->...), causing a null pointer
dereference whenm_docwas null. Corrected to||. -
Tab_gotobounds check used||instead of&&(index > 0 || index < count),
making the condition almost always true and allowing out-of-range indices to pass.
Corrected toindex >= 1 && index <= count. -
ShowAboutleaked anAboutDialoginstance on every call since the dialog was
heap-allocated but never freed. AddedWA_DeleteOnCloseso each dialog self-destructs
when closed. -
OpenFilesInNewTabwarning 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::movewas called on aconst QStringList ¶meter inOpenFilesInNewTab,
OpenFilesInVSplit, andOpenFilesInHSplit, silently falling back to a copy.
Corrected to plain assignment; the lambda captures in VSplit/HSplit now moveqfiles
correctly. -
Fix
n/Nsearch navigation skipping hits on the current page and jumping directly to
the next/previous page.getClosestHitIndexnow 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 aPageLocation(page + document-space coordinates) instead
of a scene-space point, soReshow_jump_markerrecomputes the correct scene position
at call time regardless of zoom level.