The layer cake
Inkshelf's packages are stacked in layers, from the device at the bottom to the programs at the top, and a package may only import from its own layer or the ones below it.
The journey of a book crossed many parts of the code. This chapter shows how those parts are arranged, and the one rule that keeps them from becoming a tangle.
A layer cake: each layer rests on the ones below and never on the ones above. You can lift off the top layers and the bottom still stands; you cannot pull out a bottom layer without everything above noticing.
Domains, bottom to top
All the code lives in one Go module. Apart from the two programs in cmd, everything is under
internal/, in folders named after a domain: what you need to know to work there. Fonts are
in graphics/fonts, the CSS cascade in web/css, line breaking in typesetting/layout.
From the bottom:
- device: the reader itself, without its SDK: the screen's memory, e-ink refreshes, the firmware's messages, taps and keys, Wi-Fi. Also the emulator that stands in for the reader on a computer.
- graphics: pixels. Fonts and text drawn in gray; page images prepared into
.pg4files. - web: the web formats books are written in: CSS, SVG, MathML, web fonts, images, links.
- typesetting: text on pages: the document model, line breaking, hyphenation, pagination.
- ebook: book files: an EPUB's zip and its chapters, fixed-layout pages, a CBZ's pages.
- servers: what the app needs from a server, and one package per kind of server.
- shelf: the books downloaded on the reader.
- progress: keeping the reader's progress and the server's in agreement.
- reading: the engine for reading an EPUB: positions, page turns across chapters, links.
- app: the user interface: the state, widgets, services and screens.
- cmd: the two programs:
cmd/inkshelffor the reader,cmd/emulatorfor your computer.
On the side stand two small utilities that import nothing of Inkshelf's, so any layer may use
them: naturalorder ("Tome 2" before "Tome 10") and timing, which times work for the log and
for profiles.
The rules
One rule does most of the work: a package imports only from its own domain or from domains below it. Three more keep the app independent of any one server and of itself:
- The app and the shelf never import a specific server.
appandshelf(and everything they import) talk to theservers.Serverinterface, never toservers/komga,servers/mealieorservers/calibreweb. Only the programs incmdimportservers/registry, the one package that knows every kind and builds the right one fromconfig.json. - The reader side knows nothing of servers. device, graphics, web, typesetting and ebook would work the same for a book from a USB cable.
- Screens never import each other. A screen moves to another by its id, with
state.ShowLocked, and leaves to the services whatever another screen must see changed.app/screens/listis not a screen: it draws the header, rows and footer that the list screens share, and they may import it.
And Go adds one of its own: no import cycles. If typesetting/layout imports
typesetting/document, the document can never import the layout back. The compiler refuses.
The first rule is checked by a command in the project's definition of done. It must print nothing:
go list -deps ./internal/app/... ./internal/shelf | grep -E 'servers/(komga|mealie|calibreweb)'
Why layers
- You can learn bottom-up. Each layer only uses ideas from below it. That is why this site starts with the screen, then books, then the app: you never need a part you have not seen.
- The reader side stays pure. Laying out an EPUB has nothing to do with Komga's API. Keeping
them apart means the typesetting code can be tested with a file on disk, and a new kind of server
touches only
servers/andservers/registry. - The bottom can be swapped. All the PocketBook-specific code is in
device/pocketbook. A Kobo or Kindle port, which the author has in mind, would add a sibling device package, and the layers above would barely change. That is also why names outside those packages avoid the words "PocketBook", "Komga" or "Mealie". - Changes stay local. A change to a lower layer can only affect the layers above it; a change to a screen affects no other screen.
Going deeper: the imports between domains today
From go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..., by domain (the widget's Imports
today line shows the same):
| Domain | Imports |
|---|---|
| naturalorder, timing | nothing of Inkshelf's |
| device, graphics | timing |
| web | graphics |
| typesetting | graphics, web |
| ebook | graphics, web, typesetting, naturalorder, timing |
| servers | naturalorder, timing |
| shelf | graphics, web, typesetting, ebook, servers, naturalorder, timing |
| progress | servers, shelf |
| reading | graphics, web, typesetting, ebook, shelf, timing |
| app | device, graphics, typesetting, ebook, servers, shelf, progress, reading, timing |
| cmd | app, device, graphics, servers, timing |
Notice that device is used only by app and cmd: nothing that lays out a book knows it runs on
a PocketBook.
Inside the app there is a smaller cake: widgets (what every screen draws with), then state
(everything the app knows, and its lock), then the services (session, catalog,
downloadqueue, covers, openbook: functions over the state), then the screens, then app
itself, which hands input to the screen shown.
Where it lives in the code
internal/servers/server.go · ServerThe interface every kind of server implements; all the app knows of servers.internal/servers/registry/registry.go · Opener, KindOfThe switch on config.json’s type; imported by cmd only.internal/app/doc.goHow the app is split into state, widgets, services and screens.internal/app/state/state.go · ShowLockedHow a screen moves to another without importing it.cmd/inkshelfThe program for the reader: the top of the cake, where the layers are plugged together.
The full story: Architecture.
Next: the bottom of the cake, the screen. It starts with what makes e-ink so different from the screen you are reading this on: How e-ink works.