Trying it out

This page walks you through the current functional state, step by step. It is written to be followed even if you have never built a Rust program before. It is also honest about what works today and what does not yet — see the Roadmap for the plan.

There are two ways to read this page:

  • Just trying it out — you want to see your own Nextcloud show up as a normal folder on your Linux machine. Follow Part A from top to bottom.

  • Developing / testing the engine — you want to run the test suite, lint, or build the docs. Jump to For developers.

Part A — See your Nextcloud as a folder

First, the one idea everything depends on: mounting

wusel does not "download your files into an app". It mounts your Nextcloud — it makes your entire cloud appear as an ordinary folder on your computer, by default at ~/Wusel (that is the Wusel folder in your home directory).

Once it is mounted, that folder behaves like any other folder:

  • It shows up in your graphical file manager (Files/Nautilus, Dolphin, Thunar …) — you can double-click files, drag them, open them in any program.

  • It works in the terminal too (ls ~/Wusel, cat, cp, …).

There is no separate "app view" and no "terminal-only mode". The moment it is mounted, it is just a folder, everywhere on your system. When you unmount it, the folder becomes empty again and your files live only in the cloud. Files are fetched from the server the first time you open them (nothing is copied down in advance), and your edits are uploaded back.

Do the steps in order. Steps 1–3 you do once; steps 4–7 are the everyday use.

Step 1 — Prepare your Linux (install build tools)

Because wusel is currently run from source, your machine needs a C compiler and linker (Rust uses the system linker), plus the FUSE library that makes the "folder" trick possible. If these are missing you will see errors such as error: linker 'cc' not found — that is this step, not a bug.

Run the block for your distribution.

Debian / Ubuntu / Mint (anything using apt):

sudo apt update
sudo apt install build-essential pkg-config libfuse3-dev fuse3

Fedora / RHEL / Rocky / Alma (anything using dnf):

sudo dnf group install development-tools            # C compiler, linker, make (works on dnf4 and dnf5)
sudo dnf install pkgconf-pkg-config fuse3-devel fuse3
What each package is for
  • build-essential (apt) / the development-tools group (dnf) — the C compiler and linker Rust needs to turn code into a program. Without them: linker 'cc' not found.

  • pkg-config / pkgconf-pkg-config — lets the build find the FUSE library.

  • libfuse3-dev / fuse3-devel — the FUSE headers, needed while building.

  • fuse3 — the FUSE runtime (fusermount3), needed while running the mount.

Step 2 — Install mise and turn it on

This project pins its exact toolchain (a specific Rust version, Node for the docs, …) with a tool called mise. You do not install or manage Rust yourself: mise reads the project’s mise.toml and puts precisely the pinned versions on your PATH — but only after you activate it.

Activation is the step people miss. Installing mise only drops the mise program on disk; it does not yet manage your PATH. Until you activate it, cargo is "command not found" even though Rust is installed, and you are forced to prefix everything with mise exec --. Activating mise once is exactly what makes the pinned tools appear automatically the moment you cd into the project.

  1. Install the mise program once (official guide: mise.jdx.dev):

    curl https://mise.run | sh
  2. Activate it in your shell. Add the line for your shell to its startup file (most Linux systems use bash):

    # bash:
    echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
    
    # zsh:
    echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
  3. Close the terminal and open a new one so activation takes effect, then check it worked:

    mise --version

Step 3 — Get the code and build it

Clone the public repository and let mise fetch the pinned toolchain:

git clone https://github.com/itbh-at/wusel.git
cd wusel
mise trust                                       # allow this project's pinned tools (needed once per checkout)
mise install                                     # download the pinned Rust (first time: a few minutes)
If you only want to use Wusel on Fedora rather than build it, skip this whole page and install the packaged RPM instead — Installation.

mise trust is required because mise refuses to load a project’s tool versions until you confirm you trust that project — without it you would again see "command not found". After mise install, cargo and friends resolve to the project’s pinned versions automatically whenever you are inside this directory — no mise exec -- prefix needed.

Still getting cargo: command not found?

