Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MruEntry

Most-recently-used list — a generic, persisted reactive collection with dedupe, pinning, and LRU-style cap eviction.

Apps define their own item type by implementing Keyed (a stable identity) and MruEntry (pin / touch semantics). The framework handles dedupe-on-add, pin-aware cap eviction, and cross-process-safe persistence via PersistedListModel; the app owns the item schema.

When to use

Use MruList for any "recently opened / recently used" feature: recent files, recent projects, recently visited locations, recently used palette entries, etc. The backing ListModel<T> is the same reactive handle you bind to a ListView or iterate in a menu — no separate notification plumbing is required.

Persistence

MruList::open reads <config_dir>/<name>.toml on first access (cross-process safe: the read is lock-protected, and every subsequent mutation merges by key against the document on disk, never overwriting the whole thing). Pass Duration::ZERO in tests to flush synchronously, or call MruList::flush_now explicitly.

use std::path::PathBuf;
use std::time::Duration;
use teksilo_settings::{AppPaths, Keyed, MruEntry, MruList};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Clone)]
struct RecentProject {
    path: PathBuf,
    display_name: String,
    last_opened: u64,
    pinned: bool,
}

impl Keyed for RecentProject {
    type Key = PathBuf;
    fn key(&self) -> PathBuf { self.path.clone() }
}

impl MruEntry for RecentProject {
    fn is_pinned(&self) -> bool { self.pinned }
    fn set_pinned(&mut self, p: bool) { self.pinned = p; }
    fn touch(&mut self) { self.last_opened += 1; }
}

// In tests: AppPaths::for_testing(tmp.path()) + Duration::ZERO.
// In production: AppPaths::new(qualifier, org, app).
let tmp = tempfile::tempdir().unwrap();
let paths = AppPaths::for_testing(tmp.path());
let recents: MruList<RecentProject> =
    MruList::open_with_delay(&paths, "recents", 10, Duration::ZERO).unwrap();

recents.add(RecentProject {
    path: "/projects/foo".into(),
    display_name: "Foo".into(),
    last_opened: 0,
    pinned: false,
});
assert_eq!(recents.model().len(), 1);

Builder methods at a glance

open, open_with_delay, open_at, model, max_items, add, remove, touch, set_pinned, is_pinned, clear, flush_now, path

API reference

📖 Full rustdoc API for this module

pub struct MruList

A persisted MRU list backed by PersistedListModel<T>.

Cheap to clone (Rc-shared internally). The reactive ListModel<T> returned by model() is the same handle the persistence bridge observes.

#![allow(unused)]
fn main() {
pub struct MruList<T: MruEntry> { /* fields */ }
}

Methods

pub fn open(paths: &AppPaths, name: &str, max_items: usize) -> Result<Self, SettingsFileError>

Open at <paths.config_dir()>/<name>.toml with the default debounce window.

Creates the file (and any missing parent directories) if it does not yet exist. Use open_with_delay to override the debounce in tests.

pub fn open_with_delay( paths: &AppPaths, name: &str, max_items: usize, delay: Duration, ) -> Result<Self, SettingsFileError>

Open at <paths.config_dir()>/<name>.toml with a custom debounce window.

Pass Duration::ZERO in tests to flush every mutation synchronously.

pub fn open_at( path: PathBuf, max_items: usize, delay: Duration, ) -> Result<Self, SettingsFileError>

Open at an explicit path with the given debounce window.

Lower-level alternative to open when the caller already has a resolved PathBuf (e.g. from a custom directory layout).

pub fn model(&self) -> &ListModel<T>

The underlying reactive list; bind to UI widgets via clones of this handle.

Read-only for mutation purposes. Use add, remove, touch, set_pinned, clear to mutate — those are what enqueue the matching persisted op. Mutating the returned ListModel directly updates what's on screen but is never written to disk.

pub fn max_items(&self) -> usize

Returns the maximum number of unpinned entries kept in the list.

Pinned entries do not count toward this cap and are never evicted automatically.

pub fn add(&self, mut entry: T)

Insert entry at the front, deduping by entry.key(). T::touch is invoked before insertion, so the freshly-added entry reflects "now". If a previously-pinned entry is re-added without pinned, the pin state is preserved.

pub fn remove<Q>(&self, key: &Q) where T::Key: Borrow<Q>, Q: Eq + ?Sized,

Remove the entry whose key matches, then schedule a debounced flush.

No-op when no entry with that key is present. Generic over Q so callers can pass a borrowed form of the key (e.g. &Path when T::Key = PathBuf, &str when T::Key = String) without having to allocate an owned key just to look one up.

pub fn touch<Q>(&self, key: &Q) where T::Key: Borrow<Q>, Q: Eq + ?Sized,

Mark the entry whose key matches as freshly used by calling MruEntry::touch on a clone of it, then write it back and schedule a debounced flush. No-op when no entry matches.

pub fn set_pinned<Q>(&self, key: &Q, pinned: bool) where T::Key: Borrow<Q>, Q: Eq + ?Sized,

Set the pin flag of the entry whose key matches to exactly pinned (idempotent — unlike a toggle, replaying this against an already-applied peer change does not flip it back). No-op when no entry matches.

pub fn is_pinned<Q>(&self, key: &Q) -> bool where T::Key: Borrow<Q>, Q: Eq + ?Sized,

Is the entry with this key currently pinned? false when no entry matches.

The counterpart set_pinned deliberately takes the desired value rather than toggling, because a toggle is not idempotent: replayed against a peer process's already-applied toggle it would flip the value straight back, inverting their change. A pin button still needs to toggle, though — so read the current value here and pass its negation:

let pinned = mru.is_pinned(path);
mru.set_pinned(path, !pinned);

pub fn clear(&self)

Drop every entry (pinned or not) and schedule a debounced flush.

pub fn flush_now(&self) -> Result<(), SettingsFileError>

Write the list to disk synchronously, bypassing the debounce window.

Useful at app shutdown or at the end of a test to guarantee the file reflects the in-memory state before the process exits.

pub fn path(&self) -> &Path

The TOML file path this list reads from and writes to.