Skip to content

Documents

Andrea de Ruvo edited this page Sep 4, 2026 · 21 revisions

Documents

Everything opens where it is, rather than being downloaded first.

  • Markdown rendered, with a toggle to see the source — and the figures next to it on disk are shown: ![](results/plot.png) resolves against the document's own folder and is fetched like any other file, so the jail still decides what can be read.

  • ```mermaid diagrams drawn, not left as three lines of arrows in a grey box — see below.

  • PDF drawn by Argus itself, so it opens where you left it and page width means page width — see below. A finder folds into a button over the page: the browser's Ctrl+F searches the page around a document, and a phone has no Ctrl+F at all.

  • Video and audio, played in the window rather than downloaded. The server streams the file and answers a range request, so the scrubber works from the first second instead of the recording having to arrive whole before anything can be seen, and the preview cap does not apply — a recording is bigger than any cap worth having.

    It keeps your place: where you had got to is remembered per file and survives a reload, which is the moment it is wanted, since a window watching a recording that is still being written reloads on its own. The last forty files, oldest dropped.

    What plays is what the browser can decode: mp4 and webm everywhere, mkv and mov usually not. A container it cannot decode says so and offers the download, rather than showing a black rectangle and leaving you to guess.

  • Word, ODT, RTF through pandoc when the machine has it — headings, lists, tables, links, and figures inlined — with the plain-text extraction as the fallback when it does not.

  • Excel — .xlsx and .xlsm — as a real HTML table, one per sheet, read directly rather than through pandoc — see below.

  • Images inline. Logs and any text file too large to send whole arrive as their tail, which is the part anyone wants, and the viewer says so and starts at the end.

Every rendered document is served under a CSP sandbox, in an opaque origin: a report written by somebody else must not be able to read the access token. Markdown is escaped before rendering and javascript: links are stripped.

Saving from the viewer keeps the real name. Files served to be read rather than saved carry Content-Disposition: inline with their filename, or the browser's own save button names every download after the URL — which is how everything used to arrive as file.pdf.

Where the file is

Behind the i — the same one a terminal wears, meaning the same thing — a strip says which folder the document is in and when it last changed, to the minute. The listing says 18:04 for today and 25 Aug for anything older, which is right in a column of forty files and wrong here: a document being rewritten while you read it is one whose time you want, and 25 Aug does not tell you whether the run that was meant to update it has finished. Its size is in the tooltip, which is the other half of that question. Hidden until you ask, because most of the time you already know, and it is one answer for every document rather than a state per window. Where a document has controls of its own, the strip goes under them: a PDF's page and zoom buttons stay the first row. The path opens Files there, one button puts a file browser on that folder in the desk you are on, and two copy: one a link to the file — its http address, with the token, the thing you paste into a browser or a message — and one its absolute path, the thing you paste into a terminal. Both wear the tick every other copy in this app gives you.

Two buttons because they are two different answers. For a PDF especially, what is useful to hand somebody is a link that opens the document, not a filename they cannot reach.

It is there because the header says the file's name and nothing else, which is right until the moment you want to see what is beside it or paste the path into a terminal — and both of those were three moves away.

Diagrams on their own

A .mmd or .mermaid file — what the mermaid command line reads and writes — is drawn rather than shown as a wall of arrows, which is the one case where a reader is worse off than running cat. It is the same engine that draws fenced diagrams inside a document, so it costs nothing until the first one is opened.

The colours are a choice, in Settings. auto follows the app — which is what any other mermaid tool does, and which on a dark page means mermaid's dark theme, greys. The colourful one everybody recognises from mermaid.live is default, offered here as colourful, and it can be chosen against a dark app because wanting a colourful diagram on a dark page is not a contradiction. forest is greens, flat is the old behaviour: the app's own two greys and one green, restrained inside a document and dull on a diagram you are working on.

Whichever is chosen, a diagram's own classDef and style lines win. The theme decides what an unstyled node looks like, never what a styled one does.

<> switches to the source, as it does for markdown. And a diagram that will not parse keeps its source and says what mermaid objected to, with the line number: half-written is the normal state of a diagram you are working on, and an empty box would hide the very thing you need to fix.

A 3D mesh

An .stl file is drawn as the shape it describes, orbitable with a drag — the file itself is just a list of triangles, and a wall of facet normal … vertex … is the same "worse off than cat" case a bare mermaid diagram used to be. Binary or ASCII, without being told which, because the format itself does not say either.

Faceted on purpose. Each triangle's normal is computed from its own geometry rather than trusted from the file, which is both the conventional look for a printed or milled part — edges, not a smooth blend across them — and the safer choice: a bad or absent normal in the file is a real thing that happens, and a computed one cannot be wrong in the same way.

Wears this app's palette, the same bargain a mermaid diagram already made: the model's colour and the space around it are the accent and the background, and both follow a theme switch rather than staying whatever they were drawn in.

Loaded only when a document of this kind is actually opened — three.js is by far the largest thing vendored for a viewer here, and the overwhelming majority of documents are not meshes.

A STEP file

.step and .stp open the same way — orbitable, faceted, in this app's own colours — but they are a different kind of file underneath, and the difference is worth knowing.

An STL is triangles; a STEP file is a boundary representation: curved surfaces described algebraically, the way the CAD program that wrote it actually thinks about the part, plus — often — several parts in one file, each placed where they belong in the assembly. Turning that into triangles for a screen is a CAD kernel's job, and this uses a real one: OCCT, the engine behind FreeCAD and much else, compiled to WebAssembly by occt-import-js and loaded only when a .step is actually opened — 7.5 MB, by far the largest thing vendored here, next to which three.js itself is small.

Several parts stay assembled. Centring a multi-part file the way a single STL is centred — each part pulled to the origin on its own — would scatter the assembly into a pile of disconnected shapes sitting on top of each other. Every part in the file is offset by the same amount instead, computed from the bounding box of the whole assembly, so a bolt through two plates is still a bolt through two plates.

Colour is this app's, not the file's. A STEP file can carry a colour per part, sometimes per face — the same choice binary STL's extended colour byte makes, and the same answer: ignored, in favour of the one palette used everywhere else in here. Shape is what the file is asked for; colour is asked of the theme.

occt-import-js and OCCT itself are LGPL-2.1, not MIT like everything else vendored here — noted in static/vendor/occt-import-js-*/NOTICE.md, along with why loading it as a plain fetched file rather than bundling it into app.js already satisfies what that licence asks for.

A spreadsheet

.xlsx and .xlsm render as a real <table>, one per sheet, in the workbook's own tab order — not through pandoc, even though pandoc lists xlsx as a reader it can open.

It was tried first, and it fails on real files. A worksheet's location is recorded in workbook.xml.rels as a Target, and that target is legally written two ways: relative to the package, or — what real Excel actually writes — package-absolute, starting with a /. Every .xlsx this was tested against, including several already sitting in this machine's own home directory, uses the absolute form, and pandoc's reader resolves it wrong: /xl/worksheets/sheet1.xml comes back as xl//xl/worksheets/sheet1.xml, an entry that does not exist in the archive, and the whole file fails to open. A grid has no real use for pandoc's document model regardless — a table is what a sheet already is — so this reads the same zipped XML the no-pandoc .docx fallback already reads: workbook.xml for the sheet names and order, .rels for where each one actually lives (both target forms), sharedStrings.xml for the text most cells reference rather than repeat.

No dependency, the same trade the text extractor already makes. Nothing is vendored for this, on the server or in the browser — the same "standard library, formatting lost" bargain a .docx without pandoc already accepts, kept as a grid instead of flattened to lines, because a table stripped of its columns is not a smaller table, it is a different and much less useful thing. Numbers and dates come out as whatever was actually stored — a date is a serial number in a cell with no calendar attached to it without also reading the style table, and that is not done here — the same kind of loss .docx already accepts for the sake of needing nothing else installed.

Older .xls — the pre-2007 binary format, not a zip at all — is not read. It refuses the same way any other unreadable binary file does: a plain error, and the download still works.

A document that is being rewritten

In a window — not on the full screen, where there is nothing to follow with — a document wears two buttons. The circular arrow reads it again now, whatever the disk says. The eye beside it is the switch: while it is on, the file is checked every three seconds and reloaded when it changes, which is what you want in front of a report a job is regenerating.

A PDF reloads with the rest, and comes back to the page, the scroll and the zoom you were on. It used to offer instead — this file has changed, [Reload] — on the reasoning that a document rebuilt while you read page thirty must not throw you to page one. That stopped being true when this viewer learned to remember the place, and what was left was a notice standing between you and the new version you had already asked to see.

A reload that lands mid-write draws a broken document for one tick and then fixes itself: a file still being written keeps changing, so the next check reloads it again, and the last reload is the finished one.

Code, coloured

A file of code opens coloured — 27 languages, chosen from the name — and the highlighter is loaded the first time you open one, never before: somebody who reads logs and markdown all day pays nothing for it, and the app's first paint is unchanged. It is vendored like everything else here, so nothing reaches a CDN.

Two limits, both deliberate. Above 260KB of text the file stays plain: highlighting it takes longer than reading it does, and a frozen tab is worse than grey code. And the colours are this app's, not a theme downloaded with the library — a borrowed theme brings its own background and its own greens, and two palettes in one window is a collage.

Diagrams in a document

A fenced block whose language is mermaid is a diagram everywhere else it is read — GitHub, GitLab, the editor it was written in — and here it used to be its own source in a grey box, which is the one place a reader was worse off than running cat on the file. Now it is drawn: flowcharts, sequences, state, class, ER, gantt, pie, git graphs, and the rest of what mermaid 11 knows.

Three things about how:

  • It wears this app's palette, not mermaid's own dark and light themes: the same two greens, the same greys, the same lines as the document around it. And it follows a theme switch — the colours are written into the drawing rather than read from a variable, so changing theme redraws every diagram on screen rather than leaving a black box on a white page.
  • Loaded only when a document actually has one. It is by far the largest thing vendored here, and the overwhelming majority of documents are not diagrams. Nothing reaches a CDN, as with everything else in static/vendor/.
  • A diagram that will not parse stays as its source, with one line under it saying what the parser objected to. Hiding it would lose the very text you are trying to fix, and mermaid's own red error box is not put on the page.

Labels go through mermaid's sanitiser (securityLevel: strict): no scripts, and no click handlers, in a document somebody else wrote.

Which viewer, by extension

The guess is: markdown rendered, code coloured, everything else plain. It is right nearly always, and wrong exactly where it matters to the person it is wrong for — a .log that is really JSON, a .txt that is a config, a .md you want to read as source because it is a template.

So it is only a default. Settings → Documents → Which viewer, by extension takes a row per extension you disagree about:

auto let Argus decide, which is the behaviour above
code colour it, whatever it is called
plain text, and nothing else
markdown render it

An empty row waits at the bottom: adding one is typing, not finding a button. The ✕ forgets a row and puts that extension back to auto.

Size caps and telling the server what a file really is

Which viewer, by extension above is a preference, kept per device, for the text viewers only. viewers in config.yaml is its server-side counterpart for everything else this wiki has described — PDF, mesh, spreadsheet, and the pandoc/zipped-XML documents — and it answers two different questions.

How big is too big, for this kind of file. max_preview_bytes is one number for everything by default, and that stopped being enough the day a real thesis PDF, a legitimate 10+ MB document, ran into the same cap meant to stop a phone from being handed a runaway image. viewers.max_bytes overrides it per kind:

viewers:
  max_bytes:
    pdf: 31457280        # 30 MiB — this lab's PDFs run bigger than most
    default: 2097152     # everything not named here, same as max_preview_bytes today

Kinds: pdf, mesh (.stl/.step/.stp, one cap for both), spreadsheet (.xlsx/.xlsm), document (Word/ODT/RTF/EPUB — pandoc or the no-pandoc fallback), image, and default for text and anything unrecognised. A kind with no entry falls back to max_bytes.default if that is set, and to max_preview_bytes if it is not — existing configs are unaffected by any of this until viewers is actually written.

What an extension actually is, when its name disagrees with its content — a report generated with the wrong extension, a spreadsheet export nobody thought to rename:

viewers:
  force:
    dat: pdf              # every .dat here is really a PDF
    rpt: spreadsheet       # every .rpt here is really an .xlsx

pdf, stl, step, spreadsheet, or text — the last one for the opposite mistake, an extension that would otherwise be sent through the document or spreadsheet renderer but is actually plain text. It is not a promise to render binary data as text regardless: a file that is genuinely binary is still refused, the same 415 an unrecognised binary file always gets. Forcing only stops the guess from a name being wrong, not the safety check on what is actually in the file.

Neither half applies to a file opened with Ctrl/Cmd+click — see Sessions and the terminal — which already means the real file, no interpretation, cap or otherwise.

PDFs, and keeping your place

Argus draws PDFs itself, with pdf.js, rather than handing them to the browser's viewer. That is not a preference; it is the only way to answer the questions people actually ask of a document they are reading.

The browser's viewer will not talk to the page around it. Measured: it answers documentLoaded and getSelectedText, and says nothing at all while you scroll — so where you had got to was not something that could be recorded, let alone put back. It also honours view=Fit while silently dropping view=FitH, which is why page width never survived a reload, for months, and had to be set again every single time.

So, now:

  • It opens where you left it. The page, the scroll position to the pixel, and the zoom you chose. Across a reload, across F5, across closing the tab and coming back tomorrow — it is kept in the preferences, for the last sixty documents.
  • Page width means page width, because Argus computes it rather than asking for it. Whole page and as-it-comes are the other two; the button in the bar cycles them, and Settings says which a document you have never opened starts at.
  • The page number is a box you type in, with a step either side of it: put 31 in, press Enter, and you are on page 31. The steps grey out at the ends, and the box does not rewrite itself under your fingers while you are typing in it.
  • − and + for a zoom of your own, which then stays yours: resizing the window re-fits only while a fit is what you asked for.
  • Out to a browser tab, for the things the browser does and this does not: printing, its own find, handing the file to something else. The fit you are on goes with it.
  • The text is selectable, laid invisibly over the drawing, so a citation can be copied out of a paper.
  • Only what is on screen is drawn, a few pages either side. A 53-page paper at page width would be some 200 MB of canvas if every page were kept, which a phone has not got.

The finder sits in the bar, and its panel hangs under it: Argus extracts the text on the server (pdftotext), lists every hit with its page and context, and scrolls to the page you pick rather than reloading the document to get there.

Leaving the desk and coming back keeps your place too. A desk that is not on screen is display: none, so everything inside it measures zero — which is how the reader used to be thrown to the end of the document every single time you looked at something else.

When the file is rebuilt while you are reading it — latexmk, a report an agent regenerates — the viewer still does not reload under you. It offers:

This file has changed.   [ Reload ]   ✕

The difference is that reloading is no longer a punishment: it puts you back where you were reading.

If pdf.js cannot open a document at all, the browser's own viewer is used for it instead, with a line saying so. Nobody is left looking at nothing.

Clone this wiki locally