Dave Jarvis' Repositories

A container/.gitignore
1
token.txt
12
M container/Containerfile
4444
4545
# Archives
46
ADD "https://www.omnibus-type.com/wp-content/uploads/Archivo-Narrow.zip" "archivo-narrow.zip"
4647
ADD "https://fonts.google.com/download?family=Courier%20Prime" "courier-prime.zip"
4748
ADD "https://fonts.google.com/download?family=Inconsolata" "inconsolata.zip"
4849
ADD "https://fonts.google.com/download?family=Libre%20Baskerville" "libre-baskerville.zip"
50
ADD "https://fonts.google.com/download?family=Niconne" "niconne.zip"
4951
ADD "https://fonts.google.com/download?family=Nunito" "nunito.zip"
5052
ADD "https://fonts.google.com/download?family=Roboto" "roboto.zip"
5153
ADD "https://fonts.google.com/download?family=Roboto%20Mono" "roboto-mono.zip"
5254
ADD "https://github.com/adobe-fonts/source-serif/releases/download/4.004R/source-serif-4.004.zip" "source-serif.zip"
53
ADD "https://www.omnibus-type.com/wp-content/uploads/Archivo-Narrow.zip" "archivo-narrow.zip"
5455
5556
# Typesetting software
...
7475
  echo "PS1='\\u@typesetter:\\w\\$ '" >> $PROFILE && \
7576
  unzip -d $CONTEXT_HOME $DOWNLOAD_DIR/context.zip && \
77
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "Archivo-Narrow/otf/*.otf" && \
7678
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/courier-prime.zip "*.ttf" && \
7779
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/libre-baskerville.zip "*.ttf" && \
7880
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/inconsolata.zip "**/Inconsolata/*.ttf" && \
81
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/niconne.zip "*.ttf" && \
7982
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/nunito.zip "static/*.ttf" && \
8083
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto.zip "*.ttf" && \
8184
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto-mono.zip "static/*.ttf" && \
8285
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/source-serif.zip "source-serif-4.004/OTF/SourceSerif4-*.otf" && \
83
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "Archivo-Narrow/otf/*.otf" && \
8486
  mv $DOWNLOAD_DIR/*tf $FONTS_DIR && \
8587
  fc-cache -f -v && \
M container/manage.sh
137137
# ---------------------------------------------------------------------------
138138
get_mountpoint() {
139
  $log "Mounting ${1} as ${2}"
140
141139
  local result=""
142140
  local binding="ro"
...
179177
  declare -r mount_images=$(get_mountpoint_images)
180178
  declare -r mount_fonts=$(get_mountpoint_fonts)
179
180
  $log "mount_source = '${mount_source}'"
181
  $log "mount_target = '${mount_target}'"
182
  $log "mount_images = '${mount_images}'"
183
  $log "mount_fonts = '${mount_fonts}'"
181184
182185
  ${CONTAINER_EXE} run \
M src/main/java/com/keenwrite/events/StatusEvent.java
22
package com.keenwrite.events;
33
4
import com.keenwrite.AppCommands;
5
64
import java.util.List;
75
...
1816
 */
1917
public final class StatusEvent implements AppEvent {
20
  /**
21
   * Reference a class in the top-level package that doesn't depend on any
22
   * JavaFX APIs.
23
   */
24
  private static final String PACKAGE_NAME = AppCommands.class.getPackageName();
25
2618
  private static final String ENGLISHIFY =
2719
    "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])";
...
4739
  }
4840
41
  /**
42
   * Constructs a new event that contains information about an unexpected issue.
43
   *
44
   * @param problem The issue encountered by the software, never {@code null}.
45
   */
4946
  public StatusEvent( final Throwable problem ) {
50
    this( "", problem );
47
    this( problem.getMessage(), problem );
5148
  }
5249
5350
  /**
5451
   * @param message The human-readable message text.
5552
   * @param problem May be {@code null} if no exception was thrown.
5653
   */
5754
  public StatusEvent( final String message, final Throwable problem ) {
58
    assert message != null;
59
    mMessage = message;
55
    mMessage = message == null ? "" : message;
6056
    mProblem = problem;
6157
  }
...
7571
      stream( trace.getStackTrace() )
7672
        .takeWhile( StatusEvent::filter )
77
        .limit( 10 )
73
        .limit( 15 )
7874
        .toList()
7975
        .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) );
...
10298
  private static boolean filter( final StackTraceElement e ) {
10399
    final var clazz = e.getClassName();
104
    return !(clazz.contains( PACKAGE_NAME ) ||
105
      clazz.contains( "org.renjin." ) ||
100
    return !(clazz.contains( "org.renjin." ) ||
106101
      clazz.contains( "sun." ) ||
107102
      clazz.contains( "flexmark." ) ||
108
      clazz.contains( "java." ));
103
      clazz.contains( "java." )
104
    );
109105
  }