Then mise is installed but not activated (Step 2), or you have not run mise trust in this directory (Step 3). Fix those — that is the real cure. As a one-off workaround you can prefix any single command with mise exec --, e.g. mise exec — cargo run -p wusel — login …, but you should not have to do that for every command.

Step 4 — Log in to your Nextcloud

This uses Nextcloud’s official "Login Flow": it prints a URL, you approve the login in your browser, and an app password is stored locally — in your OS keyring by default; if no keyring is usable it falls back to the 0600 file ~/.config/wusel/credentials.json (readable only by you), which otherwise holds just non-secret metadata (server + login name). You can revoke the app password anytime from Nextcloud’s security settings.

cargo run -p wusel -- login https://cloud.example.org

Open the URL it prints, confirm in the browser, and wait for: ✓ Logged in as <user> on <server>.

How to read these cargo run … commands

You are running Wusel straight from its source code, so every command has a build wrapper in front of the actual wusel command. Take the line above:

  • cargo run -p wusel — "build the wusel program and run it".

  • -- — a divider. Everything after it is handed to wusel itself, not to cargo.

  • login https://cloud.example.org — the real wusel command and its argument.

So everything after -- is exactly what wusel’s own help calls a command. If you run it with no command at all (cargo run -p wusel), wusel just prints that help and lists login, mount, service … and so on — that is expected, not an error. Those bare names are what you place after the --. Read cargo run -p wusel -- as simply "wusel".

Step 5 — Mount it

Now make your cloud appear at ~/Wusel.

You will need two terminals

The mount command keeps running for as long as your Nextcloud is mounted — it does not return to the prompt, and that is correct. Think of it like starting a server: it stays in the foreground and holds that terminal.

So:

  1. Leave the mount running in Terminal 1.

  2. Open a second terminal window (or a second tab) for everything else.

Do not close Terminal 1 while you are using your files — closing it unmounts the folder.

Terminal 1 — start the mount and leave it running:

cargo run -p wusel --features fuse -- mount
Why --features fuse

Compared with login, this command has one extra piece: --features fuse, before the --. That is a build switch — mounting needs a Linux FUSE driver, so wusel does not compile the mount in by default (which keeps the program buildable on systems without FUSE). This flag turns that capability on at build time. Everything after -- is unchanged: mount there is wusel’s command, the same one its help lists.

You only need --features fuse for the commands that actually mount (mount and service). login, pin and accounts build and run without it — that is why Step 4 had no --features.

Notice: no path is given. With no path, wusel mounts to its default, ~/Wusel, and creates that folder for you if it does not exist. (You can pass an explicit path — … — mount ~/somewhere-else — but you rarely need to.)

The first build with --features fuse compiles the FUSE part and can take a minute. When it is up, Terminal 1 will sit there quietly — that is the mount doing its job.

Step 6 — Use your files

Now switch to Terminal 2, or just open your graphical file manager.

In the file manager: open your home folder and go into Wusel. Your cloud tree is there. Double-click a file to open it — it is fetched from the server on first open.

In Terminal 2:

ls -l ~/Wusel                    # your Nextcloud tree; read-only shares show up as r--
cat ~/Wusel/Some.txt             # streamed live from the server
echo "a new line" >> ~/Wusel/Some.txt   # your edit uploads to Nextcloud

It is read-write. Create, edit, mkdir, rename and delete through the folder, and the changes sync to Nextcloud. Read-only shares reject writes (you get a permission error). If two sides changed the same file, your version is saved beside the server’s as a … (conflicted copy …) rather than lost. Editor and office scratch files — vim swap files, LibreOffice/MS Office lock files, *.tmp, backups — are kept purely local and never clutter your cloud.

Online-only by default, cached on use. Nothing is copied down in advance, so a folder of huge files costs no disk until you touch them. When you open a file its read is served live at once, and the whole file is pulled into a local cache in the background — so it (and re-reads of it) become local. The cache is bounded by a configurable size with LRU eviction, so it never grows without limit. To keep specific files permanently offline (exempt from eviction, always synced), pin them (see Pin files/folders offline).

