| implementation 'org.greenrobot:eventbus-java:3.3.1' | ||
| + // Logging. | ||
| + implementation 'org.slf4j:slf4j-api:2.1.0-alpha1' | ||
| + implementation 'org.slf4j:slf4j-nop:2.0.16' | ||
| + | ||
| // Command-line parsing | ||
| implementation "info.picocli:picocli:${v_picocli}" | ||
| tasks.withType( JavaCompile ).configureEach { | ||
| options.encoding = 'UTF-8' | ||
| -} | ||
| - | ||
| -tasks.withType( JavaExec ).configureEach { | ||
| - jvmArgs += '--enable-preview' | ||
| -} | ||
| - | ||
| -tasks.withType( Test ).configureEach { | ||
| - jvmArgs += '--enable-preview' | ||
| } | ||
| readonly SCRIPT_SRC="\$(dirname "\${BASH_SOURCE[\${#BASH_SOURCE[@]} - 1]}")" | ||
| -"\${SCRIPT_SRC}/${ARG_JAVA_DIR}/bin/java" ${OPT_JAVA} -jar "\${SCRIPT_SRC}/${FILE_APP_JAR}" "\$@" 2>/dev/null | ||
| +"\${SCRIPT_SRC}/${ARG_JAVA_DIR}/bin/java" ${OPT_JAVA} -jar "\${SCRIPT_SRC}/${FILE_APP_JAR}" "\$@" | ||
| __EOT | ||
| set SCRIPT_DIR=%~dp0 | ||
| -"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" ${OPT_JAVA} -jar "%SCRIPT_DIR%\\${FILE_APP_JAR}" %* 2>nul | ||
| +"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" ${OPT_JAVA} -jar "%SCRIPT_DIR%\\${FILE_APP_JAR}" %* | ||
| __EOT | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Future; | ||
| -import java.util.concurrent.atomic.AtomicInteger; | ||
| -import static com.keenwrite.Launcher.terminate; | ||
| import static com.keenwrite.events.StatusEvent.clue; | ||
| import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN; | ||
| public static void run( final Arguments args ) { | ||
| - final var exitCode = new AtomicInteger(); | ||
| - | ||
| final var future = new CompletableFuture<Path>() { | ||
| @Override | ||
| public boolean complete( final Path path ) { | ||
| return super.complete( path ); | ||
| } | ||
| @Override | ||
| public boolean completeExceptionally( final Throwable ex ) { | ||
| clue( ex ); | ||
| - exitCode.set( 1 ); | ||
| return super.completeExceptionally( ex ); | ||
| } | ||
| }; | ||
| file_export( args, future ); | ||
| sExecutor.shutdown(); | ||
| future.join(); | ||
| - terminate( exitCode.get() ); | ||
| } | ||
| +/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite; | ||
| + | ||
| +import com.keenwrite.cmdline.HeadlessApp; | ||
| +import com.keenwrite.events.HyperlinkOpenEvent; | ||
| +import com.keenwrite.preferences.Workspace; | ||
| +import com.keenwrite.preview.MathRenderer; | ||
| +import com.keenwrite.spelling.impl.Lexicon; | ||
| +import javafx.application.Application; | ||
| +import javafx.event.Event; | ||
| +import javafx.event.EventType; | ||
| +import javafx.scene.input.KeyCode; | ||
| +import javafx.scene.input.KeyEvent; | ||
| +import javafx.stage.Stage; | ||
| +import org.greenrobot.eventbus.Subscribe; | ||
| + | ||
| +import java.io.OutputStream; | ||
| +import java.io.PrintStream; | ||
| +import java.util.function.BooleanSupplier; | ||
| + | ||
| +import static com.keenwrite.Bootstrap.APP_TITLE; | ||
| +import static com.keenwrite.constants.GraphicsConstants.LOGOS; | ||
| +import static com.keenwrite.events.Bus.register; | ||
| +import static com.keenwrite.preferences.AppKeys.*; | ||
| +import static com.keenwrite.util.FontLoader.initFonts; | ||
| +import static javafx.scene.input.KeyCode.F11; | ||
| +import static javafx.scene.input.KeyEvent.KEY_PRESSED; | ||
| +import static javafx.scene.input.KeyEvent.KEY_RELEASED; | ||
| +import static javafx.stage.WindowEvent.WINDOW_SHOWN; | ||
| + | ||
| +/** | ||
| + * The application allows users to edit plain text files in a markup notation | ||
| + * and see a real-time preview of the formatted output. | ||
| + */ | ||
| +public final class GuiApp extends Application { | ||
| + | ||
| + private Workspace mWorkspace; | ||
| + | ||
| + /** | ||
| + * GUI application entry point. See {@link HeadlessApp} for the entry | ||
| + * point to the command-line application. | ||
| + * | ||
| + * @param args Command-line arguments. | ||
| + */ | ||
| + public static void run( final String[] args ) { | ||
| + launch( args ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates an instance of {@link KeyEvent} that represents pressing a key. | ||
| + * | ||
| + * @param code The key to simulate being pressed down. | ||
| + * @param shift Whether shift key modifier shall modify the key code. | ||
| + * @return An instance of {@link KeyEvent} that may be used to simulate | ||
| + * a key being pressed. | ||
| + */ | ||
| + public static Event keyDown( final KeyCode code, final boolean shift ) { | ||
| + return keyEvent( KEY_PRESSED, code, shift ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates an instance of {@link KeyEvent} that represents a key released | ||
| + * event without any modifier keys held. | ||
| + * | ||
| + * @param code The key code representing a key to simulate releasing. | ||
| + * @return An instance of {@link KeyEvent}. | ||
| + */ | ||
| + public static Event keyDown( final KeyCode code ) { | ||
| + return keyDown( code, false ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates an instance of {@link KeyEvent} that represents releasing a key. | ||
| + * | ||
| + * @param code The key to simulate being released up. | ||
| + * @param shift Whether shift key modifier shall modify the key code. | ||
| + * @return An instance of {@link KeyEvent} that may be used to simulate | ||
| + * a key being released. | ||
| + */ | ||
| + @SuppressWarnings( "unused" ) | ||
| + public static Event keyUp( final KeyCode code, final boolean shift ) { | ||
| + return keyEvent( KEY_RELEASED, code, shift ); | ||
| + } | ||
| + | ||
| + private static Event keyEvent( | ||
| + final EventType<KeyEvent> type, final KeyCode code, final boolean shift ) { | ||
| + return new KeyEvent( | ||
| + type, "", "", code, shift, false, false, false | ||
| + ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * JavaFX entry point. | ||
| + * | ||
| + * @param stage The primary application stage. | ||
| + */ | ||
| + @Override | ||
| + public void start( final Stage stage ) { | ||
| + // Must be instantiated after the UI is initialized (i.e., not in main) | ||
| + // because it interacts with GUI properties. | ||
| + mWorkspace = new Workspace(); | ||
| + | ||
| + // The locale was already loaded when the workspace was created. This | ||
| + // ensures that when the locale preference changes, a new spellchecker | ||
| + // instance will be loaded and applied. | ||
| + final var property = mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE ); | ||
| + property.addListener( ( _, _, _ ) -> readLexicon() ); | ||
| + | ||
| + initFonts(); | ||
| + initState( stage ); | ||
| + initStage( stage ); | ||
| + initIcons( stage ); | ||
| + initScene( stage ); | ||
| + | ||
| + MathRenderer.bindSize( mWorkspace.doubleProperty( KEY_UI_FONT_MATH_SIZE ) ); | ||
| + | ||
| + // Load the lexicon and check all the documents after all files are open. | ||
| + stage.addEventFilter( WINDOW_SHOWN, _ -> readLexicon() ); | ||
| + stage.show(); | ||
| + | ||
| + register( this ); | ||
| + } | ||
| + | ||
| + private void initState( final Stage stage ) { | ||
| + final var enable = createBoundsEnabledSupplier( stage ); | ||
| + | ||
| + stage.setX( mWorkspace.getDouble( KEY_UI_WINDOW_X ) ); | ||
| + stage.setY( mWorkspace.getDouble( KEY_UI_WINDOW_Y ) ); | ||
| + stage.setWidth( mWorkspace.getDouble( KEY_UI_WINDOW_W ) ); | ||
| + stage.setHeight( mWorkspace.getDouble( KEY_UI_WINDOW_H ) ); | ||
| + stage.setMaximized( mWorkspace.getBoolean( KEY_UI_WINDOW_MAX ) ); | ||
| + stage.setFullScreen( mWorkspace.getBoolean( KEY_UI_WINDOW_FULL ) ); | ||
| + | ||
| + mWorkspace.listen( KEY_UI_WINDOW_X, stage.xProperty(), enable ); | ||
| + mWorkspace.listen( KEY_UI_WINDOW_Y, stage.yProperty(), enable ); | ||
| + mWorkspace.listen( KEY_UI_WINDOW_W, stage.widthProperty(), enable ); | ||
| + mWorkspace.listen( KEY_UI_WINDOW_H, stage.heightProperty(), enable ); | ||
| + mWorkspace.listen( KEY_UI_WINDOW_MAX, stage.maximizedProperty() ); | ||
| + mWorkspace.listen( KEY_UI_WINDOW_FULL, stage.fullScreenProperty() ); | ||
| + } | ||
| + | ||
| + private void initStage( final Stage stage ) { | ||
| + stage.setTitle( APP_TITLE ); | ||
| + stage.addEventHandler( KEY_PRESSED, event -> { | ||
| + if( F11.equals( event.getCode() ) ) { | ||
| + stage.setFullScreen( !stage.isFullScreen() ); | ||
| + } | ||
| + } ); | ||
| + } | ||
| + | ||
| + private void initIcons( final Stage stage ) { | ||
| + stage.getIcons().addAll( LOGOS ); | ||
| + } | ||
| + | ||
| + private void initScene( final Stage stage ) { | ||
| + final var mainScene = new MainScene( mWorkspace ); | ||
| + stage.setScene( mainScene.getScene() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * When a hyperlink website URL is clicked, this method is called to launch | ||
| + * the default browser to the event's location. | ||
| + * | ||
| + * @param event The event called when a hyperlink was clicked. | ||
| + */ | ||
| + @Subscribe | ||
| + public void handle( final HyperlinkOpenEvent event ) { | ||
| + getHostServices().showDocument( event.getUri().toString() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * This will load the lexicon for the user's preferred locale and fire | ||
| + * an event when the all entries in the lexicon have been loaded. | ||
| + */ | ||
| + private void readLexicon() { | ||
| + Lexicon.read( mWorkspace.getLocale() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * When the window is maximized, full screen, or iconified, prevent updating | ||
| + * the window bounds. This is used so that if the user exits the application | ||
| + * when full screen (or maximized), restarting the application will recall | ||
| + * the previous bounds, allowing for continuity of expected behaviour. | ||
| + * | ||
| + * @param stage The window to check for "normal" status. | ||
| + * @return {@code false} when the bounds must not be changed, ergo persisted. | ||
| + */ | ||
| + private BooleanSupplier createBoundsEnabledSupplier( final Stage stage ) { | ||
| + return () -> | ||
| + !(stage.isMaximized() || stage.isFullScreen() || stage.isIconified()); | ||
| + } | ||
| +} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| package com.keenwrite; | ||
| import com.keenwrite.cmdline.Arguments; | ||
| import com.keenwrite.cmdline.ColourScheme; | ||
| import com.keenwrite.cmdline.HeadlessApp; | ||
| import picocli.CommandLine; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| +import java.io.PrintStream; | ||
| import java.util.Properties; | ||
| import java.util.function.Consumer; | ||
| /** | ||
| - * Launches the application using the {@link MainApp} class. | ||
| + * This is the main entry point to the application. The {@link Launcher} class | ||
| + * is responsible for running the application using either the {@link GuiApp} or | ||
| + * {@link HeadlessApp} class, depending on whether running with command-line | ||
| + * arguments. | ||
| * | ||
| * <p> | ||
| * This is required until modules are implemented, which may never happen | ||
| * because the application should be ported away from Java and JavaFX. | ||
| * </p> | ||
| */ | ||
| public final class Launcher implements Consumer<Arguments> { | ||
| + static { | ||
| + // We don't care about the logging provider connection message. | ||
| + System.setProperty( "slf4j.internal.verbosity", "WARN" ); | ||
| + } | ||
| /** | ||
| * Needed for the GUI. | ||
| */ | ||
| private final String[] mArgs; | ||
| + | ||
| + /** | ||
| + * Where to write error messages. | ||
| + */ | ||
| + private static final PrintStream ERRORS = System.err; | ||
| /** | ||
| * @param exitCode Code to provide back to the calling shell. | ||
| */ | ||
| - public static void terminate( final int exitCode ) { | ||
| + private static void terminate( final int exitCode ) { | ||
| System.exit( exitCode ); | ||
| } | ||
| terminate( exitCode ); | ||
| } | ||
| - else if( parseResult.isVersionHelpRequested() ) { | ||
| + | ||
| + if( parseResult.isVersionHelpRequested() ) { | ||
| showAppInfo(); | ||
| terminate( exitCode ); | ||
| + } | ||
| + | ||
| + if( arguments.debug() ) { | ||
| + arguments.iterate( parseResult, Launcher::log ); | ||
| } | ||
| + | ||
| + terminate( exitCode ); | ||
| } | ||
| if( message != null && message.toLowerCase().contains( "javafx" ) ) { | ||
| message = "Run using a Java Runtime Environment that includes JavaFX."; | ||
| - out( "ERROR: %s", message ); | ||
| + log( "ERROR: %s", message ); | ||
| } | ||
| else { | ||
| - error.printStackTrace( System.err ); | ||
| + error.printStackTrace( ERRORS ); | ||
| } | ||
| } | ||
| /** | ||
| - * Suppress writing to standard error, suppresses writing log messages. | ||
| + * Suppress writing log messages. | ||
| */ | ||
| private static void disableLogging() { | ||
| LogManager.getLogManager().reset(); | ||
| - // TODO: Delete this after JavaFX/GTK 3 no longer barfs useless warnings. | ||
| - System.err.close(); | ||
| } | ||
| /** | ||
| * Writes the given placeholder text to standard output with a new line | ||
| * appended. | ||
| * | ||
| * @param message The format string specifier. | ||
| * @param args The arguments to substitute into the format string. | ||
| */ | ||
| - private static void out( final String message, final Object... args ) { | ||
| - System.out.printf( format( "%s%n", message ), args ); | ||
| + private static void log( final String message, final Object... args ) { | ||
| + ERRORS.printf( format( "%s%n", message ), args ); | ||
| + ERRORS.flush(); | ||
| } | ||
| private static void showAppInfo() { | ||
| - out( "%n%s version %s", APP_TITLE, APP_VERSION ); | ||
| - out( "Copyright 2016-%s White Magic Software, Ltd.", APP_YEAR ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Delegates running the application via the command-line argument parser. | ||
| - * This is the main entry point for the application, regardless of whether | ||
| - * run from the command-line or as a GUI. | ||
| - * | ||
| - * @param args Command-line arguments. | ||
| - */ | ||
| - public static void main( final String[] args ) { | ||
| - installTrustManager(); | ||
| - parse( args ); | ||
| + log( "%s version %s", APP_TITLE, APP_VERSION ); | ||
| + log( "Copyright 2016-%s White Magic Software, Ltd.", APP_YEAR ); | ||
| } | ||
| if( argCount <= 0 ) { | ||
| // When no command-line arguments are provided, launch the GUI. | ||
| - MainApp.main( mArgs ); | ||
| + GuiApp.run( mArgs ); | ||
| } | ||
| else { | ||
| // When command-line arguments are supplied, run in headless mode. | ||
| - HeadlessApp.main( args ); | ||
| + HeadlessApp.run( args, ERRORS ); | ||
| } | ||
| } catch( final Throwable t ) { | ||
| log( t ); | ||
| } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Delegates running the application via the command-line argument parser. | ||
| + * This is the main entry point for the application, regardless of whether | ||
| + * run from the command-line or as a GUI. | ||
| + * | ||
| + * @param args Command-line arguments. | ||
| + */ | ||
| + public static void main( final String[] args ) { | ||
| + installTrustManager(); | ||
| + parse( args ); | ||
| } | ||
| } | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite; | ||
| - | ||
| -import com.keenwrite.cmdline.HeadlessApp; | ||
| -import com.keenwrite.events.HyperlinkOpenEvent; | ||
| -import com.keenwrite.preferences.Workspace; | ||
| -import com.keenwrite.preview.MathRenderer; | ||
| -import com.keenwrite.spelling.impl.Lexicon; | ||
| -import javafx.application.Application; | ||
| -import javafx.event.Event; | ||
| -import javafx.event.EventType; | ||
| -import javafx.scene.input.KeyCode; | ||
| -import javafx.scene.input.KeyEvent; | ||
| -import javafx.stage.Stage; | ||
| -import org.greenrobot.eventbus.Subscribe; | ||
| - | ||
| -import java.io.PrintStream; | ||
| -import java.util.function.BooleanSupplier; | ||
| - | ||
| -import static com.keenwrite.Bootstrap.APP_TITLE; | ||
| -import static com.keenwrite.constants.GraphicsConstants.LOGOS; | ||
| -import static com.keenwrite.events.Bus.register; | ||
| -import static com.keenwrite.preferences.AppKeys.*; | ||
| -import static com.keenwrite.util.FontLoader.initFonts; | ||
| -import static javafx.scene.input.KeyCode.F11; | ||
| -import static javafx.scene.input.KeyEvent.KEY_PRESSED; | ||
| -import static javafx.scene.input.KeyEvent.KEY_RELEASED; | ||
| -import static javafx.stage.WindowEvent.WINDOW_SHOWN; | ||
| - | ||
| -/** | ||
| - * Application entry point. The application allows users to edit plain text | ||
| - * files in a markup notation and see a real-time preview of the formatted | ||
| - * output. | ||
| - */ | ||
| -public final class MainApp extends Application { | ||
| - | ||
| - private Workspace mWorkspace; | ||
| - | ||
| - /** | ||
| - * TODO: Delete this after JavaFX/GTK 3 no longer barfs useless warnings. | ||
| - */ | ||
| - @SuppressWarnings( "SameParameterValue" ) | ||
| - private static void stderrRedirect( final PrintStream stream ) { | ||
| - System.setErr( stream ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * GUI application entry point. See {@link HeadlessApp} for the entry | ||
| - * point to the command-line application. | ||
| - * | ||
| - * @param args Command-line arguments. | ||
| - */ | ||
| - public static void main( final String[] args ) { | ||
| - launch( args ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates an instance of {@link KeyEvent} that represents pressing a key. | ||
| - * | ||
| - * @param code The key to simulate being pressed down. | ||
| - * @param shift Whether shift key modifier shall modify the key code. | ||
| - * @return An instance of {@link KeyEvent} that may be used to simulate | ||
| - * a key being pressed. | ||
| - */ | ||
| - public static Event keyDown( final KeyCode code, final boolean shift ) { | ||
| - return keyEvent( KEY_PRESSED, code, shift ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates an instance of {@link KeyEvent} that represents a key released | ||
| - * event without any modifier keys held. | ||
| - * | ||
| - * @param code The key code representing a key to simulate releasing. | ||
| - * @return An instance of {@link KeyEvent}. | ||
| - */ | ||
| - public static Event keyDown( final KeyCode code ) { | ||
| - return keyDown( code, false ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates an instance of {@link KeyEvent} that represents releasing a key. | ||
| - * | ||
| - * @param code The key to simulate being released up. | ||
| - * @param shift Whether shift key modifier shall modify the key code. | ||
| - * @return An instance of {@link KeyEvent} that may be used to simulate | ||
| - * a key being released. | ||
| - */ | ||
| - @SuppressWarnings( "unused" ) | ||
| - public static Event keyUp( final KeyCode code, final boolean shift ) { | ||
| - return keyEvent( KEY_RELEASED, code, shift ); | ||
| - } | ||
| - | ||
| - private static Event keyEvent( | ||
| - final EventType<KeyEvent> type, final KeyCode code, final boolean shift ) { | ||
| - return new KeyEvent( | ||
| - type, "", "", code, shift, false, false, false | ||
| - ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * JavaFX entry point. | ||
| - * | ||
| - * @param stage The primary application stage. | ||
| - */ | ||
| - @Override | ||
| - public void start( final Stage stage ) { | ||
| - // Must be instantiated after the UI is initialized (i.e., not in main) | ||
| - // because it interacts with GUI properties. | ||
| - mWorkspace = new Workspace(); | ||
| - | ||
| - // The locale was already loaded when the workspace was created. This | ||
| - // ensures that when the locale preference changes, a new spellchecker | ||
| - // instance will be loaded and applied. | ||
| - final var property = mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE ); | ||
| - property.addListener( ( _, _, _ ) -> readLexicon() ); | ||
| - | ||
| - initFonts(); | ||
| - initState( stage ); | ||
| - initStage( stage ); | ||
| - initIcons( stage ); | ||
| - initScene( stage ); | ||
| - | ||
| - MathRenderer.bindSize( mWorkspace.doubleProperty( KEY_UI_FONT_MATH_SIZE ) ); | ||
| - | ||
| - // Load the lexicon and check all the documents after all files are open. | ||
| - stage.addEventFilter( WINDOW_SHOWN, _ -> readLexicon() ); | ||
| - stage.show(); | ||
| - | ||
| - stderrRedirect( System.out ); | ||
| - | ||
| - register( this ); | ||
| - } | ||
| - | ||
| - private void initState( final Stage stage ) { | ||
| - final var enable = createBoundsEnabledSupplier( stage ); | ||
| - | ||
| - stage.setX( mWorkspace.getDouble( KEY_UI_WINDOW_X ) ); | ||
| - stage.setY( mWorkspace.getDouble( KEY_UI_WINDOW_Y ) ); | ||
| - stage.setWidth( mWorkspace.getDouble( KEY_UI_WINDOW_W ) ); | ||
| - stage.setHeight( mWorkspace.getDouble( KEY_UI_WINDOW_H ) ); | ||
| - stage.setMaximized( mWorkspace.getBoolean( KEY_UI_WINDOW_MAX ) ); | ||
| - stage.setFullScreen( mWorkspace.getBoolean( KEY_UI_WINDOW_FULL ) ); | ||
| - | ||
| - mWorkspace.listen( KEY_UI_WINDOW_X, stage.xProperty(), enable ); | ||
| - mWorkspace.listen( KEY_UI_WINDOW_Y, stage.yProperty(), enable ); | ||
| - mWorkspace.listen( KEY_UI_WINDOW_W, stage.widthProperty(), enable ); | ||
| - mWorkspace.listen( KEY_UI_WINDOW_H, stage.heightProperty(), enable ); | ||
| - mWorkspace.listen( KEY_UI_WINDOW_MAX, stage.maximizedProperty() ); | ||
| - mWorkspace.listen( KEY_UI_WINDOW_FULL, stage.fullScreenProperty() ); | ||
| - } | ||
| - | ||
| - private void initStage( final Stage stage ) { | ||
| - stage.setTitle( APP_TITLE ); | ||
| - stage.addEventHandler( KEY_PRESSED, event -> { | ||
| - if( F11.equals( event.getCode() ) ) { | ||
| - stage.setFullScreen( !stage.isFullScreen() ); | ||
| - } | ||
| - } ); | ||
| - } | ||
| - | ||
| - private void initIcons( final Stage stage ) { | ||
| - stage.getIcons().addAll( LOGOS ); | ||
| - } | ||
| - | ||
| - private void initScene( final Stage stage ) { | ||
| - final var mainScene = new MainScene( mWorkspace ); | ||
| - stage.setScene( mainScene.getScene() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * When a hyperlink website URL is clicked, this method is called to launch | ||
| - * the default browser to the event's location. | ||
| - * | ||
| - * @param event The event called when a hyperlink was clicked. | ||
| - */ | ||
| - @Subscribe | ||
| - public void handle( final HyperlinkOpenEvent event ) { | ||
| - getHostServices().showDocument( event.getUri().toString() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * This will load the lexicon for the user's preferred locale and fire | ||
| - * an event when the all entries in the lexicon have been loaded. | ||
| - */ | ||
| - private void readLexicon() { | ||
| - Lexicon.read( mWorkspace.getLocale() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * When the window is maximized, full screen, or iconified, prevent updating | ||
| - * the window bounds. This is used so that if the user exits the application | ||
| - * when full screen (or maximized), restarting the application will recall | ||
| - * the previous bounds, allowing for continuity of expected behaviour. | ||
| - * | ||
| - * @param stage The window to check for "normal" status. | ||
| - * @return {@code false} when the bounds must not be changed, ergo persisted. | ||
| - */ | ||
| - private BooleanSupplier createBoundsEnabledSupplier( final Stage stage ) { | ||
| - return () -> | ||
| - !(stage.isMaximized() || stage.isFullScreen() || stage.isIconified()); | ||
| - } | ||
| -} | ||
| import com.keenwrite.preferences.Workspace; | ||
| import com.keenwrite.preview.HtmlPreview; | ||
| -import com.keenwrite.processors.html.HtmlPreviewProcessor; | ||
| import com.keenwrite.processors.Processor; | ||
| import com.keenwrite.processors.ProcessorContext; | ||
| import com.keenwrite.processors.ProcessorFactory; | ||
| +import com.keenwrite.processors.html.HtmlPreviewProcessor; | ||
| import com.keenwrite.processors.r.Engine; | ||
| import com.keenwrite.processors.r.RBootstrapController; | ||
| import static com.keenwrite.ExportFormat.NONE; | ||
| -import static com.keenwrite.Launcher.terminate; | ||
| import static com.keenwrite.Messages.get; | ||
| import static com.keenwrite.constants.Constants.*; | ||
| import static com.keenwrite.events.Bus.register; | ||
| import static com.keenwrite.events.StatusEvent.clue; | ||
| import static com.keenwrite.io.MediaType.*; | ||
| import static com.keenwrite.io.MediaType.TypeName.TEXT; | ||
| import static com.keenwrite.io.SysFile.toFile; | ||
| import static com.keenwrite.preferences.AppKeys.*; | ||
| -import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| import static com.keenwrite.processors.ProcessorContext.Mutator; | ||
| import static com.keenwrite.processors.ProcessorContext.builder; | ||
| import static com.keenwrite.processors.ProcessorFactory.createProcessors; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| import static java.awt.Desktop.getDesktop; | ||
| import static java.util.concurrent.Executors.newFixedThreadPool; | ||
| if( closeAll() ) { | ||
| exit(); | ||
| - terminate( 0 ); | ||
| } | ||
| /** | ||
| - * Called by the {@link MainApp} to get a handle on the {@link Scene} | ||
| + * Called by the {@link GuiApp} to get a handle on the {@link Scene} | ||
| * created by an instance of {@link MainScene}. | ||
| * |
| -/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| - * | ||
| - * SPDX-License-Identifier: MIT | ||
| - */ | ||
| -package com.keenwrite.cmdline; | ||
| - | ||
| -import com.fasterxml.jackson.databind.JsonNode; | ||
| -import com.fasterxml.jackson.databind.ObjectMapper; | ||
| -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | ||
| -import com.keenwrite.ExportFormat; | ||
| -import com.keenwrite.processors.ProcessorContext; | ||
| -import com.keenwrite.processors.ProcessorContext.Mutator; | ||
| -import picocli.CommandLine; | ||
| - | ||
| -import java.io.File; | ||
| -import java.io.IOException; | ||
| -import java.nio.file.Files; | ||
| -import java.nio.file.Path; | ||
| -import java.util.HashMap; | ||
| -import java.util.Locale; | ||
| -import java.util.Map; | ||
| -import java.util.Map.Entry; | ||
| -import java.util.concurrent.Callable; | ||
| -import java.util.function.Consumer; | ||
| - | ||
| -import static com.keenwrite.constants.Constants.DIAGRAM_SERVER_NAME; | ||
| -import static java.nio.charset.StandardCharsets.UTF_8; | ||
| - | ||
| -/** | ||
| - * Responsible for mapping command-line arguments to keys that are used by | ||
| - * the application. | ||
| - */ | ||
| -@CommandLine.Command( | ||
| - name = "KeenWrite", | ||
| - mixinStandardHelpOptions = true, | ||
| - description = "Plain text editor for editing with variables" | ||
| -) | ||
| -@SuppressWarnings( "unused" ) | ||
| -public final class Arguments implements Callable<Integer> { | ||
| - @CommandLine.Option( | ||
| - names = { "--all" }, | ||
| - description = | ||
| - "Concatenate files before processing (${DEFAULT-VALUE})", | ||
| - defaultValue = "false" | ||
| - ) | ||
| - private boolean mConcatenate; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--keep-files" }, | ||
| - description = | ||
| - "Retain temporary build files (${DEFAULT-VALUE})", | ||
| - defaultValue = "false" | ||
| - ) | ||
| - private boolean mKeepFiles; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-c", "--chapters" }, | ||
| - description = | ||
| - "Export chapter ranges, no spaces (e.g., -3,5-9,15-)", | ||
| - paramLabel = "String" | ||
| - ) | ||
| - private String mChapters; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--curl-quotes" }, | ||
| - description = | ||
| - "Replace straight quotes with curly quotes (${DEFAULT-VALUE})", | ||
| - defaultValue = "true" | ||
| - ) | ||
| - private boolean mCurlQuotes; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-d", "--debug" }, | ||
| - description = | ||
| - "Enable logging to the console (${DEFAULT-VALUE})", | ||
| - paramLabel = "Boolean", | ||
| - defaultValue = "false" | ||
| - ) | ||
| - private boolean mDebug; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-i", "--input" }, | ||
| - description = | ||
| - "Source document file path", | ||
| - paramLabel = "PATH", | ||
| - defaultValue = "stdin", | ||
| - required = true | ||
| - ) | ||
| - private Path mSourcePath; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--font-dir" }, | ||
| - description = | ||
| - "Directory to specify additional fonts", | ||
| - paramLabel = "String" | ||
| - ) | ||
| - private File mFontDir; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--mode" }, | ||
| - description = | ||
| - "Enable one or more modes when typesetting", | ||
| - paramLabel = "String" | ||
| - ) | ||
| - private String mTypesetMode; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--format-subtype" }, | ||
| - description = | ||
| - "Export TeX subtype for HTML formats: svg, delimited", | ||
| - paramLabel = "String", | ||
| - defaultValue = "svg" | ||
| - ) | ||
| - private String mFormatSubtype; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--cache-dir" }, | ||
| - description = | ||
| - "Directory to store remote resources", | ||
| - paramLabel = "DIR" | ||
| - ) | ||
| - private File mCachesDir; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--image-dir" }, | ||
| - description = | ||
| - "Directory containing images", | ||
| - paramLabel = "DIR" | ||
| - ) | ||
| - private File mImagesDir; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--image-order" }, | ||
| - description = | ||
| - "Comma-separated image order (${DEFAULT-VALUE})", | ||
| - paramLabel = "String", | ||
| - defaultValue = "svg,pdf,png,jpg,tiff" | ||
| - ) | ||
| - private String mImageOrder; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--image-server" }, | ||
| - description = | ||
| - "SVG diagram rendering service (${DEFAULT-VALUE})", | ||
| - paramLabel = "String", | ||
| - defaultValue = DIAGRAM_SERVER_NAME | ||
| - ) | ||
| - private String mImageServer; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--locale" }, | ||
| - description = | ||
| - "Set localization (${DEFAULT-VALUE})", | ||
| - paramLabel = "String", | ||
| - defaultValue = "en" | ||
| - ) | ||
| - private String mLocale; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-m", "--metadata" }, | ||
| - description = | ||
| - "Map metadata keys to values, variable names allowed", | ||
| - paramLabel = "key=value" | ||
| - ) | ||
| - private Map<String, String> mMetadata; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-o", "--output" }, | ||
| - description = | ||
| - "Destination document file path", | ||
| - paramLabel = "PATH", | ||
| - defaultValue = "stdout", | ||
| - required = true | ||
| - ) | ||
| - private Path mTargetPath; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-q", "--quiet" }, | ||
| - description = | ||
| - "Suppress all status messages (${DEFAULT-VALUE})", | ||
| - defaultValue = "false" | ||
| - ) | ||
| - private boolean mQuiet; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--r-dir" }, | ||
| - description = | ||
| - "R working directory", | ||
| - paramLabel = "DIR" | ||
| - ) | ||
| - private Path mRWorkingDir; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--r-script" }, | ||
| - description = | ||
| - "R bootstrap script file path", | ||
| - paramLabel = "PATH" | ||
| - ) | ||
| - private Path mRScriptPath; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-s", "--set" }, | ||
| - description = | ||
| - "Set (or override) a document variable value", | ||
| - paramLabel = "key=value" | ||
| - ) | ||
| - private Map<String, String> mOverrides; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--sigil-opening" }, | ||
| - description = | ||
| - "Starting sigil for variable names (${DEFAULT-VALUE})", | ||
| - paramLabel = "String", | ||
| - defaultValue = "{{" | ||
| - ) | ||
| - private String mSigilBegan; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--sigil-closing" }, | ||
| - description = | ||
| - "Ending sigil for variable names (${DEFAULT-VALUE})", | ||
| - paramLabel = "String", | ||
| - defaultValue = "}}" | ||
| - ) | ||
| - private String mSigilEnded; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "--theme-dir" }, | ||
| - description = | ||
| - "Theme directory", | ||
| - paramLabel = "DIR" | ||
| - ) | ||
| - private Path mThemesDir; | ||
| - | ||
| - @CommandLine.Option( | ||
| - names = { "-v", "--variables" }, | ||
| - description = | ||
| - "Variables file path", | ||
| - paramLabel = "PATH" | ||
| - ) | ||
| - private Path mPathVariables; | ||
| - | ||
| - private final Consumer<Arguments> mLauncher; | ||
| - | ||
| - public Arguments( final Consumer<Arguments> launcher ) { | ||
| - mLauncher = launcher; | ||
| - } | ||
| - | ||
| - public ProcessorContext createProcessorContext() | ||
| - throws IOException { | ||
| - final var definitions = parse( mPathVariables ); | ||
| - final var format = ExportFormat.valueFrom( mTargetPath, mFormatSubtype ); | ||
| - final var locale = lookupLocale( mLocale ); | ||
| - final var rScript = read( mRScriptPath ); | ||
| - | ||
| - return ProcessorContext | ||
| - .builder() | ||
| - .with( Mutator::setSourcePath, mSourcePath ) | ||
| - .with( Mutator::setTargetPath, mTargetPath ) | ||
| - .with( Mutator::setThemeDir, () -> mThemesDir ) | ||
| - .with( Mutator::setCacheDir, () -> mCachesDir ) | ||
| - .with( Mutator::setImageDir, () -> mImagesDir ) | ||
| - .with( Mutator::setImageServer, () -> mImageServer ) | ||
| - .with( Mutator::setImageOrder, () -> mImageOrder ) | ||
| - .with( Mutator::setFontDir, () -> mFontDir ) | ||
| - .with( Mutator::setModesEnabled, () -> mTypesetMode ) | ||
| - .with( Mutator::setExportFormat, format ) | ||
| - .with( Mutator::setDefinitions, () -> definitions ) | ||
| - .with( Mutator::setMetadata, () -> mMetadata ) | ||
| - .with( Mutator::setOverrides, () -> mOverrides ) | ||
| - .with( Mutator::setLocale, () -> locale ) | ||
| - .with( Mutator::setConcatenate, () -> mConcatenate ) | ||
| - .with( Mutator::setChapters, () -> mChapters ) | ||
| - .with( Mutator::setSigilBegan, () -> mSigilBegan ) | ||
| - .with( Mutator::setSigilEnded, () -> mSigilEnded ) | ||
| - .with( Mutator::setRScript, () -> rScript ) | ||
| - .with( Mutator::setRWorkingDir, () -> mRWorkingDir ) | ||
| - .with( Mutator::setCurlQuotes, () -> mCurlQuotes ) | ||
| - .with( Mutator::setAutoRemove, () -> !mKeepFiles ) | ||
| - .build(); | ||
| - } | ||
| - | ||
| - public boolean quiet() { | ||
| - return mQuiet; | ||
| - } | ||
| - | ||
| - public boolean debug() { | ||
| - return mDebug; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Launches the main application window. This is called when not running | ||
| - * in headless mode. | ||
| - * | ||
| - * @return {@code 0} | ||
| - * @throws Exception The application encountered an unrecoverable error. | ||
| - */ | ||
| - @Override | ||
| - public Integer call() throws Exception { | ||
| - mLauncher.accept( this ); | ||
| - return 0; | ||
| - } | ||
| - | ||
| - private static String read( final Path path ) throws IOException { | ||
| - return path == null ? "" : Files.readString( path, UTF_8 ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Parses the given YAML document into a map of key-value pairs. | ||
| - * | ||
| - * @param vars Variable definition file to read, may be {@code null} if no | ||
| - * variables are specified. | ||
| - * @return A non-interpolated variable map, or an empty map. | ||
| - * @throws IOException Could not read the variable definition file | ||
| - */ | ||
| - private static Map<String, String> parse( final Path vars ) | ||
| - throws IOException { | ||
| - final var map = new HashMap<String, String>(); | ||
| - | ||
| - if( vars != null ) { | ||
| - final var yaml = read( vars ); | ||
| - final var factory = new YAMLFactory(); | ||
| - final var json = new ObjectMapper( factory ).readTree( yaml ); | ||
| - | ||
| - parse( json, "", map ); | ||
| - } | ||
| - | ||
| - return map; | ||
| - } | ||
| - | ||
| - private static void parse( | ||
| - final JsonNode json, final String parent, final Map<String, String> map ) { | ||
| - assert json != null; | ||
| - assert parent != null; | ||
| - assert map != null; | ||
| - | ||
| - json.fields().forEachRemaining( node -> parse( node, parent, map ) ); | ||
| - } | ||
| - | ||
| - private static void parse( | ||
| - final Entry<String, JsonNode> node, | ||
| - final String parent, | ||
| - final Map<String, String> map ) { | ||
| - assert node != null; | ||
| - assert parent != null; | ||
| - assert map != null; | ||
| - | ||
| - final var jsonNode = node.getValue(); | ||
| - final var keyName = String.format( "%s.%s", parent, node.getKey() ); | ||
| +/* Copyright 2023-2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.cmdline; | ||
| + | ||
| +import com.fasterxml.jackson.databind.JsonNode; | ||
| +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | ||
| +import com.keenwrite.ExportFormat; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| +import com.keenwrite.processors.ProcessorContext.Mutator; | ||
| +import picocli.CommandLine; | ||
| +import picocli.CommandLine.ParseResult; | ||
| + | ||
| +import java.io.File; | ||
| +import java.io.IOException; | ||
| +import java.nio.file.Files; | ||
| +import java.nio.file.Path; | ||
| +import java.util.HashMap; | ||
| +import java.util.Locale; | ||
| +import java.util.Map; | ||
| +import java.util.Map.Entry; | ||
| +import java.util.concurrent.Callable; | ||
| +import java.util.function.Consumer; | ||
| + | ||
| +import static com.keenwrite.constants.Constants.DIAGRAM_SERVER_NAME; | ||
| +import static java.lang.String.format; | ||
| +import static java.nio.charset.StandardCharsets.UTF_8; | ||
| + | ||
| +/** | ||
| + * Responsible for mapping command-line arguments to keys that are used by | ||
| + * the application. | ||
| + */ | ||
| +@CommandLine.Command( | ||
| + name = "KeenWrite", | ||
| + mixinStandardHelpOptions = true, | ||
| + description = "Plain text editor for editing with variables" | ||
| +) | ||
| +@SuppressWarnings( "unused" ) | ||
| +public final class Arguments implements Callable<Integer> { | ||
| + @CommandLine.Spec | ||
| + CommandLine.Model.CommandSpec mSpecifications; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--all" }, | ||
| + description = | ||
| + "Concatenate files before processing (${DEFAULT-VALUE})", | ||
| + defaultValue = "false" | ||
| + ) | ||
| + private boolean mConcatenate; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--keep-files" }, | ||
| + description = | ||
| + "Retain temporary build files (${DEFAULT-VALUE})", | ||
| + defaultValue = "false" | ||
| + ) | ||
| + private boolean mKeepFiles; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-c", "--chapters" }, | ||
| + description = | ||
| + "Export chapter ranges, no spaces (e.g., -3,5-9,15-)", | ||
| + paramLabel = "String" | ||
| + ) | ||
| + private String mChapters; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--curl-quotes" }, | ||
| + description = | ||
| + "Replace straight quotes with curly quotes (${DEFAULT-VALUE})", | ||
| + defaultValue = "true" | ||
| + ) | ||
| + private boolean mCurlQuotes; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-d", "--debug" }, | ||
| + description = | ||
| + "Enable logging to the console (${DEFAULT-VALUE})", | ||
| + paramLabel = "Boolean", | ||
| + defaultValue = "false" | ||
| + ) | ||
| + private boolean mDebug; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-i", "--input" }, | ||
| + description = | ||
| + "Source document file path", | ||
| + paramLabel = "PATH", | ||
| + defaultValue = "stdin", | ||
| + required = true | ||
| + ) | ||
| + private Path mSourcePath; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--font-dir" }, | ||
| + description = | ||
| + "Directory to specify additional fonts", | ||
| + paramLabel = "String" | ||
| + ) | ||
| + private File mFontDir; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--mode" }, | ||
| + description = | ||
| + "Enable one or more modes when typesetting", | ||
| + paramLabel = "String" | ||
| + ) | ||
| + private String mTypesetMode; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--format-subtype" }, | ||
| + description = | ||
| + "Export TeX subtype for HTML formats: svg, delimited", | ||
| + paramLabel = "String", | ||
| + defaultValue = "svg" | ||
| + ) | ||
| + private String mFormatSubtype; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--cache-dir" }, | ||
| + description = | ||
| + "Directory to store remote resources", | ||
| + paramLabel = "DIR" | ||
| + ) | ||
| + private File mCachesDir; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--image-dir" }, | ||
| + description = | ||
| + "Directory containing images", | ||
| + paramLabel = "DIR" | ||
| + ) | ||
| + private File mImagesDir; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--image-order" }, | ||
| + description = | ||
| + "Comma-separated image order (${DEFAULT-VALUE})", | ||
| + paramLabel = "String", | ||
| + defaultValue = "svg,pdf,png,jpg,tiff" | ||
| + ) | ||
| + private String mImageOrder; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--image-server" }, | ||
| + description = | ||
| + "SVG diagram rendering service (${DEFAULT-VALUE})", | ||
| + paramLabel = "String", | ||
| + defaultValue = DIAGRAM_SERVER_NAME | ||
| + ) | ||
| + private String mImageServer; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--locale" }, | ||
| + description = | ||
| + "Set localization (${DEFAULT-VALUE})", | ||
| + paramLabel = "String", | ||
| + defaultValue = "en" | ||
| + ) | ||
| + private String mLocale; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-m", "--metadata" }, | ||
| + description = | ||
| + "Map metadata keys to values, variable names allowed", | ||
| + paramLabel = "key=value" | ||
| + ) | ||
| + private Map<String, String> mMetadata; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-o", "--output" }, | ||
| + description = | ||
| + "Destination document file path", | ||
| + paramLabel = "PATH", | ||
| + defaultValue = "stdout", | ||
| + required = true | ||
| + ) | ||
| + private Path mTargetPath; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-q", "--quiet" }, | ||
| + description = | ||
| + "Suppress all status messages (${DEFAULT-VALUE})", | ||
| + defaultValue = "false" | ||
| + ) | ||
| + private boolean mQuiet; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--r-dir" }, | ||
| + description = | ||
| + "R working directory", | ||
| + paramLabel = "DIR" | ||
| + ) | ||
| + private Path mRWorkingDir; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--r-script" }, | ||
| + description = | ||
| + "R bootstrap script file path", | ||
| + paramLabel = "PATH" | ||
| + ) | ||
| + private Path mRScriptPath; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-s", "--set" }, | ||
| + description = | ||
| + "Set (or override) a document variable value", | ||
| + paramLabel = "key=value" | ||
| + ) | ||
| + private Map<String, String> mOverrides; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--sigil-opening" }, | ||
| + description = | ||
| + "Starting sigil for variable names (${DEFAULT-VALUE})", | ||
| + paramLabel = "String", | ||
| + defaultValue = "{{" | ||
| + ) | ||
| + private String mSigilBegan; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--sigil-closing" }, | ||
| + description = | ||
| + "Ending sigil for variable names (${DEFAULT-VALUE})", | ||
| + paramLabel = "String", | ||
| + defaultValue = "}}" | ||
| + ) | ||
| + private String mSigilEnded; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "--theme-dir" }, | ||
| + description = | ||
| + "Theme directory", | ||
| + paramLabel = "DIR" | ||
| + ) | ||
| + private Path mThemesDir; | ||
| + | ||
| + @CommandLine.Option( | ||
| + names = { "-v", "--variables" }, | ||
| + description = | ||
| + "Variables file path", | ||
| + paramLabel = "PATH" | ||
| + ) | ||
| + private Path mPathVariables; | ||
| + | ||
| + private final Consumer<Arguments> mLauncher; | ||
| + | ||
| + public Arguments( final Consumer<Arguments> launcher ) { | ||
| + mLauncher = launcher; | ||
| + } | ||
| + | ||
| + public ProcessorContext createProcessorContext() | ||
| + throws IOException { | ||
| + final var definitions = parse( mPathVariables ); | ||
| + final var format = ExportFormat.valueFrom( mTargetPath, mFormatSubtype ); | ||
| + final var locale = lookupLocale( mLocale ); | ||
| + final var rScript = read( mRScriptPath ); | ||
| + | ||
| + return ProcessorContext | ||
| + .builder() | ||
| + .with( Mutator::setSourcePath, mSourcePath ) | ||
| + .with( Mutator::setTargetPath, mTargetPath ) | ||
| + .with( Mutator::setThemeDir, () -> mThemesDir ) | ||
| + .with( Mutator::setCacheDir, () -> mCachesDir ) | ||
| + .with( Mutator::setImageDir, () -> mImagesDir ) | ||
| + .with( Mutator::setImageServer, () -> mImageServer ) | ||
| + .with( Mutator::setImageOrder, () -> mImageOrder ) | ||
| + .with( Mutator::setFontDir, () -> mFontDir ) | ||
| + .with( Mutator::setModesEnabled, () -> mTypesetMode ) | ||
| + .with( Mutator::setExportFormat, format ) | ||
| + .with( Mutator::setDefinitions, () -> definitions ) | ||
| + .with( Mutator::setMetadata, () -> mMetadata ) | ||
| + .with( Mutator::setOverrides, () -> mOverrides ) | ||
| + .with( Mutator::setLocale, () -> locale ) | ||
| + .with( Mutator::setConcatenate, () -> mConcatenate ) | ||
| + .with( Mutator::setChapters, () -> mChapters ) | ||
| + .with( Mutator::setSigilBegan, () -> mSigilBegan ) | ||
| + .with( Mutator::setSigilEnded, () -> mSigilEnded ) | ||
| + .with( Mutator::setRScript, () -> rScript ) | ||
| + .with( Mutator::setRWorkingDir, () -> mRWorkingDir ) | ||
| + .with( Mutator::setCurlQuotes, () -> mCurlQuotes ) | ||
| + .with( Mutator::setAutoRemove, () -> !mKeepFiles ) | ||
| + .build(); | ||
| + } | ||
| + | ||
| + public boolean quiet() { | ||
| + return mQuiet; | ||
| + } | ||
| + | ||
| + public boolean debug() { | ||
| + return mDebug; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Launches the main application window. This is called when not running | ||
| + * in headless mode. | ||
| + * | ||
| + * @return {@code 0} | ||
| + * @throws Exception The application encountered an unrecoverable error. | ||
| + */ | ||
| + @Override | ||
| + public Integer call() throws Exception { | ||
| + mLauncher.accept( this ); | ||
| + return 0; | ||
| + } | ||
| + | ||
| + public void iterate( | ||
| + final ParseResult parseResult, | ||
| + final Consumer<String> consumer | ||
| + ) { | ||
| + final var options = mSpecifications.options(); | ||
| + | ||
| + for( final var opt : options ) { | ||
| + consumer.accept( format( "%s=%s", opt.longestName(), opt.getValue() ) ); | ||
| + } | ||
| + } | ||
| + | ||
| + private static String read( final Path path ) throws IOException { | ||
| + return path == null ? "" : Files.readString( path, UTF_8 ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Parses the given YAML document into a map of key-value pairs. | ||
| + * | ||
| + * @param vars Variable definition file to read, may be {@code null} if no | ||
| + * variables are specified. | ||
| + * @return A non-interpolated variable map, or an empty map. | ||
| + * @throws IOException Could not read the variable definition file | ||
| + */ | ||
| + private static Map<String, String> parse( final Path vars ) | ||
| + throws IOException { | ||
| + final var map = new HashMap<String, String>(); | ||
| + | ||
| + if( vars != null ) { | ||
| + final var yaml = read( vars ); | ||
| + final var factory = new YAMLFactory(); | ||
| + final var json = new ObjectMapper( factory ).readTree( yaml ); | ||
| + | ||
| + parse( json, "", map ); | ||
| + } | ||
| + | ||
| + return map; | ||
| + } | ||
| + | ||
| + private static void parse( | ||
| + final JsonNode json, final String parent, final Map<String, String> map ) { | ||
| + assert json != null; | ||
| + assert parent != null; | ||
| + assert map != null; | ||
| + | ||
| + json.fields().forEachRemaining( node -> parse( node, parent, map ) ); | ||
| + } | ||
| + | ||
| + private static void parse( | ||
| + final Entry<String, JsonNode> node, | ||
| + final String parent, | ||
| + final Map<String, String> map ) { | ||
| + assert node != null; | ||
| + assert parent != null; | ||
| + assert map != null; | ||
| + | ||
| + final var jsonNode = node.getValue(); | ||
| + final var keyName = format( "%s.%s", parent, node.getKey() ); | ||
| if( jsonNode.isValueNode() ) { |
| +/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| package com.keenwrite.cmdline; | ||
| import com.keenwrite.AppCommands; | ||
| import com.keenwrite.events.StatusEvent; | ||
| import org.greenrobot.eventbus.Subscribe; | ||
| + | ||
| +import java.io.PrintStream; | ||
| import static com.keenwrite.events.Bus.register; | ||
| import static java.lang.String.format; | ||
| /** | ||
| * Responsible for running the application in headless mode. | ||
| */ | ||
| public class HeadlessApp { | ||
| - | ||
| /** | ||
| * Contains directives that control text file processing. | ||
| */ | ||
| private final Arguments mArgs; | ||
| + | ||
| + /** | ||
| + * Where to write error messages. | ||
| + */ | ||
| + private final PrintStream mErrStream; | ||
| /** | ||
| * Creates a new command-line version of the application. | ||
| * | ||
| - * @param args The post-processed command-line arguments. | ||
| + * @param args The post-processed command-line arguments. | ||
| + * @param errStream Where to write error messages. | ||
| */ | ||
| - public HeadlessApp( final Arguments args ) { | ||
| + public HeadlessApp( final Arguments args, final PrintStream errStream ) { | ||
| assert args != null; | ||
| mArgs = args; | ||
| + mErrStream = errStream; | ||
| register( this ); | ||
| - AppCommands.run( mArgs ); | ||
| } | ||
| final var msg = format( "%s%s", event, problem ); | ||
| - System.out.println( msg ); | ||
| + mErrStream.println( msg ); | ||
| } | ||
| + } | ||
| + | ||
| + private void run() { | ||
| + AppCommands.run( mArgs ); | ||
| } | ||
| /** | ||
| * Entry point for running the application in headless mode. | ||
| * | ||
| - * @param args The parsed command-line arguments. | ||
| + * @param args The parsed command-line arguments. | ||
| + * @param errStream Where to write error messages. | ||
| */ | ||
| - @SuppressWarnings( "ConfusingMainMethod" ) | ||
| - public static void main( final Arguments args ) { | ||
| - new HeadlessApp( args ); | ||
| + public static void run( | ||
| + final Arguments args, | ||
| + final PrintStream errStream ) { | ||
| + final var app = new HeadlessApp( args, errStream ); | ||
| + app.run(); | ||
| } | ||
| } | ||
| * Prevent instantiation. | ||
| */ | ||
| - private Constants() { | ||
| - } | ||
| + private Constants() {} | ||
| /** |
| import java.util.regex.Pattern; | ||
| -import static com.keenwrite.MainApp.keyDown; | ||
| +import static com.keenwrite.GuiApp.keyDown; | ||
| import static com.keenwrite.constants.Constants.*; | ||
| import static com.keenwrite.events.StatusEvent.clue; |
| * Use {@link #installTrustManager()}. | ||
| */ | ||
| - private PermissiveCertificate() { | ||
| - } | ||
| + private PermissiveCertificate() {} | ||
| } | ||
| */ | ||
| public abstract class ManagerOutputPane extends InstallerPane { | ||
| - private final static String PROP_EXECUTOR = | ||
| + private static final String PROP_EXECUTOR = | ||
| ManagerOutputPane.class.getCanonicalName(); | ||
| final var actualExtension = valueFrom( media ).getExtension(); | ||
| final var expectedExtension = getExtension( image.toString() ); | ||
| - System.out.println( String.format( "%s%s", image, " -> \{media}" ) ); | ||
| + System.out.printf( "%s -> %s%n", image, media ); | ||
| assertEquals( expectedExtension, actualExtension ); |
| Author | DaveJarvis <email> |
|---|---|
| Date | 2024-10-05 14:49:59 GMT-0700 |
| Commit | c2fb90a8d0c90e0d31b412476b9bdd105521de5b |
| Parent | f005e9b |
| Delta | 650 lines added, 615 lines removed, 35-line increase |