110106
M src/main/java/com/keenwrite/io/CommandNotFoundException.java
22
package com.keenwrite.io;
33
4
import java.io.File;
54
import java.io.FileNotFoundException;
65
...
1716
  public CommandNotFoundException( final String command ) {
1817
    super( command );
19
  }
20
21
  /**
22
   * Creates a new exception indicating that the given command could not be
23
   * found (or executed).
24
   *
25
   * @param file The binary file's command name that could not be run.
26
   */
27
  public CommandNotFoundException( final File file ) {
28
    this( file.getAbsolutePath() );
2918
  }
3019
}
M src/main/java/com/keenwrite/io/SysFile.java
22
package com.keenwrite.io;
33
4
import org.jetbrains.annotations.NotNull;
5
46
import java.io.File;
57
import java.io.FileInputStream;
68
import java.io.IOException;
79
import java.nio.file.Path;
810
import java.security.MessageDigest;
911
import java.security.NoSuchAlgorithmException;
12
import java.util.ArrayList;
1013
import java.util.Optional;
1114
import java.util.function.Function;
12
import java.util.regex.Pattern;
15
import java.util.function.Predicate;
1316
17
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
18
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.io.WindowsRegistry.pathsWindows;
1420
import static com.keenwrite.util.DataTypeConverter.toHex;
1521
import static java.lang.System.getenv;
1622
import static java.nio.file.Files.isExecutable;
17
import static java.util.regex.Pattern.compile;
1823
import static java.util.regex.Pattern.quote;
1924
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
...
2934
  private static final String[] EXTENSIONS = new String[]
3035
    {"", ".exe", ".bat", ".cmd", ".msi", ".com"};
36
37
  private static final String WHERE_COMMAND =
38
    IS_OS_WINDOWS ? "where" : "which";
3139
3240
  /**
3341
   * Number of bytes to read at a time when computing this file's checksum.
3442
   */
3543
  private static final int BUFFER_SIZE = 16384;
36
37
  //@formatter:off
38
  private static final String SYS_KEY =
39
    "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
40
  private static final String USR_KEY =
41
    "HKEY_CURRENT_USER\\Environment";
42
  //@formatter:on
43
44
  /**
45
   * Regular expression pattern for matching %VARIABLE% names.
46
   */
47
  private static final String VAR_REGEX = "%.*?%";
48
  private static final Pattern VAR_PATTERN = compile( VAR_REGEX );
49
50
  private static final String REG_REGEX = "\\s*path\\s+REG_EXPAND_SZ\\s+(.*)";
51
  private static final Pattern REG_PATTERN = compile( REG_REGEX );
5244
5345
  /**
...
7365
7466
  /**
75
   * Answers whether the path returned from {@link #locate()} is an executable
76
   * that can be run using a {@link ProcessBuilder}.
67
   * Answers whether an executable can be found that can be run using a
68
   * {@link ProcessBuilder}.
69
   *
70
   * @return {@code true} if the executable is runnable.
7771
   */