Live updates. Change, add or delete a file on the server (the web UI, another device) and — with the notify_push app installed on your Nextcloud — the mount reflects it within moments, including live in an open file-manager window (files appear and disappear without a manual refresh). Without notify_push, it catches up within a short revalidation interval.

If the mount feels busy in the background

A desktop file indexer (GNOME Tracker, KDE Baloo) or thumbnailer scans folders you browse and reads every file — and since opening a file caches it, that would mean a background download storm (and cache churn) you did not ask for.

By default Wusel protects against this: the mount root exposes synthetic, local-only .trackerignore/.nomedia markers (never uploaded to Nextcloud) that GNOME Tracker/LocalSearch honours to skip the whole tree. To opt back in to indexing, set [desktop] exclude_from_indexers = false in config.toml.

Caveats: KDE Baloo ignores these markers — exclude ~/Wusel in its settings instead. And thumbnailing is a separate, global "thumbnails for local files only" toggle (the FUSE mount counts as local).

Step 7 — Stop (unmount)

When you are done, go to Terminal 1 (the one running the mount) and press Ctrl+C. That unmounts ~/Wusel cleanly; the folder goes empty and your files live only in the cloud again.

If the folder is ever left in a stale state (for example because Terminal 1 was killed abruptly), force the unmount:

fusermount3 -u ~/Wusel

Recap

You installed the build tools (once), installed mise (once), built and logged in (once), then: mount in one terminal, use ~/Wusel everywhere else, and Ctrl+C to stop. That is the whole loop.

Install Wusel as a real program

Part A ran Wusel straight from source with cargo run — convenient for a first look, but it rebuilds each time and only works inside the project directory. Once you are happy with it, compile a single optimized binary and put it on your PATH. Then wusel is just a normal program you can run from anywhere, with no cargo and no mise involved.

Build the release binary (from inside the project directory):

cargo build -p wusel --features fuse --release

This produces one self-contained executable at target/release/wusel. It is an ordinary native Linux program: it does not need cargo, mise, or the source tree to run — only the fuse3 runtime library you already installed in Step 1.

Now copy it onto your PATH. System-wide is the most reliable, because /usr/local/bin is always on the PATH (it needs sudo):

sudo install -Dm755 target/release/wusel /usr/local/bin/wusel

Or install it just for your user, without sudo (this assumes ~/.local/bin is on your PATH, which it is on most desktop distributions):

install -Dm755 target/release/wusel ~/.local/bin/wusel

From now on it is a plain command:

wusel                                        # prints the list of commands
wusel login https://cloud.example.org        # log in
wusel mount                                  # mount ~/Wusel

Notice what disappeared compared with Part A: no cargo run, no --features fuse, no --. Those were all cargo build-time scaffolding. The installed binary already has mounting compiled in and there is no cargo to separate arguments from, so the command is simply wusel mount — exactly as its own help writes it.

Rebuilding and re-copying (cargo build … --release then install …) is also how you update the program after pulling new changes — there is no auto-update yet.

Keep it mounted automatically (systemd service)

Everything in Part A is manual: the mount lives only as long as Terminal 1 is open, and it is gone after a reboot or logout. That is perfect for trying things out.

If instead you want your ~/Wusel to be there permanently — mounted in the background, and again automatically after every login/reboot, with no terminal to keep open — install it as a systemd user service. This uses the installed wusel from the previous section:

wusel service enable            # mount now, and at every login, in the background
wusel service status            # is it running?
journalctl --user -u wusel@default -f   # live logs
wusel service disable           # stop doing that

Run service enable from the installed wusel (e.g. /usr/local/bin/wusel), not through cargo run. The service records the path of the exact binary you enable it with; a cargo run binary lives under target/ and would break the service the moment that directory is cleaned or rebuilt.

Table 1. Manual mount vs. service — the difference
mount (Part A) service enable

Runs in the foreground; holds a terminal

Runs in the background; no terminal needed

Stops when you press Ctrl+C or close the terminal

Keeps running; survives logout and reboot

Good for trying it out

