Class ThemeFormat

java.lang.Object
limn.components.ThemeFormat

public final class ThemeFormat extends Object
A palette as text, and back, so an application can ship a theme its designer built as a file beside the jar instead of as a recompile.

 # Limn theme
 name = Ocean
 dark = true
 background = #0B1A24
 surface = #11242F
 primary = #4FD1C5
 

Here rather than in the editor module, so that an application can load a palette its designer built without the screen that built it anywhere in the build. That split is the whole point of the format: what crosses between authoring and wearing a theme is a value, and this is how it is written down.

parse(java.lang.String) and write(limn.components.Theme) are the pure pair: text in, text out, no disk, no thread rules. load(Path) and load(InputStream) are the two lines above them that an application actually calls, and they are here because the resource case is not one line: a theme shipped inside a jar otherwise costs a try-with-resources, a null check, readAllBytes and a charset, every time, in every application. There is no save counterpart on purpose: writing is Files.writeString(path, write(theme)) and nothing is hidden in it, and nobody writes back into a jar.

No asynchronous form, and none is owed. A palette is a configuration file of about a kilobyte, read once, normally before there is a window to keep responsive. An application loading one while a window is up (from a document, or a file the user just chose) should still go through Ui.work, which is what ThemeEditorFiles does; nothing here stops it, since load(java.nio.file.Path) is an ordinary blocking call.

What may be left out. name and dark are required: the mode decides which built-in the missing tones are taken from, so a file without it would mean two different palettes. Every colour is optional and falls back to the built-in Theme.light() or Theme.dark(), which is what makes a four-line palette a usable one, and so is cornerScale, which falls back to the shipped ramp.

What may not. A key that is not name, dark, cornerScale or a Theme.Token.key() is an error rather than a shrug, and so is a repeated key: a palette that silently ignored primaryHovor would be debugged by eye, in a running application, against a tone that never moved.

  • Field Details

    • EXTENSION

      public static final String EXTENSION
      The conventional file extension, without the dot, for a file dialog's filter and for a resource name. Nothing enforces it; the parser reads whatever it is given.
      See Also:
  • Method Details

    • load

      public static Theme load(Path file) throws IOException
      Reads a palette from a file.
      
       Theme.setCurrent(ThemeFormat.load(Path.of("themes/ocean." + ThemeFormat.EXTENSION)));
       

      Blocking, and deliberately so; see the class documentation for when that is fine and when to wrap it in Ui.work.

      Throws:
      IOException - if the file cannot be read
      IllegalArgumentException - if what it holds is not a palette, naming the line
    • load

      public static Theme load(InputStream in) throws IOException
      Reads a palette from a stream, the form a theme shipped inside a jar arrives in.
      
       try (InputStream in = App.class.getResourceAsStream("/themes/ocean.limntheme")) {
           Theme.setCurrent(ThemeFormat.load(in));
       }
       

      The stream is read to the end and left open, because it was opened by the caller: a method that closed a stream it did not open would break the try-with-resources above by making the close double.

      Throws:
      IOException - if the stream cannot be read
      IllegalArgumentException - if what it holds is not a palette, naming the line
    • write

      public static String write(Theme theme)
      The palette as text: a comment header, name, dark, then every tone in Theme.Token order, one per line.

      Deterministic and newline-terminated with \n on every platform: two writes of equal palettes produce equal strings, so a saved file can be diffed and a round trip can be asserted.

      Throws:
      IllegalArgumentException - if the name or the typeface family contains a line break or another control character. The format is one key per line and a value cannot span or end one: written as-is, such a name came back as a different name or as an extra key, silently, on the next load
    • parse

      public static Theme parse(String text)
      Reads a palette back. Blank lines are ignored, and so is any line whose first non-blank character is #; every other line is key = value.

      A comment is a whole line and never the tail of one: a colour value begins with #, and there is no spelling of "comment" that could also let that through.

      Throws:
      IllegalArgumentException - on any malformed, unknown, repeated or missing key, naming the line it was on; the message is meant to be shown to whoever wrote the file