7872
  public boolean canRun() {
...
9387
   * the fully qualified path, otherwise an empty result.
9488
   *
95
   * @param map The mapping function of registry variable names to values.
96
   * @return The fully qualified {@link Path} to the executable filename
97
   * provided at construction time.
89
   * @return Fully qualified path to the executable, if found.
9890
   */
99
  public Optional<Path> locate( final Function<String, String> map ) {
91
  public Optional<Path> locate() {
92
    final var dirList = new ArrayList<String>();
93
    final var paths = pathsSane();
94
    int began = 0;
95
    int ended;
96
97
    while( (ended = paths.indexOf( pathSeparatorChar, began )) != -1 ) {
98
      final var dir = paths.substring( began, ended );
99
      began = ended + 1;
100
101
      dirList.add( dir );
102
    }
103
104
    final var dirs = dirList.toArray( new String[]{} );
105
    var path = locate( dirs, "Wizard.container.executable.path" );
106
107
    if( path.isEmpty() ) {
108
      clue();
109
110
      try {
111
        path = where();
112
      } catch( final IOException ex ) {
113
        clue( "Wizard.container.executable.which", ex );
114
      }
115
    }
116
117
    return path.isPresent()
118
      ? path
119
      : locate( System::getenv,
120
                IS_OS_WINDOWS
121
                  ? "Wizard.container.executable.registry"
122
                  : "Wizard.container.executable.path" );
123
  }
124
125
  private Optional<Path> locate( final String[] dirs, final String msg ) {
100126
    final var exe = getName();
101
    final var paths = paths( map ).split( quote( pathSeparator ) );
102127
103
    for( final var path : paths ) {
104
      final var p = Path.of( path ).resolve( exe );
128
    for( final var dir : dirs ) {
129
      final var p = Path.of( dir ).resolve( exe );
105130
106131
      for( final var extension : EXTENSIONS ) {
...
113138
    }
114139
140
    clue( msg );
115141
    return Optional.empty();
116142
  }
117143
118
  /**
119
   * Convenience method that locates a binary executable file in the path
120
   * by using {@link System#getenv(String)} to retrieve environment variables
121
   * that are expanded when parsing the PATH.
122
   *
123
   * @see #locate(Function)
124
   */
125
  public Optional<Path> locate() {
126
    return locate( System::getenv );
144
  private Optional<Path> locate(
145
    final Function<String, String> map, final String msg ) {
146
    final var paths = paths( map ).split( quote( pathSeparator ) );
147
148
    return locate( paths, msg );
127149
  }
128150
129151
  /**
130
   * Provides {@code null}-safe machinery to get a file name.
152
   * Runs {@code where} or {@code which} to determine the fully qualified path
153
   * to an executable.
131154
   *
132
   * @param p The path to the file name to retrieve (may be {@code null}).
133
   * @return The file name or the empty string if the path is not found.
155
   * @return The path to the executable for this file, if found.
156
   * @throws IOException Could not determine the location of the command.
134157
   */
135
  public static String getFileName( final Path p ) {
136
    return p == null ? "" : getPathFileName( p );
137
  }
138
139
  private static String getPathFileName( final Path p ) {
140
    assert p != null;
141
142
    final var f = p.getFileName();
158
  public Optional<Path> where() throws IOException {
159
    // The "where" command on Windows will automatically add the extension.
160
    final var args = new String[]{WHERE_COMMAND, getName()};
161
    final var output = run( text -> true, args );
162
    final var result = output.lines().findFirst();
143163
144
    return f == null ? "" : f.toString();
164
    return result.map( Path::of );
145165
  }
146166
...
153173
   * @return The revised PATH variables as stored in the registry.
154174
   */
155
  private String paths( final Function<String, String> map ) {
175
  private static String paths( final Function<String, String> map ) {
156176
    return IS_OS_WINDOWS ? pathsWindows( map ) : pathsSane();
157177
  }
158178
159
  private String pathsSane() {
160
    return getenv( "PATH" );
161
  }
179
  /**
180
   * Answers whether this file's SHA-256 checksum equals the given
181
   * hexadecimal-encoded checksum string.
182
   *
183
   * @param hex The string to compare against the checksum for this file.
184
   * @return {@code true} if the checksums match; {@code false} on any
185
   * error or checksums don't match.
186
   */
187
  public boolean isChecksum( final String hex ) {
188
    assert hex != null;
162189
163
  @SuppressWarnings( "SpellCheckingInspection" )
164
  private String pathsWindows( final Function<String, String> map ) {
165190
    try {
166
      final var hklm = query( SYS_KEY );
167
      final var hkcu = query( USR_KEY );
191
      return checksum( "SHA-256" ).equalsIgnoreCase( hex );
192
    } catch( final Exception ex ) {
193
      return false;
194
    }
195
  }
168196
169
      return expand( hklm, map ) + pathSeparator + expand( hkcu, map );
170
    } catch( final IOException ex ) {
171
      // Return the PATH environment variable if the registry query fails.
172
      return pathsSane();
197
  /**
198
   * Returns the hash code for this file.
199
   *
200
   * @return The hex-encoded hash code for the file contents.
201
   */
202
  @SuppressWarnings( "SameParameterValue" )
203
  private String checksum( final String algorithm )
204
    throws NoSuchAlgorithmException, IOException {
205
    final var digest = MessageDigest.getInstance( algorithm );
206
207
    try( final var in = new FileInputStream( this ) ) {
208
      final var bytes = new byte[ BUFFER_SIZE ];
209
      int count;
210
211
      while( (count = in.read( bytes )) != -1 ) {
212
        digest.update( bytes, 0, count );
213
      }
214
215
      return toHex( digest.digest() );
173216
    }
174217
  }
175218
176219
  /**
177
   * Queries a registry key PATH value.
220
   * Runs a command and collects standard output into a buffer.
178221
   *
179
   * @param key The registry key name to look up.
180
   * @return The value for the registry key.
222
   * @param filter Provides an injected test to determine whether the line
223
   *               read from the command's standard output is to be added to
224
   *               the result buffer.
225
   * @param args   The command and its arguments to run.
226
   * @return The standard output from the command, filtered.
227
   * @throws IOException Could not run the command.
181228
   */
182
  private String query( final String key ) throws IOException {
183
    final var regVarName = "path";
184
    final var args = new String[]{"reg", "query", key, "/v", regVarName};
229
  @NotNull
230
  public static String run( final Predicate<String> filter,
231
                            final String[] args ) throws IOException {
185232
    final var process = Runtime.getRuntime().exec( args );
186233
    final var stream = process.getInputStream();
187
    final var regValue = new StringBuffer( 1024 );
234
    final var stdout = new StringBuffer( 2048 );
188235
189236
    StreamGobbler.gobble( stream, text -> {
190
      if( text.contains( regVarName ) ) {
191
        regValue.append( parseRegEntry( text ) );
237
      if( filter.test( text ) ) {
238
        stdout.append( WindowsRegistry.parseRegEntry( text ) );
192239
      }
193240
    } );
...
200247
      process.destroy();
201248
    }
202
203
204
    return regValue.toString();
205
  }
206
207
  String parseRegEntry( final String text ) {
208
    assert text != null;
209249
210
    final var matcher = REG_PATTERN.matcher( text );
211
    return matcher.find() ? matcher.group( 1 ) : text.trim();
250
    return stdout.toString();
212251
  }
213252
214253
  /**
215
   * PATH environment variables returned from the registry have unexpanded
216
   * variables of the form %VARIABLE%. This method will expand those values,
217
   * if possible, from the environment. This will only perform a single
218
   * expansion, which should be adequate for most needs.
254
   * Provides {@code null}-safe machinery to get a file name.
219255
   *
220
   * @param s The %VARIABLE%-encoded value to expand.
221
   * @return The given value with all encoded values expanded.
256
   * @param p The path to the file name to retrieve (may be {@code null}).
257
   * @return The file name or the empty string if the path is not found.
222258
   */
223
  String expand( final String s, final Function<String, String> map ) {
224
    // Assigned to the unexpanded string, initially.
225
    String expanded = s;
226
227
    final var matcher = VAR_PATTERN.matcher( expanded );
228
229
    while( matcher.find() ) {
230
      final var match = matcher.group( 0 );
231
      String value = map.apply( match );
232
233
      if( value == null ) {
234
        value = "";
235
      }
236
      else {
237
        value = value.replace( "\\", "\\\\" );
238
      }
239
240
      final var subexpression = compile( quote( match ) );
241
      expanded = subexpression.matcher( expanded ).replaceAll( value );
242
    }
243
244
    return expanded;
259
  public static String getFileName( final Path p ) {
260
    return p == null ? "" : getPathFileName( p );
245261
  }
246262
247263
  /**
248
   * Answers whether this file's SHA-256 checksum equals the given
249
   * hexadecimal-encoded checksum string.
264
   * If the path doesn't exist right before typesetting, switch the path
265
   * to the user's home directory to increase the odds of the typesetter
266
   * succeeding. This could help, for example, if the images directory was
267
   * deleted or moved.
250268
   *
251
   * @param hex The string to compare against the checksum for this file.
252
   * @return {@code true} if the checksums match; {@code false} on any
253
   * error or checksums don't match.
269
   * @param path The path to verify existence.
270
   * @return The given path, if it exists, otherwise the user's home directory.
254271
   */
255
  public boolean isChecksum( final String hex ) {
256
    assert hex != null;
272
  public static Path normalize( final Path path ) {
273
    assert path != null;
257274
258
    try {
259
      return checksum( "SHA-256" ).equalsIgnoreCase( hex );
260
    } catch( final Exception ex ) {
261
      return false;
262
    }
275
    return path.toFile().exists() ? path : USER_DIRECTORY.toPath();
263276
  }
264277
265
  /**
266
   * Returns the hash code for this file.
267
   *
268
   * @return The hex-encoded hash code for the file contents.
269
   */
270
  @SuppressWarnings( "SameParameterValue" )
271
  private String checksum( final String algorithm )
272
    throws NoSuchAlgorithmException, IOException {
273
    final var digest = MessageDigest.getInstance( algorithm );
278
  private static String pathsSane() {
279
    return getenv( "PATH" );
280
  }
274281
275
    try( final var in = new FileInputStream( this ) ) {
276
      final var bytes = new byte[ BUFFER_SIZE ];
277
      int count;
282
  private static String getPathFileName( final Path p ) {
283
    assert p != null;
278284
279
      while( (count = in.read( bytes )) != -1 ) {
280
        digest.update( bytes, 0, count );
281
      }
285
    final var f = p.getFileName();
282286
283
      return toHex( digest.digest() );
284
    }
287
    return f == null ? "" : f.toString();
285288
  }
286289
}
A src/main/java/com/keenwrite/io/WindowsRegistry.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.IOException;
5
import java.util.function.Function;
6
import java.util.regex.Pattern;
7
8
import static java.io.File.pathSeparator;
9
import static java.lang.System.getenv;
10
import static java.util.regex.Pattern.compile;
11
import static java.util.regex.Pattern.quote;
12
13
/**
14
 * Responsible for obtaining Windows registry key values.
15
 */
16
public class WindowsRegistry {
17
  //@formatter:off
18
  private static final String SYS_KEY =
19
    "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
20
  private static final String USR_KEY =
21
    "HKEY_CURRENT_USER\\Environment";
22
  //@formatter:on
23
24
  /**
25
   * Regular expression pattern for matching %VARIABLE% names.
26
   */
27
  private static final String VAR_REGEX = "%.*?%";
28
  private static final Pattern VAR_PATTERN = compile( VAR_REGEX );
29
30
  private static final String REG_REGEX = "\\s*path\\s+REG_EXPAND_SZ\\s+(.*)";
31
  private static final Pattern REG_PATTERN = compile( REG_REGEX );
32
33
  /**
34
   * Returns the value of the Windows PATH registry key.
35
   *
36
   * @return The PATH environment variable if the registry query fails.
37
   */
38
  @SuppressWarnings( "SpellCheckingInspection" )
39
  public static String pathsWindows( final Function<String, String> map ) {
40
    try {
41
      final var hklm = query( SYS_KEY );
42
      final var hkcu = query( USR_KEY );
43
44
      return expand( hklm, map ) + pathSeparator + expand( hkcu, map );
45
    } catch( final IOException ex ) {
46
      return getenv( "PATH" );
47
    }
48
  }
49
50
  /**
51
   * Queries a registry key PATH value.
52
   *
53
   * @param key The registry key name to look up.
54
   * @return The value for the registry key.
55
   */
56
  private static String query( final String key ) throws IOException {
57
    final var registryVarName = "path";
58
    final var args = new String[]{"reg", "query", key, "/v", registryVarName};
59
60
    return SysFile.run( text -> text.contains( registryVarName ), args );
61
  }
62
63
  static String parseRegEntry( final String text ) {
64
    assert text != null;
65
66
    final var matcher = REG_PATTERN.matcher( text );
67
    return matcher.find() ? matcher.group( 1 ) : text.trim();
68
  }
69
70
  /**
71
   * PATH environment variables returned from the registry have unexpanded
72
   * variables of the form %VARIABLE%. This method will expand those values,
73
   * if possible, from the environment. This will only perform a single
74
   * expansion, which should be adequate for most needs.
75
   *
76
   * @param s The %VARIABLE%-encoded value to expand.
77
   * @return The given value with all encoded values expanded.
78
   */
79
  static String expand( final String s, final Function<String, String> map ) {
80
    // Assigned to the unexpanded string, initially.
81
    String expanded = s;
82
83
    final var matcher = VAR_PATTERN.matcher( expanded );
84
85
    while( matcher.find() ) {
86
      final var match = matcher.group( 0 );
87
      String value = map.apply( match );
88
89
      if( value == null ) {
90
        value = "";
91
      }
92
      else {
93
        value = value.replace( "\\", "\\\\" );
94
      }
95
96
      final var subexpression = compile( quote( match ) );
97
      expanded = subexpression.matcher( expanded ).replaceAll( value );
98
    }
99
100
    return expanded;
101
  }
102
}
1103
M src/main/java/com/keenwrite/processors/PdfProcessor.java
77
import static com.keenwrite.events.StatusEvent.clue;
88
import static com.keenwrite.io.MediaType.TEXT_XML;
9
import static com.keenwrite.io.SysFile.normalize;
910
import static com.keenwrite.typesetting.Typesetter.Mutator;
1011
import static java.nio.file.Files.deleteIfExists;
...
3940
      clue( "Main.status.typeset.setting", "target", targetPath );
4041
41
      final var parent = targetPath.toAbsolutePath().getParent();
42
      final var parent = normalize( targetPath.toAbsolutePath().getParent() );
4243
4344
      final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent );
4445
      final var sourcePath = writeString( document, xhtml );
4546
      clue( "Main.status.typeset.setting", "source", sourcePath );
4647
47
      final var themeDir = context.getThemeDir();
48
      final var themeDir = normalize( context.getThemeDir() );
4849
      clue( "Main.status.typeset.setting", "themes", themeDir );
4950
50
      final var imageDir = context.getImageDir();
51
      final var imageDir = normalize( context.getImageDir() );
5152
      clue( "Main.status.typeset.setting", "images", imageDir );
5253
53
      final var cacheDir = context.getCacheDir();
54
      final var imageOrder = context.getImageOrder();
55
      clue( "Main.status.typeset.setting", "order", imageOrder );
56
57
      final var cacheDir = normalize( context.getCacheDir() );
5458
      clue( "Main.status.typeset.setting", "caches", cacheDir );
5559
56
      final var fontDir = context.getFontDir();
60
      final var fontDir = normalize( context.getFontDir() );
5761
      clue( "Main.status.typeset.setting", "fonts", fontDir );
58
59
      final var autoRemove = context.getAutoRemove();
60
      clue( "Main.status.typeset.setting", "purge", autoRemove );
6162
62
      final var rWorkDir = context.getRWorkingDir();
63
      final var rWorkDir = normalize( context.getRWorkingDir() );
6364
      clue( "Main.status.typeset.setting", "r-work", rWorkDir );
6465
65
      final var imageOrder = context.getImageOrder();
66
      clue( "Main.status.typeset.setting", "order", imageOrder );
66
      final var autoRemove = context.getAutoRemove();
67
      clue( "Main.status.typeset.setting", "purge", autoRemove );
6768
6869
      final var typesetter = Typesetter
6970
        .builder()
70
        .with( Mutator::setSourcePath, sourcePath )
7171
        .with( Mutator::setTargetPath, targetPath )
72
        .with( Mutator::setSourcePath, sourcePath )
7273
        .with( Mutator::setThemeDir, themeDir )
7374
        .with( Mutator::setImageDir, imageDir )
M src/main/java/com/keenwrite/typesetting/GuestTypesetter.java
1212
import java.util.concurrent.Callable;
1313
14
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
14
import static com.keenwrite.events.StatusEvent.clue;
1515
import static com.keenwrite.io.StreamGobbler.gobble;
16
import static com.keenwrite.typesetting.containerization.Podman.MANAGER;
16
import static com.keenwrite.io.SysFile.normalize;
1717
import static java.lang.String.format;
1818
...
8181
8282
    return true;
83
  }
84
85
  /**
86
   * If the path doesn't exist right before typesetting, switch the path
87
   * to the user's home directory to increase the odds of the typesetter
88
   * succeeding. This could help, for example, if the images directory was
89
   * deleted or moved.
90
   *
91
   * @param path The path to verify existence.
92
   * @return The given path, if it exists, otherwise the user's home directory.
93
   */
94
  private static Path normalize( final Path path ) {
95
    assert path != null;
96
97
    return path.toFile().exists()
98
      ? path
99
      : USER_DIRECTORY.toPath();
10083
  }
10184
...
10992
   */
11093
  static boolean isReady() {
111
    if( MANAGER.canRun() ) {
94
    if( Podman.canRun() ) {
11295
      final var exitCode = new StringBuilder();
11396
      final var manager = new Podman();
...
122105
        // If the typesetter ran with an exit code of 0, it is available.
123106
        return exitCode.indexOf( "0" ) == 0;
124
      } catch( final CommandNotFoundException ignored ) { }
107
      } catch( final CommandNotFoundException ex ) {
108
        clue( ex );
109
      }
125110
    }
126111
M src/main/java/com/keenwrite/typesetting/containerization/Podman.java
66
77
import java.io.File;
8
import java.io.IOException;
8
import java.nio.file.Files;
99
import java.nio.file.Path;
1010
import java.util.LinkedList;
1111
import java.util.List;
12
import java.util.NoSuchElementException;
1312
1413
import static com.keenwrite.Bootstrap.CONTAINER_VERSION;
14
import static com.keenwrite.events.StatusEvent.clue;
1515
import static java.lang.String.format;
16
import static java.lang.String.join;
1617
import static java.lang.System.arraycopy;
1718
import static java.util.Arrays.copyOf;
19
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
1820
1921
/**
2022
 * Provides facilities for interacting with a container environment.
2123
 */
2224
public final class Podman implements ContainerManager {
23
  public static final SysFile MANAGER = new SysFile( "podman" );
25
  private static final String BINARY = "podman";
26
  private static final Path BINARY_PATH =
27
    Path.of(
28
      format( IS_OS_WINDOWS
29
                ? "C:\\Program Files\\RedHat\\Podman\\%s.exe"
30
                : "/usr/bin/%s",
31
              BINARY
32
      )
33
    );
34
  private static final SysFile MANAGER = new SysFile( BINARY );
35
2436
  public static final String CONTAINER_SHORTNAME = "typesetter";
2537
  public static final String CONTAINER_NAME =
2638
    format( "%s:%s", CONTAINER_SHORTNAME, CONTAINER_VERSION );
2739
2840
  private final List<String> mMountPoints = new LinkedList<>();
2941
3042
  public Podman() { }
43
44
  /**
45
   * Answers whether the container is installed and runnable on the host.
46
   *
47
   * @return {@code true} if the container is available.
48
   */
49
  public static boolean canRun() {
50
    try {
51
      return getExecutable().toFile().isFile();
52
    } catch( final Exception ex ) {
53
      clue( "Wizard.container.executable.run.error", ex );
54
55
      // If the binary couldn't be found, then indicate that it cannot run.
56
      return false;
57
    }
58
  }
59
60
  private static Path getExecutable() {
61
    final var executable = Files.isExecutable( BINARY_PATH );
62
63
    clue( "Wizard.container.executable.run.scan", BINARY_PATH, executable );
64
65
    return executable
66
      ? BINARY_PATH
67
      : MANAGER.locate().orElseThrow();
68
  }
3169
3270
  @Override
3371
  public int install( final File exe ) {
3472
    // This monstrosity runs the installer in the background without displaying
3573
    // a secondary command window, while blocking until the installer completes
3674
    // and an exit code can be determined. I hate Windows.
37
    final var builder = processBuilder(
38
      "cmd", "/c",
39
      format(
40
        "start /b /high /wait cmd /c %s /quiet /install & exit ^!errorlevel^!",
41
        exe.getAbsolutePath()
42
      )
75
    final var cmd = format(
76
      "start /b /high /wait cmd /c %s /quiet /install & exit ^!errorlevel^!",
77
      exe.getAbsolutePath()
4378
    );
79
80
    clue( "Wizard.container.install.command", cmd );
81
82
    final var builder = processBuilder( "cmd", "/c", cmd );
4483
4584
    try {
85
      clue( "Wizard.container.install.await", cmd );
86
4687
      // Wait for installation to finish (successfully or not).
4788
      return wait( builder.start() );
...
128169
    throws CommandNotFoundException {
129170
    try {
130
      final var exe = MANAGER.locate();
131
      final var path = exe.orElseThrow();
171
      final var path = getExecutable();
172
      final var joined = join( ",", args );
173
174
      clue( "Wizard.container.process.enter", path, joined );
175
132176
      final var builder = processBuilder( path, args );
133177
      final var process = builder.start();
134178
135179
      processor.start( process.getInputStream() );
136180
137181
      return wait( process );
138
    } catch( final NoSuchElementException |
139
                   IOException |
140
                   InterruptedException ex ) {
182
    } catch( final Exception ex ) {
183
      clue( ex );
141184
      throw new CommandNotFoundException( MANAGER.toString() );
142185
    }
...
152195
  private static int wait( final Process process ) throws InterruptedException {
153196
    final var exitCode = process.waitFor();
197
198
    clue( "Wizard.container.process.exit", exitCode );
199
154200
    process.destroy();
155201
D src/main/java/com/keenwrite/typesetting/installer/WizardConstants.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer;
3
4
/**
5
 * Provides common constants across all panes.
6
 */
7
public class WizardConstants {
8
9
10
  private WizardConstants() { }
11
}
121
M src/main/java/com/keenwrite/typesetting/installer/panes/AbstractDownloadPane.java
5353
5454
    if( thread instanceof Task<?> downloader && downloader.isRunning() ) {
55
      clue( "Wizard.container.install.download.running" );
5556
      return;
5657
    }
...
7071
    }
7172
    else {
73
      clue( "Wizard.container.install.download.started", mUri );
74
7275
      final var task = downloadAsync( mUri, target, ( progress, bytes ) -> {
7376
        final var suffix = progress < 0 ? ".bytes" : ".progress";
M src/main/java/com/keenwrite/ui/actions/GuiCommands.java
271271
   */
272272
  private void file_export_pdf( final boolean dir ) {
273
    final var workspace = getWorkspace();
274
    final var themes = workspace.getFile(
275
      KEY_TYPESET_CONTEXT_THEMES_PATH
276
    );
277
    final var theme = workspace.stringProperty(
278
      KEY_TYPESET_CONTEXT_THEME_SELECTION
279
    );
280
    final var chapters = workspace.stringProperty(
281
      KEY_TYPESET_CONTEXT_CHAPTERS
282
    );
283
    final var settings = ExportSettings
284
      .builder()
285
      .with( ExportSettings.Mutator::setTheme, theme )
286
      .with( ExportSettings.Mutator::setChapters, chapters )
287
      .build();
288
289273
    // Don't re-validate the typesetter installation each time. If the
290274
    // user mucks up the typesetter installation, it'll get caught the
291275
    // next time the application is started. Don't use |= because it
292276
    // won't short-circuit.
293277
    mCanTypeset = mCanTypeset || Typesetter.canRun();
294278
295279
    if( mCanTypeset ) {
280
      final var workspace = getWorkspace();
281
      final var theme = workspace.stringProperty(
282
        KEY_TYPESET_CONTEXT_THEME_SELECTION
283
      );
284
      final var chapters = workspace.stringProperty(
285
        KEY_TYPESET_CONTEXT_CHAPTERS
286
      );
287
288
      final var settings = ExportSettings
289
        .builder()
290
        .with( ExportSettings.Mutator::setTheme, theme )
291
        .with( ExportSettings.Mutator::setChapters, chapters )
292
        .build();
293
294
      final var themes = workspace.getFile(
295
        KEY_TYPESET_CONTEXT_THEMES_PATH
296
      );
297
296298
      // If the typesetter is installed, allow the user to select a theme. If
297299
      // the themes aren't installed, a status message will appear.
M src/main/resources/bootstrap.properties
11
application.title=KeenWrite
2
container.version=2.11.5
2
container.version=3.0.0
33
M src/main/resources/com/keenwrite/messages.properties
319319
Wizard.typesetter.name=ConTeXt
320320
Wizard.typesetter.container.name=Podman
321
Wizard.typesetter.container.version=4.3.1
322
Wizard.typesetter.container.checksum=b741702663234ca36e1555149721580dc31ae76985d50c022a8641c6db2f5b93
323
Wizard.typesetter.themes.version=1.8.2
324
Wizard.typesetter.themes.checksum=00e0f46ea2cb4a812a4780b61a838ea94d78167c1abc9e16767401914a5e989d
321
Wizard.typesetter.container.version=4.5.1
322
Wizard.typesetter.container.checksum=883c9901d1eedb3a130256574afa270656e4d322dce0bb880808d9d7776eff8a
323
Wizard.typesetter.themes.version=1.8.3
324
Wizard.typesetter.themes.checksum=983b8d3c4051c6c002b8111ea786ebc83d5496ab5f8d734e96413c59304b2a75
325
326
Wizard.container.install.command=Installing container using: ''{0}''
327
Wizard.container.install.await=Waiting for installer to finish
328
Wizard.container.install.download.started=Download ''{0}'' started
329
Wizard.container.install.download.running=Download in progress, please wait
330
Wizard.container.process.enter=Running ''{0}'' ''{1}''
331
Wizard.container.process.exit=Process exit code (zero means success): {0}
332
Wizard.container.executable.run.scan=''{0}'' is executable: {1}
333
Wizard.container.executable.run.error=Cannot run container
334
Wizard.container.executable.which=Cannot find container using search command
335
Wizard.container.executable.path=Cannot find container using PATH variable
336
Wizard.container.executable.registry=Cannot find container using registry
325337
326338
# STEP 1: Introduction panel (all)
M src/main/resources/lexicons/en.txt
Binary file
M src/test/java/com/keenwrite/io/SysFileTest.java
44
import org.junit.jupiter.api.Test;
55
6
import static org.junit.jupiter.api.Assertions.assertEquals;
7
import static org.junit.jupiter.api.Assertions.assertTrue;
6
import java.io.IOException;
7
import java.nio.file.Path;
8
import java.util.Optional;
9
import java.util.concurrent.atomic.AtomicBoolean;
10
import java.util.function.Function;
11
12
import static org.junit.jupiter.api.Assertions.*;
813
914
class SysFileTest {
10
  private static final String REG_PATH_PREFIX =
11
    "%USERPROFILE%";
12
  private static final String REG_PATH_SUFFIX =
13
    "\\AppData\\Local\\Microsoft\\WindowsApps;";
14
  private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX;
1515
1616
  @Test
1717
  void test_Locate_ExistingExecutable_PathFound() {
18
    final var command = "ls";
19
    final var file = new SysFile( command );
20
    assertTrue( file.canRun() );
21
22
    final var located = file.locate();
23
    assertTrue( located.isPresent() );
24
25
    final var path = located.get();
26
    final var actual = path.toAbsolutePath().toString();
27
    final var expected = "/usr/bin/" + command;
28
29
    assertEquals( expected, actual );
18
    testFunction( SysFile::locate, "ls", "/usr/bin/ls" );
3019
  }
3120
3221
  @Test
33
  void test_Parse_RegistryEntry_ValueObtained() {
34
    final var file = new SysFile( "unused" );
35
    final var expected = REG_PATH;
36
    final var actual =
37
      file.parseRegEntry( "    path    REG_EXPAND_SZ    " + expected );
38
39
    assertEquals( expected, actual );
22
  void test_Where_ExistingExecutable_PathFound() {
23
    testFunction( sysFile -> {
24
      try {
25
        return sysFile.where();
26
      } catch( final IOException e ) {
27
        throw new RuntimeException( e );
28
      }
29
    }, "which", "/usr/bin/which" );
4030
  }
4131
42
  @Test
43
  void test_Expand_RegistryEntry_VariablesExpanded() {
44
    final var value = "UserProfile";
45
    final var file = new SysFile( "unused" );
46
    final var expected = value + REG_PATH_SUFFIX;
47
    final var actual = file.expand( REG_PATH, s -> value );
32
  void testFunction( final Function<SysFile, Optional<Path>> consumer,
33
                     final String command,
34
                     final String expected ) {
35
    final var file = new SysFile( command );
36
    final var path = consumer.apply( file );
37
    final var failed = new AtomicBoolean( false );
4838
49
    assertEquals( expected, actual );
39
    assertTrue( file.canRun() );
40
41
    path.ifPresentOrElse(
42
      location -> {
43
        final var actual = location.toAbsolutePath().toString();
44
45
        assertEquals( expected, actual );
46
      },
47
      () -> failed.set( true )
48
    );
49
50
    assertFalse( failed.get() );
5051
  }
5152
}
A src/test/java/com/keenwrite/io/WindowsRegistryTest.java
1
package com.keenwrite.io;
2
3
import org.junit.jupiter.api.Test;
4
5
import static com.keenwrite.io.WindowsRegistry.*;
6
import static org.junit.jupiter.api.Assertions.*;
7
8
class WindowsRegistryTest {
9
  private static final String REG_PATH_PREFIX =
10
    "%USERPROFILE%";
11
  private static final String REG_PATH_SUFFIX =
12
    "\\AppData\\Local\\Microsoft\\WindowsApps;";
13
  private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX;
14
15
  @Test
16
  void test_Parse_RegistryEntry_ValueObtained() {
17
    final var expected = REG_PATH;
18
    final var actual = parseRegEntry(
19
      "    path    REG_EXPAND_SZ    " + expected
20
    );
21
22
    assertEquals( expected, actual );
23
  }
24
25
  @Test
26
  void test_Expand_RegistryEntry_VariablesExpanded() {
27
    final var value = "UserProfile";
28
    final var expected = value + REG_PATH_SUFFIX;
29
    final var actual = expand( REG_PATH, s -> value );
30
31
    assertEquals( expected, actual );
32
  }
33
}
134