Early work on shape tools and 'st' command.
This commit is contained in:
parent
6f3c68a0ee
commit
37683e1852
|
@ -0,0 +1,81 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
import java.awt.Point;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BitmapShape implements Shape {
|
||||||
|
public final List<List<Boolean>> grid = new ArrayList<>();
|
||||||
|
public final Point origin = new Point();
|
||||||
|
|
||||||
|
public BitmapShape() {
|
||||||
|
this(0,0);
|
||||||
|
}
|
||||||
|
public BitmapShape(int height, int width) {
|
||||||
|
while (grid.size() < height) {
|
||||||
|
grid.add(newRow(width));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Boolean> newRow(int width) {
|
||||||
|
List<Boolean> row = new ArrayList<>();
|
||||||
|
while (row.size() < width) {
|
||||||
|
row.add(Boolean.FALSE);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertColumn() {
|
||||||
|
origin.y++;
|
||||||
|
for (List<Boolean> row : grid) {
|
||||||
|
row.add(0, Boolean.FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void addColumn() {
|
||||||
|
for (List<Boolean> row : grid) {
|
||||||
|
row.add(Boolean.FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void insertRow() {
|
||||||
|
origin.x++;
|
||||||
|
grid.add(0, newRow(getWidth()));
|
||||||
|
}
|
||||||
|
public void addRow() {
|
||||||
|
grid.add(newRow(getWidth()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return grid.size();
|
||||||
|
}
|
||||||
|
public int getWidth() {
|
||||||
|
return grid.isEmpty() ? 0 : grid.get(0).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void plot(int x, int y) {
|
||||||
|
plot(x, y, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
public void plot(int x, int y, Boolean pixel) {
|
||||||
|
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grid.get(y).set(x, pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean get(int x, int y) {
|
||||||
|
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
return grid.get(y).get(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BitmapShape toBitmap() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VectorShape toVector() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
public interface Shape {
|
||||||
|
public BitmapShape toBitmap();
|
||||||
|
public VectorShape toVector();
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import io.github.applecommander.bastools.api.shapes.exporters.TextShapeExporter;
|
||||||
|
|
||||||
|
public interface ShapeExporter {
|
||||||
|
/** Export a single shape to the OutputStream. */
|
||||||
|
public void export(Shape shape, OutputStream outputStream);
|
||||||
|
/** Export a single shape to the File. */
|
||||||
|
public default void export(Shape shape, File file) throws IOException {
|
||||||
|
Objects.requireNonNull(shape);
|
||||||
|
Objects.requireNonNull(file);
|
||||||
|
export(shape, file.toPath());
|
||||||
|
}
|
||||||
|
/** Export a single shape to the Path. */
|
||||||
|
public default void export(Shape shape, Path path) throws IOException {
|
||||||
|
Objects.requireNonNull(shape);
|
||||||
|
Objects.requireNonNull(path);
|
||||||
|
try (OutputStream outputStream = Files.newOutputStream(path)) {
|
||||||
|
export(shape, outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Export the entire shape table to the OutputStream. */
|
||||||
|
public void export(ShapeTable shapeTable, OutputStream outputStream);
|
||||||
|
/** Export the entire shape table to the File. */
|
||||||
|
public default void export(ShapeTable shapeTable, File file) throws IOException {
|
||||||
|
Objects.requireNonNull(shapeTable);
|
||||||
|
Objects.requireNonNull(file);
|
||||||
|
export(shapeTable, file.toPath());
|
||||||
|
}
|
||||||
|
/** Export the entire shape table to the Path. */
|
||||||
|
public default void export(ShapeTable shapeTable, Path path) throws IOException {
|
||||||
|
Objects.requireNonNull(shapeTable);
|
||||||
|
Objects.requireNonNull(path);
|
||||||
|
try (OutputStream outputStream = Files.newOutputStream(path)) {
|
||||||
|
export(shapeTable, outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextShapeExporter.Builder text() {
|
||||||
|
return new TextShapeExporter.Builder();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import io.github.applecommander.bastools.api.utils.Streams;
|
||||||
|
|
||||||
|
public class ShapeTable {
|
||||||
|
public static ShapeTable read(byte[] data) {
|
||||||
|
Objects.requireNonNull(data);
|
||||||
|
ShapeTable shapeTable = new ShapeTable();
|
||||||
|
ByteBuffer buf = ByteBuffer.wrap(data)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
int count = Byte.toUnsignedInt(buf.get());
|
||||||
|
// unused:
|
||||||
|
buf.get();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int offset = buf.getShort();
|
||||||
|
// load empty shapes as empty...
|
||||||
|
if (offset == 0) {
|
||||||
|
shapeTable.shapes.add(new VectorShape());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// defer to VectorShape to process bits
|
||||||
|
buf.mark();
|
||||||
|
buf.position(offset);
|
||||||
|
shapeTable.shapes.add(VectorShape.from(buf));
|
||||||
|
buf.reset();
|
||||||
|
}
|
||||||
|
return shapeTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShapeTable read(InputStream inputStream) throws IOException {
|
||||||
|
Objects.requireNonNull(inputStream);
|
||||||
|
return read(Streams.toByteArray(inputStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShapeTable read(File file) throws IOException {
|
||||||
|
Objects.requireNonNull(file);
|
||||||
|
return read(file.toPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShapeTable read(Path path) throws IOException {
|
||||||
|
Objects.requireNonNull(path);
|
||||||
|
return read(Files.readAllBytes(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public final List<Shape> shapes = new ArrayList<>();
|
||||||
|
|
||||||
|
public void write(OutputStream outputStream) throws IOException {
|
||||||
|
Objects.requireNonNull(outputStream);
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(File file) throws IOException {
|
||||||
|
Objects.requireNonNull(file);
|
||||||
|
try (OutputStream outputStream = new FileOutputStream(file)) {
|
||||||
|
write(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(Path path) throws IOException {
|
||||||
|
Objects.requireNonNull(path);
|
||||||
|
write(path.toFile());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents all "plot vectors" available in an Applesoft shape table.
|
||||||
|
*
|
||||||
|
* @see <a href="https://archive.org/stream/Applesoft_BASIC_Programming_Reference_Manual_Apple_Computer#page/n103/mode/2up">
|
||||||
|
* Applesoft BASIC Programming Reference Manual</a>
|
||||||
|
*/
|
||||||
|
public enum VectorCommand {
|
||||||
|
// Order here is specific to the encoding within the shape itself
|
||||||
|
MOVE_UP, MOVE_RIGHT, MOVE_DOWN, MOVE_LEFT,
|
||||||
|
PLOT_UP, PLOT_RIGHT, PLOT_DOWN, PLOT_LEFT;
|
||||||
|
|
||||||
|
public final boolean plot;
|
||||||
|
public final int xmove;
|
||||||
|
public final int ymove;
|
||||||
|
|
||||||
|
private VectorCommand() {
|
||||||
|
this.plot = (this.ordinal() & 0b100) != 0;
|
||||||
|
// up 0b00
|
||||||
|
// right 0b01
|
||||||
|
// down 0b10
|
||||||
|
// left 0b11
|
||||||
|
if ((this.ordinal() & 0b001) == 1) {
|
||||||
|
this.xmove = 2 - (this.ordinal() & 0b011);
|
||||||
|
this.ymove = 0;
|
||||||
|
} else {
|
||||||
|
this.xmove = 0;
|
||||||
|
this.ymove = (this.ordinal() & 0b011) - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class VectorShape implements Shape {
|
||||||
|
public static VectorShape from(ByteBuffer buf) {
|
||||||
|
Objects.requireNonNull(buf);
|
||||||
|
VectorShape shape = new VectorShape();
|
||||||
|
|
||||||
|
VectorCommand[] commands = VectorCommand.values();
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
int code = Byte.toUnsignedInt(buf.get());
|
||||||
|
if (code == 0) break;
|
||||||
|
|
||||||
|
int vector1 = code & 0b111;
|
||||||
|
int vector2 = (code >> 3) & 0b111;
|
||||||
|
int vector3 = (code >> 6) & 0b011; // Cannot plot
|
||||||
|
|
||||||
|
shape.vectors.add(commands[vector1]);
|
||||||
|
|
||||||
|
if (vector2 != 0 || vector3 != 0) {
|
||||||
|
shape.vectors.add(commands[vector2]);
|
||||||
|
|
||||||
|
if (vector3 != 0) {
|
||||||
|
shape.vectors.add(commands[vector3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final List<VectorCommand> vectors = new ArrayList<>();
|
||||||
|
|
||||||
|
public VectorShape moveUp() { return add(VectorCommand.MOVE_UP); }
|
||||||
|
public VectorShape moveRight() { return add(VectorCommand.MOVE_RIGHT); }
|
||||||
|
public VectorShape moveDown() { return add(VectorCommand.MOVE_DOWN); }
|
||||||
|
public VectorShape moveLeft() { return add(VectorCommand.MOVE_LEFT); }
|
||||||
|
public VectorShape plotUp() { return add(VectorCommand.PLOT_UP); }
|
||||||
|
public VectorShape plotRight() { return add(VectorCommand.PLOT_RIGHT); }
|
||||||
|
public VectorShape plotDown() { return add(VectorCommand.PLOT_DOWN); }
|
||||||
|
public VectorShape plotLeft() { return add(VectorCommand.PLOT_LEFT); }
|
||||||
|
|
||||||
|
private VectorShape add(VectorCommand vectorCommand) {
|
||||||
|
this.vectors.add(vectorCommand);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BitmapShape toBitmap() {
|
||||||
|
BitmapShape shape = new BitmapShape();
|
||||||
|
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
for (VectorCommand command : vectors) {
|
||||||
|
if (command.plot) {
|
||||||
|
while (y < 0) {
|
||||||
|
shape.insertRow();
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
while (y >= shape.getHeight()) {
|
||||||
|
shape.addRow();
|
||||||
|
}
|
||||||
|
while (x < 0) {
|
||||||
|
shape.insertColumn();
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
while (x >= shape.getWidth()) {
|
||||||
|
shape.addColumn();
|
||||||
|
}
|
||||||
|
shape.plot(x,y);
|
||||||
|
}
|
||||||
|
x += command.xmove;
|
||||||
|
y += command.ymove;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VectorShape toVector() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,245 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes.exporters;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import io.github.applecommander.bastools.api.shapes.BitmapShape;
|
||||||
|
import io.github.applecommander.bastools.api.shapes.Shape;
|
||||||
|
import io.github.applecommander.bastools.api.shapes.ShapeExporter;
|
||||||
|
import io.github.applecommander.bastools.api.shapes.ShapeTable;
|
||||||
|
|
||||||
|
public class TextShapeExporter implements ShapeExporter {
|
||||||
|
private int maxWidth = 80;
|
||||||
|
private BorderStrategy borderStrategy = BorderStrategy.BOX_DRAWING;
|
||||||
|
|
||||||
|
/** Use the {@code Builder} to create a TextShapeExporter. */
|
||||||
|
private TextShapeExporter() { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void export(Shape shape, OutputStream outputStream) {
|
||||||
|
Objects.requireNonNull(shape);
|
||||||
|
Objects.requireNonNull(outputStream);
|
||||||
|
|
||||||
|
BitmapShape b = shape.toBitmap();
|
||||||
|
PrintWriter pw = new PrintWriter(outputStream);
|
||||||
|
drawTopLine(pw, 1, b.getWidth());
|
||||||
|
|
||||||
|
Queue<BitmapShape> bqueue = new LinkedList<>(Arrays.asList(b));
|
||||||
|
drawRow(pw, bqueue, 1, b.getHeight(), b.getWidth());
|
||||||
|
|
||||||
|
drawBottomLine(pw, 1, b.getWidth());
|
||||||
|
pw.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void export(ShapeTable shapeTable, OutputStream outputStream) {
|
||||||
|
Objects.requireNonNull(shapeTable);
|
||||||
|
Objects.requireNonNull(outputStream);
|
||||||
|
|
||||||
|
List<BitmapShape> blist = shapeTable.shapes.stream()
|
||||||
|
.map(Shape::toBitmap)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
int width = blist.stream().mapToInt(BitmapShape::getWidth).max().getAsInt();
|
||||||
|
int height = blist.stream().mapToInt(BitmapShape::getHeight).max().getAsInt();
|
||||||
|
|
||||||
|
int columns = Math.max(1, this.maxWidth / width);
|
||||||
|
|
||||||
|
PrintWriter pw = new PrintWriter(outputStream);
|
||||||
|
drawTopLine(pw, columns, width);
|
||||||
|
|
||||||
|
Queue<BitmapShape> bqueue = new LinkedList<>(blist);
|
||||||
|
drawRow(pw, bqueue, columns, height, width);
|
||||||
|
while (!bqueue.isEmpty()) {
|
||||||
|
drawDividerLine(pw, columns, width);
|
||||||
|
drawRow(pw, bqueue, columns, height, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBottomLine(pw, columns, width);
|
||||||
|
pw.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawTopLine(PrintWriter pw, int columns, int width) {
|
||||||
|
borderStrategy.topLeftCorner(pw);
|
||||||
|
borderStrategy.horizontalLine(pw, width);
|
||||||
|
for (int i=1; i<columns; i++) {
|
||||||
|
borderStrategy.topDivider(pw);
|
||||||
|
borderStrategy.horizontalLine(pw, width);
|
||||||
|
}
|
||||||
|
borderStrategy.topRightCorner(pw);
|
||||||
|
}
|
||||||
|
private void drawDividerLine(PrintWriter pw, int columns, int width) {
|
||||||
|
borderStrategy.dividerLeftEdge(pw);
|
||||||
|
borderStrategy.dividerHorizontalLine(pw, width);
|
||||||
|
for (int i=1; i<columns; i++) {
|
||||||
|
borderStrategy.dividerMiddle(pw);
|
||||||
|
borderStrategy.dividerHorizontalLine(pw, width);
|
||||||
|
}
|
||||||
|
borderStrategy.dividerRightEdge(pw);
|
||||||
|
}
|
||||||
|
private void drawBottomLine(PrintWriter pw, int columns, int width) {
|
||||||
|
borderStrategy.bottomLeftCorner(pw);
|
||||||
|
borderStrategy.horizontalLine(pw, width);
|
||||||
|
for (int i=1; i<columns; i++) {
|
||||||
|
borderStrategy.bottomDivider(pw);
|
||||||
|
borderStrategy.horizontalLine(pw, width);
|
||||||
|
}
|
||||||
|
borderStrategy.bottomRightCorner(pw);
|
||||||
|
}
|
||||||
|
private void drawRow(PrintWriter pw, Queue<BitmapShape> bqueue, int columns, int height, int width) {
|
||||||
|
BitmapShape[] bshapes = new BitmapShape[columns];
|
||||||
|
for (int i=0; i<bshapes.length; i++) {
|
||||||
|
bshapes[i] = bqueue.isEmpty() ? new BitmapShape() : bqueue.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y=0; y<height; y++) {
|
||||||
|
borderStrategy.verticalLine(pw);
|
||||||
|
drawRowLine(pw, bshapes[0], width, y);
|
||||||
|
for (int c=1; c<bshapes.length; c++) {
|
||||||
|
borderStrategy.dividerVerticalLine(pw);
|
||||||
|
drawRowLine(pw, bshapes[c], width, y);
|
||||||
|
}
|
||||||
|
borderStrategy.verticalLine(pw);
|
||||||
|
pw.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void drawRowLine(PrintWriter pw, BitmapShape bshape, int width, int y) {
|
||||||
|
List<Boolean> row = bshape.grid.size() > y ? bshape.grid.get(y) : new ArrayList<>();
|
||||||
|
for (int x=0; x<width; x++) {
|
||||||
|
Boolean plot = row.size() > x ? row.get(x) : Boolean.FALSE;
|
||||||
|
if (bshape.origin.x == x && bshape.origin.y == y) {
|
||||||
|
pw.printf("%c", plot ? '*' : '+');
|
||||||
|
} else {
|
||||||
|
pw.printf("%c", plot ? 'X' : '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BorderStrategy {
|
||||||
|
/** No border but with spaces between shapes. Note the tricky newline in {@code dividerLeftEdge}. */
|
||||||
|
NONE('\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', ' ', '\0', '\n', '\0', '\0'),
|
||||||
|
/**
|
||||||
|
* A border comprised of the box drawing characters.
|
||||||
|
* @see <a href='https://en.wikipedia.org/wiki/Box-drawing_character'>Wikipedia article on box characters</a>
|
||||||
|
*/
|
||||||
|
BOX_DRAWING('\u2500', '\u2502', '\u250C', '\u2510', '\u2514', '\u2518', '\u252C', '\u2534',
|
||||||
|
'\u2502', '\u2500', '\u251C', '\u2524', '\u253C'),
|
||||||
|
/** A simple border based on plain ASCII characters. */
|
||||||
|
ASCII_TEXT('-', '|', '+', '+', '+', '+', '+', '+', '|', '-', '+', '+', '+');
|
||||||
|
|
||||||
|
private final char horizontalLine;
|
||||||
|
private final char verticalLine;
|
||||||
|
private final char topLeftCorner;
|
||||||
|
private final char topRightCorner;
|
||||||
|
private final char bottomLeftCorner;
|
||||||
|
private final char bottomRightCorner;
|
||||||
|
private final char topDivider;
|
||||||
|
private final char bottomDivider;
|
||||||
|
private final char dividerVerticalLine;
|
||||||
|
private final char dividerHorizontalLine;
|
||||||
|
private final char dividerLeftEdge;
|
||||||
|
private final char dividerRightEdge;
|
||||||
|
private final char dividerMiddle;
|
||||||
|
|
||||||
|
private BorderStrategy(char horizontalLine, char verticalLine, char topLeftCorner, char topRightCorner,
|
||||||
|
char bottomLeftCorner, char bottomRightCorner, char topDivider, char bottomDivider,
|
||||||
|
char dividerVerticalLine, char dividerHorizontalLine, char dividerLeftEdge, char dividerRightEdge,
|
||||||
|
char dividerMiddle) {
|
||||||
|
this.horizontalLine = horizontalLine;
|
||||||
|
this.verticalLine = verticalLine;
|
||||||
|
this.topLeftCorner = topLeftCorner;
|
||||||
|
this.topRightCorner = topRightCorner;
|
||||||
|
this.bottomLeftCorner = bottomLeftCorner;
|
||||||
|
this.bottomRightCorner = bottomRightCorner;
|
||||||
|
this.topDivider = topDivider;
|
||||||
|
this.bottomDivider = bottomDivider;
|
||||||
|
this.dividerVerticalLine = dividerVerticalLine;
|
||||||
|
this.dividerHorizontalLine = dividerHorizontalLine;
|
||||||
|
this.dividerLeftEdge = dividerLeftEdge;
|
||||||
|
this.dividerRightEdge = dividerRightEdge;
|
||||||
|
this.dividerMiddle = dividerMiddle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void print(Consumer<String> output, char ch) {
|
||||||
|
print(output, ch, 1);
|
||||||
|
}
|
||||||
|
private void print(Consumer<String> output, char ch, int width) {
|
||||||
|
if (ch != '\0') {
|
||||||
|
output.accept(new String(new char[width]).replace('\0', ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void horizontalLine(PrintWriter pw, int width) {
|
||||||
|
print(pw::print, horizontalLine, width);
|
||||||
|
}
|
||||||
|
public void verticalLine(PrintWriter pw) {
|
||||||
|
print(pw::print, verticalLine);
|
||||||
|
}
|
||||||
|
public void topLeftCorner(PrintWriter pw) {
|
||||||
|
print(pw::print, topLeftCorner);
|
||||||
|
}
|
||||||
|
public void topRightCorner(PrintWriter pw) {
|
||||||
|
print(pw::println, topRightCorner);
|
||||||
|
}
|
||||||
|
public void bottomLeftCorner(PrintWriter pw) {
|
||||||
|
print(pw::print, bottomLeftCorner);
|
||||||
|
}
|
||||||
|
public void bottomRightCorner(PrintWriter pw) {
|
||||||
|
print(pw::println, bottomRightCorner);
|
||||||
|
}
|
||||||
|
public void topDivider(PrintWriter pw) {
|
||||||
|
print(pw::print, topDivider);
|
||||||
|
}
|
||||||
|
public void bottomDivider(PrintWriter pw) {
|
||||||
|
print(pw::print, bottomDivider);
|
||||||
|
}
|
||||||
|
public void dividerVerticalLine(PrintWriter pw) {
|
||||||
|
print(pw::print, dividerVerticalLine);
|
||||||
|
}
|
||||||
|
public void dividerHorizontalLine(PrintWriter pw, int width) {
|
||||||
|
print(pw::print, dividerHorizontalLine, width);
|
||||||
|
}
|
||||||
|
public void dividerLeftEdge(PrintWriter pw) {
|
||||||
|
print(pw::print, dividerLeftEdge);
|
||||||
|
}
|
||||||
|
public void dividerRightEdge(PrintWriter pw) {
|
||||||
|
print(pw::println, dividerRightEdge);
|
||||||
|
}
|
||||||
|
public void dividerMiddle(PrintWriter pw) {
|
||||||
|
print(pw::print, dividerMiddle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private TextShapeExporter textShapeExporter = new TextShapeExporter();
|
||||||
|
|
||||||
|
public Builder maxWidth(int maxWidth) {
|
||||||
|
textShapeExporter.maxWidth = maxWidth;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public Builder noBorder() {
|
||||||
|
return borderStrategy(BorderStrategy.NONE);
|
||||||
|
}
|
||||||
|
public Builder asciiTextBorder() {
|
||||||
|
return borderStrategy(BorderStrategy.ASCII_TEXT);
|
||||||
|
}
|
||||||
|
public Builder boxDrawingBorder() {
|
||||||
|
return borderStrategy(BorderStrategy.BOX_DRAWING);
|
||||||
|
}
|
||||||
|
public Builder borderStrategy(BorderStrategy borderStrategy) {
|
||||||
|
textShapeExporter.borderStrategy = borderStrategy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShapeExporter build() {
|
||||||
|
return textShapeExporter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package io.github.applecommander.bastools.api.utils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class Streams {
|
||||||
|
private Streams() { /* Prevent construction */ }
|
||||||
|
|
||||||
|
/** Utility method to read all bytes from an InputStream. */
|
||||||
|
public static byte[] toByteArray(InputStream inputStream) throws IOException {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
while (true) {
|
||||||
|
byte[] buf = new byte[1024];
|
||||||
|
int len = inputStream.read(buf);
|
||||||
|
if (len == -1) break;
|
||||||
|
outputStream.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class ShapesTest {
|
||||||
|
/**
|
||||||
|
* This shape is taken from the Applesoft BASIC Programmer's Reference Manual (1987), p146.
|
||||||
|
*/
|
||||||
|
public ShapeTable readStandardShapeTable() {
|
||||||
|
final byte[] sample = { 0x01, 0x00, 0x04, 0x00, 0x12, 0x3F, 0x20, 0x64, 0x2d, 0x15, 0x36, 0x1e, 0x07, 0x00 };
|
||||||
|
ShapeTable st = ShapeTable.read(sample);
|
||||||
|
assertNotNull(st);
|
||||||
|
assertNotNull(st.shapes);
|
||||||
|
assertEquals(1, st.shapes.size());
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStandardShapeTableVectors() {
|
||||||
|
ShapeTable st = readStandardShapeTable();
|
||||||
|
|
||||||
|
VectorShape expected = new VectorShape()
|
||||||
|
.moveDown().moveDown()
|
||||||
|
.plotLeft().plotLeft()
|
||||||
|
.moveUp().plotUp().plotUp().plotUp()
|
||||||
|
.moveRight().plotRight().plotRight().plotRight()
|
||||||
|
.moveDown().plotDown().plotDown().plotDown()
|
||||||
|
.moveLeft().plotLeft();
|
||||||
|
|
||||||
|
Shape s = st.shapes.get(0);
|
||||||
|
assertNotNull(s);
|
||||||
|
assertEquals(expected.vectors, s.toVector().vectors);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStandardShapeTableBitmap() {
|
||||||
|
ShapeTable st = readStandardShapeTable();
|
||||||
|
|
||||||
|
BitmapShape expected = new BitmapShape(5, 5);
|
||||||
|
for (int i=1; i<=3; i++) {
|
||||||
|
expected.plot(i, 0);
|
||||||
|
expected.plot(i, 4);
|
||||||
|
expected.plot(0, i);
|
||||||
|
expected.plot(4, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape s = st.shapes.get(0);
|
||||||
|
assertEquals(expected.grid, s.toBitmap().grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTextShapeExporterNoBorder() {
|
||||||
|
ShapeTable st = readStandardShapeTable();
|
||||||
|
|
||||||
|
final String expected = ".XXX.\n"
|
||||||
|
+ "X...X\n"
|
||||||
|
+ "X.+.X\n"
|
||||||
|
+ "X...X\n"
|
||||||
|
+ ".XXX.\n";
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ShapeExporter exp = ShapeExporter.text().noBorder().build();
|
||||||
|
exp.export(st.shapes.get(0), outputStream);
|
||||||
|
String actual = new String(outputStream.toByteArray());
|
||||||
|
|
||||||
|
assertEquals(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTextShapeExporterAsciiBorder() {
|
||||||
|
ShapeTable st = readStandardShapeTable();
|
||||||
|
|
||||||
|
final String expected = "+-----+\n"
|
||||||
|
+ "|.XXX.|\n"
|
||||||
|
+ "|X...X|\n"
|
||||||
|
+ "|X.+.X|\n"
|
||||||
|
+ "|X...X|\n"
|
||||||
|
+ "|.XXX.|\n"
|
||||||
|
+ "+-----+\n";
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ShapeExporter exp = ShapeExporter.text().asciiTextBorder().build();
|
||||||
|
exp.export(st.shapes.get(0), outputStream);
|
||||||
|
String actual = new String(outputStream.toByteArray());
|
||||||
|
|
||||||
|
assertEquals(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTextShapeTableExporterNoBorder() {
|
||||||
|
ShapeTable st = readStandardShapeTable();
|
||||||
|
|
||||||
|
// Simulate 4 of these identical shapes by adding 3 more
|
||||||
|
st.shapes.add(st.shapes.get(0));
|
||||||
|
st.shapes.add(st.shapes.get(0));
|
||||||
|
st.shapes.add(st.shapes.get(0));
|
||||||
|
|
||||||
|
final String oneExpectedRow = ".XXX. .XXX.\n"
|
||||||
|
+ "X...X X...X\n"
|
||||||
|
+ "X.+.X X.+.X\n"
|
||||||
|
+ "X...X X...X\n"
|
||||||
|
+ ".XXX. .XXX.\n";
|
||||||
|
String expected = oneExpectedRow + "\n" + oneExpectedRow;
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ShapeExporter exp = ShapeExporter.text().maxWidth(12).noBorder().build();
|
||||||
|
exp.export(st, outputStream);
|
||||||
|
String actual = new String(outputStream.toByteArray());
|
||||||
|
|
||||||
|
assertEquals(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTextShapeTableExporterAsciiBorder() {
|
||||||
|
ShapeTable st = readStandardShapeTable();
|
||||||
|
|
||||||
|
// Simulate 4 of these identical shapes by adding 3 more
|
||||||
|
st.shapes.add(st.shapes.get(0));
|
||||||
|
st.shapes.add(st.shapes.get(0));
|
||||||
|
st.shapes.add(st.shapes.get(0));
|
||||||
|
|
||||||
|
final String divider = "+-----+-----+\n";
|
||||||
|
final String oneExpectedRow = divider
|
||||||
|
+ "|.XXX.|.XXX.|\n"
|
||||||
|
+ "|X...X|X...X|\n"
|
||||||
|
+ "|X.+.X|X.+.X|\n"
|
||||||
|
+ "|X...X|X...X|\n"
|
||||||
|
+ "|.XXX.|.XXX.|\n";
|
||||||
|
String expected = oneExpectedRow + oneExpectedRow + divider;
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ShapeExporter exp = ShapeExporter.text().maxWidth(12).asciiTextBorder().build();
|
||||||
|
exp.export(st, outputStream);
|
||||||
|
String actual = new String(outputStream.toByteArray());
|
||||||
|
|
||||||
|
assertEquals(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package io.github.applecommander.bastools.api.shapes;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class VectorCommandTest {
|
||||||
|
@Test
|
||||||
|
public void testDirections() {
|
||||||
|
test(0, 1, VectorCommand.MOVE_DOWN, VectorCommand.PLOT_DOWN);
|
||||||
|
test(0, -1, VectorCommand.MOVE_UP, VectorCommand.PLOT_UP);
|
||||||
|
test(-1, 0, VectorCommand.MOVE_LEFT, VectorCommand.PLOT_LEFT);
|
||||||
|
test(1, 0, VectorCommand.MOVE_RIGHT, VectorCommand.PLOT_RIGHT);
|
||||||
|
}
|
||||||
|
public void test(int xmove, int ymove, VectorCommand... commands) {
|
||||||
|
for (VectorCommand command : commands) {
|
||||||
|
assertEquals(xmove, command.xmove);
|
||||||
|
assertEquals(ymove, command.ymove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlot() {
|
||||||
|
test(false, VectorCommand.MOVE_DOWN, VectorCommand.MOVE_LEFT, VectorCommand.MOVE_RIGHT, VectorCommand.MOVE_UP);
|
||||||
|
test(true, VectorCommand.PLOT_DOWN, VectorCommand.PLOT_LEFT, VectorCommand.PLOT_RIGHT, VectorCommand.PLOT_UP);
|
||||||
|
}
|
||||||
|
public void test(boolean plot, VectorCommand... commands) {
|
||||||
|
for (VectorCommand command : commands) {
|
||||||
|
assertEquals(plot, command.plot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
include 'api'
|
include 'api'
|
||||||
include 'tools:bt'
|
include 'tools:bt'
|
||||||
|
include 'tools:st'
|
||||||
|
|
||||||
rootProject.name = 'bastools'
|
rootProject.name = 'bastools'
|
||||||
project(":api").name = 'bastools-api'
|
project(":api").name = 'bastools-api'
|
||||||
project(":tools").name = 'bastools-tools'
|
project(":tools").name = 'bastools-tools'
|
||||||
project(":tools:bt").name = 'bastools-tools-bt'
|
project(":tools:bt").name = 'bastools-tools-bt'
|
||||||
|
project(":tools:st").name = 'bastools-tools-st'
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
plugins {
|
||||||
|
id 'org.springframework.boot' version '2.0.2.RELEASE'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName = "io.github.applecommander.bastools.tools.st.Main"
|
||||||
|
|
||||||
|
bootJar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Implementation-Title': 'Shape Tools CLI',
|
||||||
|
'Implementation-Version': "${version} (${new Date().format('yyyy-MM-dd HH:mm')})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile 'info.picocli:picocli:3.0.2'
|
||||||
|
compile 'net.sf.applecommander:applesingle-api:1.2.1'
|
||||||
|
compile project(':bastools-api')
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package io.github.applecommander.bastools.tools.st;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import io.github.applecommander.bastools.api.shapes.Shape;
|
||||||
|
import io.github.applecommander.bastools.api.shapes.ShapeExporter;
|
||||||
|
import io.github.applecommander.bastools.api.shapes.ShapeTable;
|
||||||
|
import io.github.applecommander.bastools.api.shapes.exporters.TextShapeExporter.BorderStrategy;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.Help.Visibility;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
import picocli.CommandLine.Parameters;
|
||||||
|
|
||||||
|
@Command(name = "extract", description = { "Extract shapes from shape table" },
|
||||||
|
parameterListHeading = "%nParameters:%n",
|
||||||
|
descriptionHeading = "%n",
|
||||||
|
optionListHeading = "%nOptions:%n")
|
||||||
|
public class ExtractCommand implements Callable<Void> {
|
||||||
|
@Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true)
|
||||||
|
private boolean helpFlag;
|
||||||
|
|
||||||
|
@Option(names = "--stdin", description = "Read from stdin")
|
||||||
|
private boolean stdinFlag;
|
||||||
|
|
||||||
|
@Option(names = "--stdout", description = "Write to stdout")
|
||||||
|
private boolean stdoutFlag;
|
||||||
|
|
||||||
|
@Option(names = { "-o", "--output" }, description = "Write to filename")
|
||||||
|
private String filename;
|
||||||
|
|
||||||
|
@Option(names = "--border", description = "Set border style (none, simple, box)", showDefaultValue = Visibility.ALWAYS)
|
||||||
|
private String borderStyle = "simple";
|
||||||
|
|
||||||
|
@Option(names = { "-w", "--width" }, description = "Set text width", showDefaultValue = Visibility.ALWAYS)
|
||||||
|
private int textWidth = 80;
|
||||||
|
|
||||||
|
@Option(names = "--shape", description = "Extract specific shape")
|
||||||
|
private int shapeNum = 0;
|
||||||
|
|
||||||
|
@Parameters(arity = "0..1", description = "File to process")
|
||||||
|
private Path file;
|
||||||
|
|
||||||
|
private BorderStrategy borderStrategy;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void call() throws IOException {
|
||||||
|
validateArguments();
|
||||||
|
|
||||||
|
ShapeTable shapeTable = stdinFlag ? ShapeTable.read(System.in) : ShapeTable.read(file);
|
||||||
|
|
||||||
|
ShapeExporter exporter = ShapeExporter.text()
|
||||||
|
.borderStrategy(borderStrategy)
|
||||||
|
.maxWidth(textWidth)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if (shapeNum > 0) {
|
||||||
|
if (shapeNum <= shapeTable.shapes.size()) {
|
||||||
|
Shape shape = shapeTable.shapes.get(shapeNum-1);
|
||||||
|
if (stdoutFlag) {
|
||||||
|
exporter.export(shape, System.out);
|
||||||
|
} else {
|
||||||
|
exporter.export(shape, Paths.get(filename));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IOException("Invalid shape number");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (stdoutFlag) {
|
||||||
|
exporter.export(shapeTable, System.out);
|
||||||
|
} else {
|
||||||
|
exporter.export(shapeTable, Paths.get(filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateArguments() throws IOException {
|
||||||
|
if (stdoutFlag && filename != null) {
|
||||||
|
throw new IOException("Please choose one of stdout or output file");
|
||||||
|
}
|
||||||
|
if ((stdinFlag && file != null) || (!stdinFlag && file == null)) {
|
||||||
|
throw new IOException("Please select ONE of stdin or file");
|
||||||
|
}
|
||||||
|
switch (borderStyle) {
|
||||||
|
case "box":
|
||||||
|
this.borderStrategy = BorderStrategy.BOX_DRAWING;
|
||||||
|
break;
|
||||||
|
case "simple":
|
||||||
|
this.borderStrategy = BorderStrategy.ASCII_TEXT;
|
||||||
|
break;
|
||||||
|
case "none":
|
||||||
|
this.borderStrategy = BorderStrategy.NONE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IOException("Please select a valid border strategy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package io.github.applecommander.bastools.tools.st;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.HelpCommand;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary entry point into the Shape Tools utility.
|
||||||
|
*/
|
||||||
|
@Command(name = "st", mixinStandardHelpOptions = true, versionProvider = VersionProvider.class,
|
||||||
|
descriptionHeading = "%n",
|
||||||
|
commandListHeading = "%nCommands:%n",
|
||||||
|
optionListHeading = "%nOptions:%n",
|
||||||
|
description = "Shape Tools utility",
|
||||||
|
subcommands = {
|
||||||
|
ExtractCommand.class,
|
||||||
|
HelpCommand.class,
|
||||||
|
})
|
||||||
|
public class Main implements Runnable {
|
||||||
|
@Option(names = "--debug", description = "Dump full stack trackes if an error occurs")
|
||||||
|
private static boolean debugFlag;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try {
|
||||||
|
CommandLine.run(new Main(), args);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (Main.debugFlag) {
|
||||||
|
t.printStackTrace(System.err);
|
||||||
|
} else {
|
||||||
|
String message = t.getMessage();
|
||||||
|
while (t != null) {
|
||||||
|
message = t.getMessage();
|
||||||
|
t = t.getCause();
|
||||||
|
}
|
||||||
|
System.err.printf("Error: %s\n", Optional.ofNullable(message).orElse("An error occurred."));
|
||||||
|
}
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
CommandLine.usage(this, System.out);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package io.github.applecommander.bastools.tools.st;
|
||||||
|
|
||||||
|
import io.github.applecommander.applesingle.AppleSingle;
|
||||||
|
import io.github.applecommander.bastools.api.BasTools;
|
||||||
|
import picocli.CommandLine.IVersionProvider;
|
||||||
|
|
||||||
|
/** Display version information. Note that this is dependent on the Spring Boot Gradle plugin configuration. */
|
||||||
|
public class VersionProvider implements IVersionProvider {
|
||||||
|
public String[] getVersion() {
|
||||||
|
return new String[] {
|
||||||
|
String.format("%s: %s", Main.class.getPackage().getImplementationTitle(),
|
||||||
|
Main.class.getPackage().getImplementationVersion()),
|
||||||
|
String.format("%s: %s", BasTools.TITLE, BasTools.VERSION),
|
||||||
|
String.format("AppleSingle API: %s", AppleSingle.VERSION)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue