┌──────────────────────────── Filee.App (Avalonia) ─────────────────────────────┐
global │ TriggerService ──► GestureDetector ──► RadialController ──► RadialWindow/DonutMenu │
mouse/keys │ (SharpHook) (pure state (drag / click (transparent, │
─────────► │ machine) mode) topmost window) │
│ │ drop / click │
Explorer │ SingleInstance (pipe) ── --convert files ──►│ │
context │ Home drop zone ─────────────────────────────┘ │
menu │ ▼ │
│ ConversionService ──► ToastWindow (progress) │
│ MainWindow (settings pages) ◄──► UserDataStore (JSON in %APPDATA%\Filee) │
└──────────────────────────────────────────────┬──────────────────────────────────────┘
▼
┌──────────────────────────── Filee.Core ───────────────────────────────────────────┐
│ JobQueue ──► RoutePlanner (Dijkstra over converter edges) ──► IConverter steps │
│ FormatRegistry · Preset · OutputPathResolver · ToolbarProfile · ProfileSelector │
└──────────────────────────────────────────────┬──────────────────────────────────────┘
▼
┌──────────────────────────── Filee.Engines ────────────────────────────────────────┐
│ Built in: Magick (images, RAW) · PdfSharp · Pdfium · PdfPig · Svg.Skia · ICNS │
│ HwpxWriter + DocxWriter over DocumentReaders (DOCX, XLSX, XLS, ODS, PPTX, │
│ PDF, HTML, EPUB, MOBI, FB2, HWPX, Markdown) · Spreadsheets · OOXML variants │
│ · E-mail · Data · E-books · CAD · Fonts · Subtitles · 3D models │
│ Bundled: rhwp (HWP→PDF, HWP↔HWPX) · 7-Zip (archives) · Unhwp · Markdig │
│ Optional: FFmpeg · LibreOffice + H2Orestart · calibre · Ghostscript · Pandoc │
└────────────────────────────────────────────────────────────────────────────────────┘
- Trigger –
TriggerServicereceives global input from SharpHook and feedsGestureDetector. A drag gesture (modifier + drag past the threshold) opens the donut before files are known. - Donut –
RadialWindowis transparent, topmost and not activated, so Explorer's drag continues. The firstDragEnterdelivers the file list;RadialViewModelpicks aToolbarProfileby extension (ProfileSelector: the first normal profile that has every file's extension; for a mix of types the first mixed-selection profile whose checked extensions cover the files, else the one with nothing checked) and asksPresetAvailabilitywhich presets can run (disabled slices get a reason). - Drop – the slice under the cursor is a preset.
ConversionService.Startenqueues aConversionJob. The drop is always reported as Copy so Explorer never deletes the source files. - Plan – for every file,
RoutePlannerfinds the cheapest chain of converter edges from the source format to the target (at most 3 hops). Engine priority (Settings → Engines) breaks ties. Some edges are limited: tracing only starts from raster images (ImageSourcesOnly), and PDF → PPTX picture slides are only ever a route of their own (DirectOnly), never a step of a longer one. - Run –
JobQueueruns steps with per-engine concurrency limits. Intermediate files live in a temp folder; final outputs are named byOutputPathResolver(pattern, folder, conflict policy; the source is never overwritten). Each name stays reserved while a conversion holds it. A conversion that fails or is cancelled deletes what it began writing, but only at a path that was its alone: nothing was there before and no other conversion was handed the same name ("Overwrite"). It frees its names and removes an output folder it made if that is left empty. A PDF target's finishing (watermark, page numbers, password;IPdfFinisher) runs after the last step, on every PDF a file became, and once on a merged PDF. Replace the original (OutputRule.ReplaceOriginal, off by default) comes after that, for a finished result of the source's own format and extension, or another spelling of it (.jpeg/.jpg; TXT also covers scripts and logs, PPT also.ppsand.pot, so those need the same extension). The result is first moved next to the source under a temporary name, then the source goes to the Recycle Bin / Trash (IPlatformServices.MoveToTrash, in the app and the command line), then the result takes its name. Windows only ever recycles (IFileOperation with a sink that refuses a delete it can't recycle). When the source can't go there (no Recycle Bin on the drive, a file open elsewhere, a read-only file), both stay and the file's notice says why, with a translated reason (trash.*). A result "Only if smaller" copied from the source leaves the source alone. Watch folders and files from a browser never replace. "Merge into one PDF" converts each file to PDF and merges with PDFsharp. "Compress into one archive" packs the dropped files, unconverted, with the archive engine (IFileCombiner). "Merge videos", "Merge audio" and "Video from images" join the files, in the order picked in the merge order window, into one file with FFmpeg (IMediaJoiner); pictures become PNG through the image engines first. Like the other one-file jobs, the output follows "subfolder only for several files", and a join that fails or is cancelled is undone by the job's allocator, which deletes only paths that were the job's own. - Report – progress and results appear in the toast window and in Home → Recent conversions.
Renderers check only the original source for referenced fonts.
ConversionStep.MissingFontsreports any missing families toFileResult.MissingFonts; the toast card can offer one trusted source, while the CLI and watch jobs report the names without pausing conversion or installing anything.
A picture, a link to a file or a download dragged out of a browser opens the donut like a file from the file manager.
A drag gesture over a browser (WebBrowsers, by process name) counts as over the file manager, so the default scope
covers it; click and key gestures don't, as they read the file manager's selection.
DraggedFiles.Read asks BrowserDrags (App, reads Avalonia's drag data) first; BrowserDrag (Core, pure rules)
decides what in the drag is a file:
| Source | What the drag carries | What Filee takes |
|---|---|---|
| Chrome / Edge, Windows | the picture as a virtual file (FileGroupDescriptorW + FileContents, which Avalonia 12 gives as a storage item without a local path), a .url virtual file for links, UniformResourceLocatorW, text/x-moz-url, HTML Format |
the picture's own bytes; for a link, its address |
| Firefox, Windows | the picture written to %TEMP% (CF_HDROP), text/x-moz-url, file promise formats |
the temporary copy (a local file in the temp folder and a web address on the drag) |
| Chrome / Firefox, Linux | text/x-moz-url, _NETSCAPE_URL, text/html (Avalonia turns text/uri-list into local files only and drops http addresses) |
the address |
| Safari / Chrome, macOS | file promises (not receivable through Avalonia), public.url, public.html, sometimes picture data |
the address, else the picture data |
Rules, in order: files handed over with their bytes (never .url shortcuts or programs); a picture (an HTML fragment
that is one <img> and no text, or a Firefox file promise); a DownloadURL a page sets; links only when the address
ends in an extension Filee converts and isn't a web page (.html, .php, none); last, picture pixels for a blob:
picture. Plain text is never read. data: addresses are decoded.
Until the conversion runs, the files stand as placeholders (WebDrop): paths in %TEMP%\Filee\web\<drop>\drop\<n>\
named like the file will be, so the donut picks the profile by extension without touching the network. No HEAD request
is made: a picture without an extension stands as .jpg and a link without one isn't offered. ConversionService.Start
gives the job a WebDropFetcher (ISourceFetcher): JobQueue saves every source first (half of its progress), then
converts. Failures are per file (error.web_*), cancelling stops a download, and the job toast/ring shows both phases.
While files are saved, a badge pops up above the donut's progress ring and above the toast's progress bar with the
speed and the amounts ("2.4 MB/s · 12.3 / 48.0 MB", "· 2 of 3 files" for several): FileResult.Transfer holds each
file's byte count, ConversionJob.Transfer() sums them, and DownloadBadge (App) averages the speed over the last
second with the Engines page's TransferRate and units, rewritten four times a second and shown after 300 ms.
- Where: Settings → General → "Files dragged from a browser" (
AppSettings.BrowserDrops): the user's Downloads folder (IPlatformServices.DownloadsFolder: the shell's known folder on Windows,XDG_DOWNLOAD_DIRon Linux,~/Downloadson macOS) or a chosen one. "Next to the source" means this folder for these files (OutputRule.SourceFolder, set per job, never saved), so subfolder rules apply there and fixed folders stay fixed. - Save only the converted file: the original goes to the drop's work folder, which
Releasedeletes when the job ends; leftovers from a crash are deleted at start-up after a day. - Downloads (
WebDownloader): http and https only, at most 5 redirects (never from https to http or off the web), 2 GB cap whether or not the server says the size, 30 s to answer and 30 s without data at most. No cookies or credentials are sent: a file behind a sign-in comes back as the sign-in page, which is refused as a web page ("save it from the browser first"). A browser's own bytes (virtual files, Firefox's copy) don't have that limit. - Names and safety: Content-Disposition, else the page's name, else the address; then the first bytes decide the
extension of pictures and PDFs (
.jpgthat is WebP becomes.webp). Names are cut to a plain file name (no folders, no device names, at most 120 characters) and never replace an existing file (name (2).ext). Programs, installers and scripts are refused by extension, media type and first bytes; nothing downloaded is ever run, and every file goes through the same converters as any other.
Controls/DonutMenu.cs draws the ring itself (no child controls) for speed. DonutGeometry.cs holds the pure math
(slice angles, hit testing) and is unit tested. The same control is the live toolbar and, with IsEditMode, the
editor on the Donut toolbar settings page (drag to reorder, ✎ / right-click to edit).
Animations are small springs advanced per frame; Motion.Enabled == false (Reduce animations) snaps them.
Every ToolbarProfile has two faces: PresetIds (the front) and BackPresetIds (the back; the built-in donuts get
the less common formats and tools of their file type, see BuiltInData.CreateProfiles). The flip key
(DonutSettings.FlipKey, Tab unless recorded otherwise in Settings → Shortcuts; EffectiveFlipKey refuses keys the
donut or a keyboard shortcut uses) and the mouse wheel (FlipWithWheel, one flip per turn of the wheel: notches less
than DonutMenu.WheelQuietMs apart are one turn) flip the open donut (DonutMenu.Flip.cs): the ring turns about its
vertical axis in 200 ms, an open group's ring folding first, and halfway, edge-on, FaceSwapping has
RadialViewModel show the other face, so slices, hit tests and drops always belong to the face that is drawn.
Reduce animations swaps the faces at once. RadialViewModel builds both faces with the same PresetAvailability
check (the back right after the front is on screen); a face with no slice for the files (or none at all) isn't
flipped to, and only then does the hub leave out its "⟳ Tab back" hint (a mouse in place of the key when only the
wheel flips). In click mode the focused window gets the key (DonutMenu.HandleKey) and the donut the wheel. During a
drag the donut's window has no keyboard focus, so RadialController turns on TriggerService.WatchFlipKey while
the drag donut shows files and flips on FlipKeyPressed / FlipWheelTurned: the global hook only listens (it
can't hold an event back; Explorer and Finder ignore Tab during a drag) and reports one flip per key press.
The editor's Front / Back switch (ToolbarPageViewModel.EditingBack) edits either list with the same drag and drop.
A group is one slice that opens up to six non-group actions. Its members may be conversions or interactive tools
(for example the PDF Edit group contains Crop and the page editor); groups cannot contain another group. The group
editor offers both kinds, and UserDataStore.EnsureConsistency removes missing, duplicate, nested or excess members
without excluding tools.
The image Crop tool (tool-crop) and PDF Crop (tool-pdf-crop) open Views/CropOverlayWindow instead of running a job:
the files one after another,
each cropped by hand and saved by CropService (the preset's output rule, the source's quality kept, animated or
multi-frame pictures refused, the EXIF thumbnail dropped), one history entry for the run. A PDF preview is rendered
with PDFium and the chosen rectangle changes every page's crop box in a copied document opened in modify mode, so
text, vectors, outlines, forms, page labels and document information remain PDF objects and stay attached.
CropOverlayViewModelwalks the queue (Enter saves and opens the next file, Skip, Esc closes), keeps aCropQueueItemper file for the strip of thumbnails (waiting, current, done, skipped, failed; a thumbnail is a small copy of the preview, made once) and, after the last file, shows the summary when something failed or was skipped. Every file starts free and whole, its portrait/landscape choice following the picture.Controls/CropCanvasis the stage: the picture insideImagePadding, the part outside the frame frosted (a copy at most 40 px across, drawn large) under a veil of the page colour, the frame in the accent colour with round handles, a rule-of-thirds grid while dragging and a size chip that follows the frame. A frame being moved snaps onto the picture's vertical or horizontal centre line withinCropMath.SnapDistance(8 px) and shows that line as a dashed guide until the drag ends. The preview is downscaled once byCropService.LoadPreviewAsync; nothing is decoded again while dragging. Programmatic changes (reset, a ratio, a new picture) ease in with tweens read against the clock, so a lost frame never leaves one half done; dragging and arrow-key nudges follow at once, andPlayAnimations(fromMotion.Enabled) turns all of it off.Controls/AspectDialis a small donut of the ratios (free, original, 1:1, 5:4, 4:3, 3:2, 16:9): each slice aRingSegmentbutton that answers only inside its wedge (DonutGeometry.Wedge, rounded by the softness), the hub swaps portrait and landscape. A with the pointer over the stage opens it around the pointer, as the donut opens, growing from its middle and kept inside the window; A without a pointer there (the keyboard) and its toolbar button open it over the button with a slice focused for the arrow keys.AspectGlyphis the dial in miniature on that button, andCropSummaryRingthe end summary's donut (a slice per file).Controls/CropMathholds the arithmetic (ratios and their portrait forms, nudging by whole pixels, the frame in pixels, where the size chip goes, where the dial opens) and is unit tested.
tool-pdf-pages opens Views/PdfPageEditorWindow: it previews, reorders, rotates, deletes, adds and extracts pages.
Each source PDF's bytes and page sizes are cached once after the window has appeared; thumbnails and the large preview
render off the UI thread. A whole-document edit keeps the original catalogue and changes /Rotate in modify mode.
An actual reorder, deletion, addition or extraction builds a new PDF from selected page objects. A PDF that opens
without a password but requires an owner password for modify mode uses the same page-copy fallback as compression;
its outlines and form fields are not preserved by that fallback. Generated copies are writable even when the source
has the read-only attribute; the source's bytes and attributes are left alone.
The Cut a clip tool (tool-clip, first on the back of the video donut) opens Views/ClipWindow instead of running a
job. The window only chooses the clip; writing it is an ordinary conversion.
Core/Presets/ClipCutterdoes every edit without the window: the parts of the clip (start, end, speed, volume, mute), gaps left by Delete, the blade, edge trims that take back or cut away, music on A2, fades, Remove silence and undo / redo (one step per drag). The result is aClipEdit, unit tested on its own.ClipWindowViewModelholds the window's state: the overview of the whole video (Controls/ClipTimeline: 16 frames, the loudness, the handles) and the sequence under it (Controls/ClipSequence: V1 pictures, A1 the video's sound, A2 music). Both are custom-drawn controls; their pictures and loudness are taken again only when the clip's ends rest.- Space plays the clip as it will be written:
Engines/Media/ClipPreviewruns the same filter graph as the output, the pictures as raw BGRA frames over a pipe and the sound throughffplay(when it sits next to Filee's FFmpeg; without it the preview is silent). FFmpeg for the window runs below normal priority. - Save calls
ClipTool.ConversionOf, which carries theClipEditinMediaOptions.Clip, so the toast, cancel, the output rules ({name}_clipnext to the video) and the history work as for any conversion.FfmpegConvertercopies one plain stretch with-ss/-toand-c copy(Fast, no re-encoding), or encodes the exact cut. - Anything else is one filter graph (
MediaEncoding.ClipGraphOf): the video is read once,split/trimper piece,setptsand chainedatempofor speed,volume,color/anullsrcfor gaps,concat, the music as a second input (atrim,volume,afade,adelay,amix), thenfade/afade. It is encoded with the preset's quality, size and codec.
The tool-sheet-studio tool opens an independent Views/SpreadsheetPrintStudioWindow for each dropped XLSX workbook
instead of starting a normal conversion job. SpreadsheetPrintStudioViewModel owns an ordered, non-destructive print plan: each visible
sheet starts as one region and the user can add more regions, change paper, orientation, scale, margins, page order,
hidden rows and manual breaks, then see the PDF that those settings will produce.
SpreadsheetPrintInspectorreads workbook metadata and streams the large worksheet data withXmlReader; opening a large workbook neither builds nor retains anXDocumentfor every sheet.SpreadsheetPrintPackagemakes a disposable XLSX underJobQueue.DefaultWorkRoot. It keeps every worksheet and named range so cross-sheet formulas remain valid, hides the sheets not being rendered, and changes only the chosen sheet's print settings. A new print area is written only after the user explicitly changes or accepts the range.SpreadsheetPrintServiceruns LibreOffice once for each region. Preview and export use the same disposable-workbook path; export then joins the region PDFs in the chosen order with PDFsharp. The ordinary XLSX → PDF donut conversion can still differ because route planning may select Filee's built-in spreadsheet reader instead of LibreOffice.PdfPreviewDocumentcaches a PDF's bytes and page geometry. The studio asks PDFium/PDFtoImage for small thumbnails and only one large selected page, all decoded away from the UI thread; exporting never performs preview rendering. Cancellation closes LibreOffice and the temporary job folder is removed on success, error or window close.
UserDataStore owns settings.json, presets.json, profiles.json and history.json
(System.Text.Json source generation, atomic writes, broken files are kept as *.broken).
Bump AppSettings.CurrentSchemaVersion and add a step to SettingsMigrations when the format changes.
LocalizationServiceloadsAssets/i18n/<lang>.jsonintoApplication.Resources; XAML uses{DynamicResource key}, so switching language updates the UI live.ThemeServicewrites the accent colour, corner radii, fonts, the reading direction and Fluent'sSystemAccentColor*into application resources. Arabic reads right to left:Styles/Soft.axaml's Window style takesFlowDirectionfrom that resource, so every window mirrors; the donut, the crop canvas and animated images stay left to right. Light/dark surface colours are theme dictionaries inStyles/Soft.axaml; a user'stheme.json(CustomTheme, see CUSTOM-THEMES.md) overrides them with theme dictionaries in application resources, which are searched first.- Window zoom (
Services/UiZoom): Theme → Window size (− / + and Reset), and in any tracked window the platform's command key (HotkeyConfiguration.CommandModifiers: Ctrl, Cmd on macOS) with the mouse wheel, plus / minus (Shift allowed, Alt not: AltGr is Ctrl + Alt) or 0 for 100 %. 70–180 % in 10 % steps, every window at once.UiZoom.Track(window)wraps the window's content in aLayoutTransformControl, so layouts see the room they really have. Small wheel deltas (touchpads, smooth wheels, pinching on a Windows touchpad) add up to one step per whole notch. Windows that can't be resized grow with the zoom (their designed width × the zoom) up to their screen, cap their height to it and scroll; the engine setup keeps itself on screen and scrolls its own list (fitsItself). Tracked: the main window, the preset, group, rename and merge-order editors, the engine setup, LibreOffice options, close, sponsor, what's new and update-progress windows andWindowService's message dialog. Not tracked, so at 100 %: the donut (it has its own size), the toast and the corner notices (update, engine update, feedback, format request, elevation), which are placed in a screen corner like system notifications, and the crop overlay, whose wheel zooms the crop. Drop-down lists, context menus and tooltips open in their own popup windows outside the transform and stay at 100 % too. The size isUiScalein settings.json (1 when missing, so no schema step); it is written byUserDataStore.SaveUiScale0.6 s after the last change (and on exit), without raisingSettingsChanged.
After an update, Filee shows the new version's features once as slides (an animation or picture, a title, a line or two). The pieces:
Assets/WhatsNew/<version>/slides.jsonlists the slides:id,media(a file next to it), and the i18n keys oftitleandtext(whats_new.<version>.<id>.title, in every language file).SlideDeck(Controls) reads every manifest from the assembly's resources; a broken one is skipped.AnimatedImageshows GIF, animated WebP or a still picture. Avalonia'sImageshows only the first frame, soAnimatedImageDecoderdecodes frames with Skia'sSKCodec, which also says which earlier frame each one is drawn over (RequiredFrame, disposal included). The file is copied into anSKDataand each frame is decoded straight into aWriteableBitmap's framebuffer: no managed buffer is ever handed to native code.FrameTimelinekeeps the frame durations (0–10 ms counts as 100 ms, as in browsers) and the loop count. The animation pauses while the control is hidden or its window is minimized, shows the first frame with reduced motion (AutoPlay), and gives the decoder and bitmap back when it leaves the visual tree or its source changes.SlideShowis the deck itself (picture, texts, position dots), built in code soFilee.Setupcan link the same files for a slideshow on its progress page (AutoAdvance). All slides' texts sit in one grid cell, only the current one visible, so the height is the tallest slide's and the window doesn't jump.WhatsNewWindowadds the card, ✕, Previous / Next ("Get started" on the last slide), ←/→ and Esc.WhatsNewServicedecides at start-up, before the first run is marked complete:- a fresh install (first run not completed) needs no slides, as the first-run setup introduces Filee; its version is stored as seen;
- otherwise
AppSettings.WhatsNewSeenVersion(null in settings older than the slides, which means "updated") is compared with the running version, and the newest deck after it, up to this version, is shown; - the same version or a downgrade shows nothing and never lowers what was seen; an update without a deck is
stored as seen quietly.
The version is stored when the window opens. A start in the background (
--backgroundat sign-in, or the Explorer menu) keeps the deck untilWindowService.ShowMainraisesMainShown; the installer starts an updated Filee with--background --whats-new, which shows it at once. About → "What's new in Filee X" opens the newest deck again.
The <!-- update --> lines of CHANGELOG.md are the other half: the update card shows them for a version that isn't
installed yet; the slides introduce the installed one. The demo animations are rendered by
tests/Filee.App.Tests/WhatsNewMedia.cs from the real windows (see CONTRIBUTING.md, "Releasing").
Filee.Cli(filee-cli.exe, run asfileethroughcli\filee.cmd, which Setup can put on PATH) composes the same pipeline as the app without any UI (CliHost:UserDataStore,EngineRegistry,ConverterCatalog,JobQueue) and reads the app's presets and engine order. It is published into the app folder, so both share one runtime and the bundled engines; its name can't befilee.exenext toFilee.exeon a case-insensitive file system.Filee.Core/Watching/FolderWatcherwatches oneWatchRulewith aFileSystemWatcherand converts a file once it has kept its size and time for a moment and opens without sharing (downloads and copies in progress are skipped). Temporary files, unknown formats and dedicated output and originals folders are ignored. A rule may also write into the watched folder itself. In that case, generated file versions (path, size and modification time) and generated result directories are recorded in.filee-watch-<rule hash>.jsoninside the watched root. The hidden state file prevents those results from becoming new inputs, including after an inbox watcher restarts; a changed file with an old output name is accepted again. A failure to save this state is logged because results may be retried after restart. Removing a rule's state file also forgets its results. The tray app runs one per rule (WatchFolderService, Settings → Watch folders, conversions in the toast and history);filee watchruns one on its own, with a stableclirule identity across command restarts.
Everything OS-specific goes through IPlatformServices (Filee.Core/Platform). Windows lives in
Filee.Platform.Windows; macOS will get its own project. Global input uses SharpHook on both.
Home → Recent conversions opens a result with OpenFile (the shell on Windows, open on macOS, xdg-open on Linux,
so a file is handed to its app and never started itself; only formats Filee knows are offered) and, where
SupportsOpenWith (Windows), the shell's "Open with" dialog (SHOpenWithDialog, on a thread of its own because it
blocks until closed). The cards' pictures come from HistoryThumbnails: Skia decodes them off the UI thread, scaled
down while decoding where the format allows, from a copy read in one go so the file isn't held open, and a small cache
keeps them while the files are unchanged. For video files, FFmpeg extracts and scales a representative frame off the UI thread without locking the file.
On macOS, files reach Filee from file managers other than Finder too (issue #160). The "Convert with Filee" service
(MacOSServicesProvider) and the donut's drags (DraggedFiles) read file URLs first, then the older
NSFilenamesPboardType array of paths (PasteboardPaths); the service also takes plain-text lines that are absolute
paths. The donut registers those types with AppKit next to Avalonia's own, so such a drag reaches it, and a drag or
service request without readable files is logged with the types it offered. File promises (NSFilePromiseProvider)
can't be received through Avalonia; for a browser drag Filee reads the address and HTML it also offers, which the donut
registers too (see "Files dragged from a web browser"). A click-mode donut opened for a later launch or the service forgives one loss of
focus within a second of opening (ActivationGrace): Filee becoming the active app can take focus away from it.
The donut, the toast and the notices are transparent windows. On Windows Filee keeps Avalonia's rendering defaults
(ANGLE on Direct3D 11 composed through WinUI composition, then DirectComposition, then the window's redirection
surface; software rendering when Direct3D 11 fails): each of them keeps per-pixel transparency. The log says what was
picked (Rendering: AngleEgl with WinUIComposition), lists the graphics adapters, carries Avalonia's own errors from
starting the GPU paths (AvaloniaLogForwarder), and describes each kind of transparent window the first time it
shows (TransparentWindows). A system backdrop (Acrylic, Mica) or an accent blur on such a window turns its
transparent area into a blurred rectangle (issue #269). Filee sets neither, so one found there came from another
program: each time the window shows, a backdrop is set back to none and the accent (which can't be read back) is
turned off. For a report that still shows the wrong thing, the FILEE_RENDERING environment variable picks another
way to draw (WindowsRendering): dcomp (skip WinUI composition), redirection (Direct3D into the redirection
surface) or software (no GPU); unset or auto keeps the defaults. Filee reads it at start.
Settings → General → "Show “Convert with Filee” in the Explorer context menu" (AppSettings.ContextMenuEnabled) is
applied by WindowsPlatformServices.SetContextMenu at start-up and whenever settings change (installed builds only):
- Classic verb
HKCU\Software\Classes\*\shell\Filee(Filee.exe --convert "%1", one process per file; the running instance collects them throughSingleInstance) and a Send To shortcut. Per user, no admin rights. On Windows 10 this is the entry; on Windows 11 it sits under "Show more options". - Windows 11 top-level entry (optional): an
IExplorerCommandinFileeExplorerMenu.dll(src/Filee.ExplorerMenu, plain C, no C runtime) declared by the sparse packageFileeExplorerMenu.msix(ExplorerMenuPackage.CreateManifest:desktop4:FileExplorerContextMenus,desktop5:ItemType Type="*",com:SurrogateServer,uap10:AllowExternalContent,AppListEntry="none"). Both files ship next to Filee.exe and the package's external location is the install folder (C:\Program Files\Fileeby default).- The package is unsigned (publisher contains
OID.2.25.311729368913984317654407730594956997722=1; Filee has no code-signing certificate). Windows only registers an unsigned package with executable content with administrator rights (it installs it for all users); a per-user attempt fails with 0x80073D2B "an unsigned package cannot include Executable activations". Setup (src/Filee.Setup) has those rights anyway and registers it on Windows 11 x64 while its option is ticked. Later the General page offers "Add to the main menu", which runsAdd-AppxPackage -Path … -ExternalLocation … -AllowUnsignedin Windows PowerShell through one UAC prompt (ExplorerMenuRegistration). A signed package would register per user without a prompt. - The manifest version (
ExplorerMenuPackage.ManifestVersion) is independent of the app version, so app updates don't need a new prompt: the DLL is loaded from the install folder and updated with the app. Bump it only when the manifest changes; the page then offers to add the entry again ("Outdated"). - The DLL shows the entry only for files (not folders or items inside ZIP folders) and only while
%APPDATA%\Filee\explorer-menu.txtexists. The app writes the title in the UI language there while the setting is on and deletes it when the setting is off, so switching the setting takes effect without re-registering. While the package is registered the classic verb is removed (Windows lists packaged commands under "Show more options" too). - Invoke starts
Filee.exe --convert "<path>"…next to the DLL and returns at once. Selections longer than a command line go through%TEMP%\Filee-convert-*.txt(--convert-list, one path per line), which the app deletes. - Updates: Setup asks the running Filee to exit (
Filee.exe --quit), then renames aFileeExplorerMenu.dllthat Explorer still has loaded into its backup folder (Windows allows renaming a loaded DLL, not replacing or deleting it) and has Windows delete the old copy at the next restart, so updating needs no restart. An update keeps the registration as it was and refreshes it. Uninstall runsFilee.exe --uninstall-cleanup(UninstallCleanup): it removes the package, the title file, the classic verb, the startup entry and downloaded engines; the uninstaller then asks whether to delete the settings too (they stay by default). - Build:
pwsh build/build-explorer-menu.ps1downloads a pinned Zig (SHA-256 checked) intobuild/.cache, compiles the DLL and packs the MSIX withbuild/tools/make-explorer-package.cs(Windows' own packaging API, no SDK). Output:build/.cache/explorer-menu; CI builds it for the tests,build/build-installer.ps1builds it into the publish folder.
- The package is unsigned (publisher contains
Debugging: Get-AppxPackage Filee.ExplorerMenu shows the registration (InstallLocation is the external location);
Get-AppPackageLog -ActivityID <id> explains a failed deployment. To try a local build, copy
FileeExplorerMenu.dll + .msix next to an installed Filee.exe and use the button, or run elevated:
Add-AppxPackage -Path FileeExplorerMenu.msix -ExternalLocation <folder with Filee.exe> -AllowUnsigned. Explorer picks
the entry up for new menus; if not, restart Explorer. To remove it by hand:
Get-AppxPackage Filee.ExplorerMenu | Remove-AppxPackage.
Filee-<version>-win-Setup.exe is Filee's own installer, src/Filee.Setup (it replaced the Inno Setup script of
1.x). It is an Avalonia program with the app's look and translations: it links Styles/Soft.axaml (which therefore
styles only Avalonia's own controls; Filee's controls are in Controls.axaml), SoftPalette (accent, radii, font
order), AppLanguages and Assets/i18n/*.json (Setup's texts are the installer.* keys). To stay small it uses
Windows' own UI fonts (Segoe UI, Nirmala UI and the UI language's CJK font first; titles bold, as those CJK fonts
have no semibold) instead of the app's embedded Noto fonts.
-
Look: one borderless 640×420 card (
SetupWindow, its own window buttons, drag to move) whose states crossfade (Views/StateViews.axaml): Welcome (the logo assembling itself,DonutLogo; one Install button; an Options sheet sliding up with the language, folder, options and engines), Progress (the new version's "What's new" slides, a thinShimmerBar), Done (CheckBurst, "Launch Filee"), Failed ("Copy details") and the uninstaller's states.SoftBackdropdraws slowly drifting accent blobs behind it. The slides are the app's (SlideShow,AnimatedImage,SlideDeckandAssets/WhatsNew, linked): the newest deck up to the version being installed, advancing every 6 s. Every animation runs only while the window is visible and not minimized (FrameLoop) and is a still picture when Windows' "Show animations" is off (SetupMotion)./SILENTshows only the progress card, without slides. After an update Setup starts Filee with--background --whats-new, so the slides show once in the app as well. -
Build (
build/build-installer.ps1): Setup is published as one self-contained, trimmed file (-p:FileeSetupPublish=true), so it runs on Windows 10 1809+ x64 without .NET.build/tools/make-setup.csappends the app's publish folder as the payload:[program][LZMA streams of all files][manifest JSON][72-byte trailer](SetupPayload): ~48 MB segments compressed in parallel with SharpCompress's LZMA (48 MB dictionary), programs first through the x86 branch filter (X86Filter, from the LZMA SDK) as 7-Zip and Inno Setup do. The manifest lists every file with its size, SHA-256 and filter, the SHA-256 of the compressed data, the version and the engines to offer (fromEngineDownloads). Setup.exe's signature goes after the trailer (Setup looks for the trailer in front of the certificate table; the manifest is padded so the file ends on the 8-byte boundary signtool needs), so Setup.exe is signed after packing. The Setup program is signed before: its signature stays at the end of the program, Setup.exe's header stops pointing to it, and the manifest'sprogramSignaturelets the uninstaller copied out of Setup.exe (CopyProgramTo) point to it again, souninstall.exeis signed too. The code is trim- and AOT-safe (compiled bindings, source-generated P/Invoke and COM); Native AOT would need the MSVC linker on the build machine and still couldn't link SkiaSharp's native libraries in. -
Rights: Setup starts unelevated (asInvoker) and, for an install for all users, starts itself again with administrator rights (one UAC prompt) and waits. The elevated copy writes what to start at the end into a file (
/LAUNCHFILE), and the unelevated one starts Filee from it as the signed-in user. Elevated without such a parent (run as administrator, a package manager), it starts Filee with Explorer's token. -
Install (
Installer): checks the free space and the payload's hash first, closes Filee (--quit, then ends what still runs from the folder), unpacks into<folder>\~setup-*\newchecking every file, checks that every program finds the Microsoft C++ runtime it imports (next to it or in System32;RuntimeCheck), then swaps the files in (FileSwap: replaced and obsolete files are renamed into a backup, which works for files in use) and writes the uninstall entry, the shortcuts and PATH. Any failure until then puts everything back. Filee's own settings (Explorer menu, start at sign-in, engines, a picked language) are handed to Filee on its command line, as before. -
Updates from 1.x: the uninstall key stays
{C8686E06-8327-40A2-B0F7-3DEDD61D9248}_is1(Inno Setup's AppId), so Apps & Features keeps one Filee.PreviousInstallreads the folder and choices of an Inno Setup installation; an update keeps the folder and what is registered now (desktop shortcut, PATH, the top-level menu package), removesunins000.*and every file 2.0 doesn't have, and rewrites the key without theInno Setup: …values. The app counts as installed whenuninstall.exe(or 1.x'sunins000.exe) is next to it (UpdateService.IsInstalled). -
Uninstall:
uninstall.exeis Setup without its payload. Run from the install folder it copies itself to%TEMP%and waits for the copy, which removes whatuninstall.jsonlists (other files in the folder stay), the shortcuts, the PATH entry and the key. Files in use are moved out and deleted at the next restart. A program without a payload is the uninstaller only asuninstall.exe, asFilee.Setup.exe(a development build) or with/UNINSTALL(SetupStart); any other name is a Setup.exe whose download didn't finish, and it says so instead (exit code 1). -
Command line: Inno Setup's switches (
SetupOptions, documented at the top of the file), because Filee 1.x updates itself with/SILENT /NORESTARTand winget passes/VERYSILENT /SUPPRESSMSGBOXES /NORESTART. Exit codes as Inno Setup's: 0 done, 1 could not start, 2 cancelled before installing (also a declined UAC prompt), 4 failed. -
Testing:
Filee.Setup.Testsruns installs, updates (also over a simulated 1.x installation) and uninstalls againstMemoryRegistry, a fakeISetupSystemand scratch folders, and renders every page./SANDBOX=<folder>runs the real Setup with a registry file and all folders inside<folder>;build-installer.ps1 -CheckOnly(CI) installs, updates and uninstalls that way.
On Linux, Filee is distributed as a self-contained archive (Filee-<version>-<rid>.tar.gz for linux-x64 and
linux-arm64) containing the application, filee-cli, bundled conversion engines, application icon, .desktop launcher,
and standalone shell scripts (install.sh, uninstall.sh).
- Installation:
install.shinstalls for the current user into~/.local/share/filee/applicationwithout root, creating a launcher symlink at~/.local/bin/fileeand desktop registration at~/.local/share/applications/com.filee.app.desktop. A.filee-packagemarker containingcom.filee.appidentifies the directory as an authentic managed installation. Running without--upgraderefuses to overwrite an existing installation. - In-app updates (
UpdateService): Checks GitHub releases for matching<rid>.tar.gzand<rid>-SHA256SUMS.txtassets. The tarball is verified against SHA-256 before extraction. Archive extraction validates that all entries stay within the target directory, rejecting absolute paths, directory symlinks, and escaping links, and masks permissions to standard rwx bits (excluding setuid/setgid/sticky bits). - Transactional upgrade:
UpdateAsyncstages the payload in a private directory (~/.local/share/filee/.update-*), spawns a detached/bin/shhelper, and requests application shutdown (App.Quit). Once the old process exits, the helper executesinstall.sh --upgrade(which safely moves the existing installation to a backup directory, validates the new files, and rolls back on failure or signal interruption), and restarts Filee with--background --whats-new. Output and rollback diagnostics are appended to Filee's daily log (~/.config/Filee/logs/). Downloaded conversion engines (~/.local/share/Filee/engines) and user settings (~/.config/Filee) remain untouched. - Uninstallation:
uninstall.shremoves the application directory, desktop launcher, and~/.local/bin/fileesymlink after verifying the.filee-packagemarker. User settings and converted files are kept.