Good for actually living with it

service enable writes and enables a systemd user unit for you — no editing unit files or running systemctl by hand. (It needs a systemd user session.) If you installed the Fedora RPM instead of building from source, the unit is already there as /usr/lib/systemd/user/wusel@.service — use systemctl --user enable --now wusel@default and skip service enable entirely.

Configuration is optional

You do not need a config file to complete Part A. With no config at all, wusel already mounts to ~/Wusel and uses sensible defaults. Skip this section unless you specifically want to change one of the defaults below.

If you do want to change something, create ~/.config/wusel/config.toml. Every key is optional:

[mount]
# Override the mountpoint used by `mount` with no path.
# Give an ABSOLUTE path — "~" is not expanded here, it would be taken literally
# and create a directory actually named "~".
point = "/home/you/Wusel"

[cache]
max_size = "5GB"        # blob budget; "0"/"unlimited" = no limit
max_age  = "30d"        # drop blobs unused this long; "0" = never

[sync]
revalidate_secs = 30    # re-list a directory older than this (no-push fallback)
push_floor_secs = 5     # min seconds between push-triggered re-lists of one directory
text_merge      = false # true = try a 3-way text merge on conflict before a conflicted copy
# What happens when a PINNED file goes out of date on the server:
#   manual = emblem only        ask = notify when more files go stale (default)
#   auto   = fetch it, but only over an unmetered connection; anything less
#            certain than "not metered" falls back to ask
refresh_pinned  = "ask"
# What an OUTDATED offline file serves when you OPEN it — a different question
# from the one above, which is about fetching unasked in the background:
#   newest            = always the current version (default)
#   newest-unmetered  = the current one, unless the connection is metered
#   offline           = the copy that is here; you refresh when you mean to
# While an outdated copy is what gets served, the file is READ-ONLY — see the
# concurrency page for why editing it would lose the newer version silently.
open_pinned     = "newest"
# ephemeral editor/OS files kept purely local (never uploaded). Setting this
# REPLACES the built-in default; omit it to keep the default set.
ignore_patterns = [".*.sw?", "4913", "*~", ".~lock.*#", "~$*", ".#*", "*.tmp", ".goutputstream-*", ".DS_Store", "Thumbs.db"]

[state]
# Where the metadata database goes. Normally derived from XDG_STATE_HOME; set it
# only to override, including to overrule the automatic move off a network
# filesystem (NFS/CIFS), where SQLite cannot lock reliably. A path you name here
# is always used — and warned about if it is on such a filesystem.
db_path = "/var/tmp/wusel/state.sqlite"

[tls]
ca_cert  = "/etc/wusel/my-ca.pem"   # trust a private / self-signed CA
insecure = false                      # true = disable TLS verification (DANGER, testing only)

[auth]
keyring = false   # default true (OS keyring); false = keep the app password in the 0600 file (see below)

[desktop]
exclude_from_indexers = true  # default; false = let GNOME Tracker index the mount

Self-hosting with a self-signed certificate? Point tls.ca_cert at your CA certificate, or set tls.insecure = true for a throwaway test (it logs a loud warning).

Storing the app password in the OS keyring

By default the app password lives in your desktop keyring (it is a revocable app password, not your account password); the credentials file then holds only non-secret metadata (server + login name). To keep it in the 0600 file instead, set [auth] keyring = false, or log in once with wusel login --keyring false <server-url>.

This is fail-soft on purpose — the keyring is never allowed to break the tool:

  • If the keyring cannot be written and verified at login, Wusel silently keeps the password in the 0600 file (it tells you it did).

  • If a keyring-stored password cannot be read later (the keyring is locked, or its service is not running — common on headless/SSH boxes), you get a clear message telling you to unlock it, re-run login, or set keyring = false — never a cryptic failure.

The keyring works unattended for a desktop user because the login keyring is unlocked at graphical login and Wusel runs as a systemd user service in that session. On a server without a graphical login, set keyring = false (the file).

On macOS: run it in a Linux container

