| Author | Dave Jarvis <email> |
|---|---|
| Date | 2023-01-03 16:48:34 GMT-0800 |
| Commit | fd5d7030d37502bbc35d5303df1a10f9b610e288 |
| Parent | bb4c75e |
| Delta | 573 lines added, 1 line removed, 572-line increase |
| return ProcessorContext | ||
| .builder() | ||
| - .with( ProcessorContext.Mutator::setInputPath, inputPath ) | ||
| + .with( ProcessorContext.Mutator::setSourcePath, inputPath ) | ||
| .with( ProcessorContext.Mutator::setExportFormat, XHTML_TEX ) | ||
| .with( ProcessorContext.Mutator::setCaret, () -> Caret.builder().build() ) |
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.io; | ||
| + | ||
| +import org.junit.jupiter.api.Test; | ||
| + | ||
| +import java.io.FileNotFoundException; | ||
| + | ||
| +import static org.junit.jupiter.api.Assertions.*; | ||
| + | ||
| +class UserDataDirTest { | ||
| + @Test | ||
| + void test_Unix_GetAppDirectory_DirectoryExists() | ||
| + throws FileNotFoundException { | ||
| + final var path = UserDataDir.getAppPath( "test" ); | ||
| + final var file = path.toFile(); | ||
| + | ||
| + assertTrue( file.exists() ); | ||
| + assertTrue( file.delete() ); | ||
| + assertFalse( file.exists() ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.util; | ||
| + | ||
| +import java.time.Duration; | ||
| + | ||
| +import static java.lang.String.format; | ||
| +import static java.util.concurrent.TimeUnit.*; | ||
| + | ||
| +/** | ||
| + * Responsible for time-related functionality. | ||
| + */ | ||
| +public final class Time { | ||
| + /** | ||
| + * Converts an elapsed time to a human-readable format (hours, minutes, | ||
| + * seconds, and milliseconds). | ||
| + * | ||
| + * @param duration An elapsed time. | ||
| + * @return Human-readable elapsed time. | ||
| + */ | ||
| + public static String toElapsedTime( final Duration duration ) { | ||
| + final var elapsed = duration.toMillis(); | ||
| + final var hours = MILLISECONDS.toHours( elapsed ); | ||
| + final var eHours = elapsed - HOURS.toMillis( hours ); | ||
| + final var minutes = MILLISECONDS.toMinutes( eHours ); | ||
| + final var eMinutes = eHours - MINUTES.toMillis( minutes ); | ||
| + final var seconds = MILLISECONDS.toSeconds( eMinutes ); | ||
| + final var eSeconds = eMinutes - SECONDS.toMillis( seconds ); | ||
| + final var milliseconds = MILLISECONDS.toMillis( eSeconds ); | ||
| + | ||
| + return format( "%02d:%02d:%02d.%03d", | ||
| + hours, minutes, seconds, milliseconds ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +import com.keenwrite.typesetting.containerization.ContainerManager; | ||
| + | ||
| +/** | ||
| + * Responsible for initializing the container manager on all platforms except | ||
| + * for Linux. | ||
| + */ | ||
| +public final class ManagerInitializationPane extends ManagerOutputPane { | ||
| + | ||
| + private static final String PREFIX = | ||
| + "Wizard.typesetter.all.3.install.container"; | ||
| + | ||
| + public ManagerInitializationPane() { | ||
| + super( | ||
| + PREFIX + ".correct", | ||
| + PREFIX + ".missing", | ||
| + ContainerManager::start, | ||
| + 35 | ||
| + ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public String getHeaderKey() { | ||
| + return PREFIX + ".header"; | ||
| + } | ||
| +} | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +import com.keenwrite.io.CommandNotFoundException; | ||
| +import com.keenwrite.typesetting.containerization.ContainerManager; | ||
| +import com.keenwrite.typesetting.containerization.StreamProcessor; | ||
| +import javafx.concurrent.Task; | ||
| +import javafx.scene.control.TextArea; | ||
| +import javafx.scene.layout.BorderPane; | ||
| +import org.apache.commons.lang3.function.FailableBiConsumer; | ||
| +import org.controlsfx.dialog.Wizard; | ||
| + | ||
| +import static com.keenwrite.Messages.get; | ||
| +import static com.keenwrite.io.StreamGobbler.gobble; | ||
| + | ||
| +/** | ||
| + * Responsible for showing the output from running commands against a container | ||
| + * manager. There are a few installation steps that run different commands | ||
| + * against the installer, which are platform-specific and cannot be merged. | ||
| + * Common functionality between them is codified in this class. | ||
| + */ | ||
| +public abstract class ManagerOutputPane extends InstallerPane { | ||
| + private final String PROP_EXECUTOR = getClass().getCanonicalName(); | ||
| + | ||
| + private final String mCorrectKey; | ||
| + private final String mMissingKey; | ||
| + private final FailableBiConsumer | ||
| + <ContainerManager, StreamProcessor, CommandNotFoundException> mFc; | ||
| + private final ContainerManager mContainer; | ||
| + private final TextArea mTextArea; | ||
| + | ||
| + public ManagerOutputPane( | ||
| + final String correctKey, | ||
| + final String missingKey, | ||
| + final FailableBiConsumer | ||
| + <ContainerManager, StreamProcessor, CommandNotFoundException> fc, | ||
| + final int cols | ||
| + ) { | ||
| + mFc = fc; | ||
| + mCorrectKey = correctKey; | ||
| + mMissingKey = missingKey; | ||
| + mTextArea = textArea( 5, cols ); | ||
| + mContainer = createContainer(); | ||
| + | ||
| + final var borderPane = new BorderPane(); | ||
| + final var titledPane = titledPane( "Output", mTextArea ); | ||
| + | ||
| + borderPane.setBottom( titledPane ); | ||
| + setContent( borderPane ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void onEnteringPage( final Wizard wizard ) { | ||
| + disableNext( true ); | ||
| + | ||
| + try { | ||
| + final var properties = wizard.getProperties(); | ||
| + final var thread = properties.get( PROP_EXECUTOR ); | ||
| + | ||
| + if( thread instanceof Thread executor && executor.isAlive() ) { | ||
| + return; | ||
| + } | ||
| + | ||
| + final Task<Void> task = createTask( () -> { | ||
| + mFc.accept( | ||
| + mContainer, | ||
| + input -> gobble( input, line -> append( mTextArea, line ) ) | ||
| + ); | ||
| + properties.remove( thread ); | ||
| + return null; | ||
| + } ); | ||
| + | ||
| + task.setOnSucceeded( event -> { | ||
| + append( mTextArea, get( mCorrectKey ) ); | ||
| + properties.remove( thread ); | ||
| + disableNext( false ); | ||
| + } ); | ||
| + task.setOnFailed( event -> append( mTextArea, get( mMissingKey ) ) ); | ||
| + task.setOnCancelled( event -> append( mTextArea, get( mMissingKey ) ) ); | ||
| + | ||
| + final var executor = createThread( task ); | ||
| + properties.put( PROP_EXECUTOR, executor ); | ||
| + executor.start(); | ||
| + } catch( final Exception e ) { | ||
| + throw new RuntimeException( e ); | ||
| + } | ||
| + } | ||
| +} | ||
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +import static com.keenwrite.typesetting.containerization.Podman.CONTAINER_NAME; | ||
| + | ||
| +/** | ||
| + * Responsible for installing the typesetter's image via the container manager. | ||
| + */ | ||
| +public final class TypesetterImageDownloadPane extends ManagerOutputPane { | ||
| + private static final String PREFIX = | ||
| + "Wizard.typesetter.all.4.download.image"; | ||
| + | ||
| + public TypesetterImageDownloadPane() { | ||
| + super( | ||
| + PREFIX + ".correct", | ||
| + PREFIX + ".missing", | ||
| + (container, processor) -> container.pull( processor, CONTAINER_NAME ), | ||
| + 45 | ||
| + ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public String getHeaderKey() { | ||
| + return PREFIX + ".header"; | ||
| + } | ||
| +} | ||
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +import com.keenwrite.io.UserDataDir; | ||
| +import com.keenwrite.io.Zip; | ||
| +import com.keenwrite.preferences.Workspace; | ||
| +import javafx.collections.ObservableMap; | ||
| +import org.controlsfx.dialog.Wizard; | ||
| + | ||
| +import java.io.File; | ||
| +import java.io.IOException; | ||
| + | ||
| +import static com.keenwrite.Messages.get; | ||
| +import static com.keenwrite.events.StatusEvent.clue; | ||
| +import static com.keenwrite.preferences.AppKeys.KEY_TYPESET_CONTEXT_THEMES_PATH; | ||
| + | ||
| +/** | ||
| + * Responsible for downloading themes into the application's data directory. | ||
| + * The data directory differs between platforms, which is handled | ||
| + * transparently by the {@link UserDataDir} class. | ||
| + */ | ||
| +public class TypesetterThemesDownloadPane extends AbstractDownloadPane { | ||
| + private static final String PREFIX = | ||
| + "Wizard.typesetter.all.5.download.themes"; | ||
| + | ||
| + private final Workspace mWorkspace; | ||
| + | ||
| + public TypesetterThemesDownloadPane( final Workspace workspace ) { | ||
| + assert workspace != null; | ||
| + mWorkspace = workspace; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void onEnteringPage( final Wizard wizard ) { | ||
| + // Delete the target themes file to force re-download so that unzipping | ||
| + // the file takes place. This side-steps checksum validation, which would | ||
| + // be best implemented after downloading. | ||
| + deleteTarget(); | ||
| + super.onEnteringPage( wizard ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected void onDownloadSucceeded( | ||
| + final String threadName, final ObservableMap<Object, Object> properties ) { | ||
| + super.onDownloadSucceeded( threadName, properties ); | ||
| + | ||
| + try { | ||
| + process( getTarget() ); | ||
| + } catch( final Exception ex ) { | ||
| + clue( ex ); | ||
| + } | ||
| + } | ||
| + | ||
| + private void process( final File target ) throws IOException { | ||
| + Zip.extract( target.toPath() ); | ||
| + | ||
| + // Replace the default themes directory with the downloaded version. | ||
| + final var root = Zip.root( target.toPath() ).toFile(); | ||
| + | ||
| + // Make sure the typesetter will know where to find the themes. | ||
| + mWorkspace.fileProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ).set( root ); | ||
| + mWorkspace.save(); | ||
| + | ||
| + // The themes pack is no longer needed. | ||
| + deleteTarget(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String getPrefix() { | ||
| + return PREFIX; | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String getChecksum() { | ||
| + return get( "Wizard.typesetter.themes.checksum" ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +/** | ||
| + * Responsible for installing the container manager for any operating system | ||
| + * that was not explicitly detected. | ||
| + */ | ||
| +public final class UniversalManagerInstallPane extends InstallerPane { | ||
| + private static final String PREFIX = | ||
| + "Wizard.typesetter.all.2.install.container"; | ||
| + | ||
| + public UniversalManagerInstallPane() { } | ||
| + | ||
| + @Override | ||
| + protected String getHeaderKey() { | ||
| + return PREFIX + ".header"; | ||
| + } | ||
| +} | ||
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +import com.keenwrite.ui.clipboard.Clipboard; | ||
| +import javafx.geometry.Insets; | ||
| +import javafx.scene.Node; | ||
| +import javafx.scene.control.ButtonBar; | ||
| +import javafx.scene.control.ComboBox; | ||
| +import javafx.scene.control.TextArea; | ||
| +import javafx.scene.layout.BorderPane; | ||
| +import javafx.scene.layout.HBox; | ||
| +import javafx.scene.layout.VBox; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| +import static com.keenwrite.Messages.get; | ||
| +import static com.keenwrite.Messages.getInt; | ||
| +import static java.lang.String.format; | ||
| +import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC; | ||
| + | ||
| +public final class UnixManagerInstallPane extends InstallerPane { | ||
| + private static final String PREFIX = | ||
| + "Wizard.typesetter.unix.2.install.container"; | ||
| + | ||
| + private final TextArea mCommands = textArea( 2, 40 ); | ||
| + | ||
| + public UnixManagerInstallPane() { | ||
| + final var titledPane = titledPane( "Run", mCommands ); | ||
| + final var comboBox = createUnixOsCommandMap(); | ||
| + final var selection = comboBox.getSelectionModel(); | ||
| + selection | ||
| + .selectedItemProperty() | ||
| + .addListener( ( c, o, n ) -> mCommands.setText( n.command() ) ); | ||
| + | ||
| + // Auto-select if running on macOS. | ||
| + if( IS_OS_MAC ) { | ||
| + final var items = comboBox.getItems(); | ||
| + | ||
| + for( final var item : items ) { | ||
| + if( "macOS".equalsIgnoreCase( item.name ) ) { | ||
| + selection.select( item ); | ||
| + break; | ||
| + } | ||
| + } | ||
| + } | ||
| + else { | ||
| + selection.select( 0 ); | ||
| + } | ||
| + | ||
| + final var distro = label( PREFIX + ".os" ); | ||
| + distro.setText( distro.getText() + ":" ); | ||
| + distro.setPadding( new Insets( PAD / 2.0, PAD, 0, 0 ) ); | ||
| + | ||
| + final var hbox = new HBox(); | ||
| + hbox.getChildren().add( distro ); | ||
| + hbox.getChildren().add( comboBox ); | ||
| + hbox.setPadding( new Insets( 0, 0, PAD, 0 ) ); | ||
| + | ||
| + final var stepsPane = new VBox(); | ||
| + final var steps = stepsPane.getChildren(); | ||
| + steps.add( label( PREFIX + ".step.0" ) ); | ||
| + steps.add( spacer() ); | ||
| + steps.add( label( PREFIX + ".step.1" ) ); | ||
| + steps.add( label( PREFIX + ".step.2" ) ); | ||
| + steps.add( label( PREFIX + ".step.3" ) ); | ||
| + steps.add( label( PREFIX + ".step.4" ) ); | ||
| + steps.add( spacer() ); | ||
| + | ||
| + steps.add( flowPane( | ||
| + label( PREFIX + ".details.prefix" ), | ||
| + hyperlink( PREFIX + ".details.link" ), | ||
| + label( PREFIX + ".details.suffix" ) | ||
| + ) ); | ||
| + steps.add( spacer() ); | ||
| + | ||
| + final var border = new BorderPane(); | ||
| + border.setTop( stepsPane ); | ||
| + border.setCenter( hbox ); | ||
| + border.setBottom( titledPane ); | ||
| + | ||
| + setContent( border ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Node createButtonBar() { | ||
| + final var node = super.createButtonBar(); | ||
| + final var layout = new BorderPane(); | ||
| + final var copyButton = button( PREFIX + ".copy.began" ); | ||
| + | ||
| + // Change the label to indicate clipboard is updated. | ||
| + copyButton.setOnAction( event -> { | ||
| + Clipboard.write( mCommands.getText() ); | ||
| + copyButton.setText( get( PREFIX + ".copy.ended" ) ); | ||
| + } ); | ||
| + | ||
| + if( node instanceof ButtonBar buttonBar ) { | ||
| + copyButton.setMinWidth( buttonBar.getButtonMinWidth() ); | ||
| + } | ||
| + | ||
| + layout.setPadding( new Insets( PAD, PAD, PAD, PAD ) ); | ||
| + layout.setLeft( copyButton ); | ||
| + layout.setRight( node ); | ||
| + | ||
| + return layout; | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String getHeaderKey() { | ||
| + return PREFIX + ".header"; | ||
| + } | ||
| + | ||
| + private record UnixOsCommand( String name, String command ) | ||
| + implements Comparable<UnixOsCommand> { | ||
| + @Override | ||
| + public int compareTo( | ||
| + final @NotNull UnixOsCommand other ) { | ||
| + return toString().compareToIgnoreCase( other.toString() ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public String toString() { | ||
| + return name; | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates a collection of *nix distributions mapped to instructions for users | ||
| + * to run in a terminal. | ||
| + * | ||
| + * @return A map of *nix to instructions. | ||
| + */ | ||
| + private static ComboBox<UnixOsCommand> createUnixOsCommandMap() { | ||
| + new ComboBox<UnixOsCommand>(); | ||
| + final var comboBox = new ComboBox<UnixOsCommand>(); | ||
| + final var items = comboBox.getItems(); | ||
| + final var prefix = PREFIX + ".command"; | ||
| + final var distros = getInt( prefix + ".distros", 14 ); | ||
| + | ||
| + for( int i = 1; i <= distros; i++ ) { | ||
| + final var suffix = format( ".%02d", i ); | ||
| + final var name = get( prefix + ".os.name" + suffix ); | ||
| + final var command = get( prefix + ".os.text" + suffix ); | ||
| + | ||
| + items.add( new UnixOsCommand( name, command ) ); | ||
| + } | ||
| + | ||
| + items.sort( UnixOsCommand::compareTo ); | ||
| + | ||
| + return comboBox; | ||
| + } | ||
| +} | ||
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +import javafx.collections.ObservableMap; | ||
| + | ||
| +import static com.keenwrite.Messages.get; | ||
| +import static com.keenwrite.typesetting.installer.panes.WindowsManagerInstallPane.WIN_BIN; | ||
| + | ||
| +/** | ||
| + * Responsible for downloading the container manager software on Windows. | ||
| + */ | ||
| +public final class WindowsManagerDownloadPane extends AbstractDownloadPane { | ||
| + private static final String PREFIX = | ||
| + "Wizard.typesetter.win.2.download.container"; | ||
| + | ||
| + @Override | ||
| + protected void updateProperties( | ||
| + final ObservableMap<Object, Object> properties ) { | ||
| + properties.put( WIN_BIN, getTarget() ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String getPrefix() { | ||
| + return PREFIX; | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String getChecksum() { | ||
| + return get( "Wizard.typesetter.container.checksum" ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.typesetting.installer.panes; | ||
| + | ||
| +import com.keenwrite.typesetting.containerization.ContainerManager; | ||
| +import javafx.scene.control.TextArea; | ||
| +import javafx.scene.layout.BorderPane; | ||
| +import javafx.scene.layout.VBox; | ||
| +import org.controlsfx.dialog.Wizard; | ||
| + | ||
| +import java.io.File; | ||
| + | ||
| +import static com.keenwrite.Messages.get; | ||
| + | ||
| +/** | ||
| + * Responsible for installing the container manager on Windows. | ||
| + */ | ||
| +public final class WindowsManagerInstallPane extends InstallerPane { | ||
| + /** | ||
| + * Property for the installation thread to help with reentrancy. | ||
| + */ | ||
| + private static final String WIN_INSTALLER = "windows.container.installer"; | ||
| + | ||
| + /** | ||
| + * Shared property to track name of container manager binary file. | ||
| + */ | ||
| + static final String WIN_BIN = "windows.container.binary"; | ||
| + | ||
| + private static final String PREFIX = | ||
| + "Wizard.typesetter.win.2.install.container"; | ||
| + | ||
| + private final ContainerManager mContainer; | ||
| + private final TextArea mCommands; | ||
| + | ||
| + public WindowsManagerInstallPane() { | ||
| + mCommands = textArea( 2, 55 ); | ||
| + | ||
| + final var titledPane = titledPane( "Output", mCommands ); | ||
| + append( mCommands, get( PREFIX + ".status.running" ) ); | ||
| + | ||
| + final var stepsPane = new VBox(); | ||
| + final var steps = stepsPane.getChildren(); | ||
| + steps.add( label( PREFIX + ".step.0" ) ); | ||
| + steps.add( spacer() ); | ||
| + steps.add( label( PREFIX + ".step.1" ) ); | ||
| + steps.add( label( PREFIX + ".step.2" ) ); | ||
| + steps.add( label( PREFIX + ".step.3" ) ); | ||
| + steps.add( spacer() ); | ||
| + steps.add( titledPane ); | ||
| + | ||
| + final var border = new BorderPane(); | ||
| + border.setTop( stepsPane ); | ||
| + | ||
| + mContainer = createContainer(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void onEnteringPage( final Wizard wizard ) { | ||
| + disableNext( true ); | ||
| + | ||
| + // Pull the fully qualified installer path from the properties. | ||
| + final var properties = wizard.getProperties(); | ||
| + final var thread = properties.get( WIN_INSTALLER ); | ||
| + | ||
| + if( thread instanceof Thread installer && installer.isAlive() ) { | ||
| + return; | ||
| + } | ||
| + | ||
| + final var binary = properties.get( WIN_BIN ); | ||
| + final var key = PREFIX + ".status"; | ||
| + | ||
| + if( binary instanceof File exe ) { | ||
| + final var task = createTask( () -> { | ||
| + final var exit = mContainer.install( exe ); | ||
| + | ||
| + // Remove the installer after installation is finished. | ||
| + properties.remove( thread ); | ||
| + | ||
| + final var msg = exit == 0 | ||
| + ? get( key + ".success" ) | ||
| + : get( key + ".failure", exit ); | ||
| + | ||
| + append( mCommands, msg ); | ||
| + disableNext( exit != 0 ); | ||
| + | ||
| + return null; | ||
| + } ); | ||
| + | ||
| + final var installer = createThread( task ); | ||
| + properties.put( WIN_INSTALLER, installer ); | ||
| + installer.start(); | ||
| + } | ||
| + else { | ||
| + append( mCommands, get( PREFIX + ".unknown", binary ) ); | ||
| + } | ||
| + } | ||
| + | ||
| + @Override | ||
| + public String getHeaderKey() { | ||
| + return PREFIX + ".header"; | ||
| + } | ||
| +} | ||