FUSE is a Linux kernel feature, so the mount is Linux-only (native macOS support is far-future, experimental work — a File Provider frontend, not FUSE). On a Mac, do the mount inside the project’s podman container, which already has the build tools from Step 1:

mise run fuse-shell                                   # a Linux shell with /dev/fuse
# then, inside the container:
cargo run -p wusel --features fuse -- mount /mnt/nc
ls -l /mnt/nc
fusermount3 -u /mnt/nc

If mounting inside the container fails with a permission error, re-run mise run fuse-shell — the script already passes /dev/fuse and SYS_ADMIN.

For developers

The rest of this page is for working on Wusel rather than just using it. None of it needs a FUSE mount; it runs natively (including on macOS). The one exception is the real mount end-to-end test, which needs a Linux FUSE driver (mise run fuse-test, in the podman container).

Tests, type-check, lint

mise run check      # type-check the workspace except wusel-fuse (no binary produced)
mise run test       # build + run the native tests (wusel-core, wusel, wusel-desktop, wusel-mock)
mise run clippy     # lint, failing on any warning (-D warnings)
mise run fmt-check  # formatting is enforced
mise run fuse-test  # real FUSE mount e2e against wusel-mock (Linux container)

The native tests exercise the platform-independent engine (wusel-core) and the CLI (wusel without the fuse feature) — no FUSE driver needed. They include our own Rust mock server (wusel-mock, run in-process) with end-to-end coverage of the WebDAV client, the write buffer, conflict handling, deferred create, pinning, the ignore list, background revalidation and the ETag sync walk, plus a notify_push and a self-signed-TLS end-to-end. See Development for the testing strategy.

The CLI surface

cargo run -p wusel            # prints the usage: login / mount / service / accounts

Commands: login, mount, service enable|disable|status, accounts, account remove, pin/pins/unpin, desktop, cache clear, search-provider. A --account NAME selects a profile; without it the implicit default account is used.

Pin files/folders offline

Pinning keeps a file or directory available offline: it is downloaded now and never evicted, so it stays readable with the network gone. It needs no mount — it writes straight into the cache.

A pin is not yet a live copy. If the file changes on the server, the pinned copy is marked stale and the new content is fetched on the next read — which does touch the network. Until that read happens, going offline leaves you with the old version. Refreshing pinned files proactively is a roadmap item.
cargo run -p wusel -- pin Documents        # a directory (recursively)
cargo run -p wusel -- pin Notes.txt        # a single file
cargo run -p wusel -- pin                  # no path = the whole account
cargo run -p wusel -- pins                 # list pins
cargo run -p wusel -- unpin Documents      # release (blobs become evictable)

Directory pins cover their whole subtree; oversized files are cached too (a pin overrides the size budget). Add --account NAME for a named profile.

Pins are kept in ~/.config/wusel/pins.json, next to the configuration and not in the state database — they are what you said, not something we fetched. So cache clear leaves them alone, they survive a rebuilt or relocated database, and they travel with a roaming home directory. Their files download again on first use after a clear.

Multiple accounts

A single-account user can ignore this. To add a second Nextcloud:

cargo run -p wusel -- --account work login https://work.example.org
cargo run -p wusel --features fuse -- --account work mount   # → ~/Wusel-work
cargo run -p wusel -- accounts            # list configured accounts
cargo run -p wusel -- account remove work # remove a named profile (server untouched)

Each account is fully isolated (credentials, state, cache, mountpoint). Mountpoints may not overlap; mounting the same server+user twice is warned.

Build the docs

mise exec -- ./documentation/build.sh          # or: … build.sh watch

What does NOT work yet

  • File-manager integration on non-GNOME desktops (KDE Dolphin, Nemo, Thunar); GNOME/Nautilus emblems + context menus, GNOME Shell search and native notifications work.

  • Proactive refresh of pinned files when the server copy changes (see the note under Pin files/folders offline).

  • Refinements: fully concurrent content reads (the FUSE loop is single-threaded), and WebDAV LOCK (advisory locking). Emblems do refresh live, but a background hydration only pushes the refresh once the download finishes — see File-manager integration.