Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions cli/src/main/java/io/github/dfa1/vortex/cli/CliHandles.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

/// Shared handle plumbing for the interactive subcommands (`view`, `tui`).
/// Shared plumbing for the subcommands: local-file / http(s) target resolution ([#openTarget]),
/// cause-chain rendering ([#describe]), and the worker-thread handle round-trip the interactive
/// `view`/`tui` commands need.
///
/// A [VortexReader] uses a confined [java.lang.foreign.Arena], so the file must be
/// opened and closed on the same [IoWorker] thread the TUI later dispatches I/O to. These
/// helpers centralise that worker round-trip plus the local-file / http(s) target resolution.
/// A [VortexReader] uses a confined [java.lang.foreign.Arena], so for the interactive commands the
/// file must be opened and closed on the same [IoWorker] thread the TUI later dispatches I/O to.
@SuppressWarnings("java:S106") // CLI tool: user-facing diagnostics go to System.err by design.
final class CliHandles {

Expand All @@ -37,7 +38,7 @@ static Optional<VortexHandle> openOnWorker(IoWorker worker, String target)
AtomicReference<IOException> failure = new AtomicReference<>();
worker.runAndAwait(() -> {
try {
handle.set(open(target));
handle.set(openTarget(target));
} catch (IOException e) {
failure.set(e);
}
Expand Down Expand Up @@ -80,7 +81,13 @@ static String describe(Throwable t) {
return sb.toString();
}

private static VortexHandle open(String target) throws IOException {
/// Opens `target` as a local path or `http(s)://` URL, printing a diagnostic and returning
/// `null` when the file is missing or the URL is malformed.
///
/// @param target a local path or an `http(s)://` URL
/// @return the opened handle, or `null` if the target is missing or malformed
/// @throws IOException if opening the file or URL fails
static VortexHandle openTarget(String target) throws IOException {
if (target.startsWith("http://") || target.startsWith("https://")) {
try {
return VortexHttpReader.open(new URI(target));
Expand Down
21 changes: 4 additions & 17 deletions cli/src/main/java/io/github/dfa1/vortex/cli/ExportCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@ static int run(String[] args) {
: deriveOutputPath(inputPath);
try {
ExportOptions options = ExportOptions.defaults()
.withProgressListener(ExportCommand::renderProgress);
.withProgressListener(ProgressBar::render);
if (toStdout) {
Writer stdout = new OutputStreamWriter(System.out, StandardCharsets.UTF_8);
CsvExporter.exportCsv(inputPath, stdout, options);
stdout.flush();
clearProgress();
ProgressBar.clear();
} else {
CsvExporter.exportCsv(inputPath, outputPath, options);
clearProgress();
ProgressBar.clear();
printResult(inputPath, outputPath);
}
return ExitStatus.OK;
} catch (IOException e) {
clearProgress();
ProgressBar.clear();
System.err.println("error: " + e.getMessage());
return ExitStatus.ERROR;
}
Expand All @@ -66,17 +66,4 @@ private static void printResult(Path inputPath, Path outputPath) throws IOExcept
System.out.printf("written: %s (%s → %s)%n",
outputPath, ByteSize.format(inputBytes), ByteSize.format(outputBytes));
}

private static void renderProgress(long done, long total) {
int pct = total > 0 ? (int) (done * 100L / total) : 100;
int filled = pct * 30 / 100;
String bar = "=".repeat(filled) + (filled < 30 ? ">" : "") + " ".repeat(Math.max(0, 29 - filled));
System.err.printf("\r [%s] %3d%% %,d / %,d rows", bar, pct, done, total);
System.err.flush();
}

private static void clearProgress() {
System.err.printf("\r%-80s\r", "");
System.err.flush();
}
}
20 changes: 7 additions & 13 deletions cli/src/main/java/io/github/dfa1/vortex/cli/ImportCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ static int run(String[] args) {
return runCsv(inputPath, parsedArgs.outputPath(), parsedArgs.delimiter());
}
} catch (IOException e) {
clearProgress();
ProgressBar.clear();
System.err.println("error: " + e.getMessage());
return ExitStatus.ERROR;
}
Expand Down Expand Up @@ -84,7 +84,7 @@ private static int runCsv(Path csvPath, Path vortexPath, Character delimiter) th
options = options.withDelimiter(delimiter);
}
CsvImporter.importCsv(csvPath, vortexPath, options);
clearProgress();
ProgressBar.clear();
printResult(csvPath, vortexPath, options.writeOptions().allowedCascading());
return ExitStatus.OK;
}
Expand All @@ -94,7 +94,7 @@ private static int runParquet(Path parquetPath, Path vortexPath) throws IOExcept
io.github.dfa1.vortex.parquet.ImportOptions.defaults()
.withProgressListener(ImportCommand::renderProgress);
ParquetImporter.importParquet(parquetPath, vortexPath, options);
clearProgress();
ProgressBar.clear();
printResult(parquetPath, vortexPath, options.writeOptions().allowedCascading());
return ExitStatus.OK;
}
Expand All @@ -114,21 +114,15 @@ private static void printResult(Path inputPath, Path vortexPath, int cascadingDe
sizeChange, cascadingInfo);
}

/// Progress callback for imports. An indeterminate `total` (`< 0`, e.g. a streamed source with
/// no known row count) shows just the running row count; otherwise delegates to the shared bar.
private static void renderProgress(long done, long total) {
if (total < 0) {
System.err.printf("\r imported %,d rows", done);
System.err.flush();
} else {
int pct = total > 0 ? (int) (done * 100L / total) : 100;
int filled = pct * 30 / 100;
String bar = "=".repeat(filled) + (filled < 30 ? ">" : "") + " ".repeat(Math.max(0, 29 - filled));
System.err.printf("\r [%s] %3d%% %,d / %,d rows", bar, pct, done, total);
ProgressBar.render(done, total);
}
System.err.flush();
}

private static void clearProgress() {
System.err.printf("\r%-80s\r", "");
System.err.flush();
}

private static Path deriveOutputPath(Path inputPath) {
Expand Down
43 changes: 2 additions & 41 deletions cli/src/main/java/io/github/dfa1/vortex/cli/InspectCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@

import io.github.dfa1.vortex.inspect.VortexInspector;
import io.github.dfa1.vortex.reader.VortexHandle;
import io.github.dfa1.vortex.reader.VortexHttpReader;
import io.github.dfa1.vortex.reader.VortexReader;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;

@SuppressWarnings("java:S106") // CLI command: stdout is the intended output channel
final class InspectCommand {
Expand All @@ -22,49 +16,16 @@ static int run(String[] args) {
System.err.println("usage: inspect <file.vortex | http(s)://url>");
return ExitStatus.USAGE_ERROR;
}
try (VortexHandle handle = open(args[1])) {
try (VortexHandle handle = CliHandles.openTarget(args[1])) {
if (handle == null) {
return ExitStatus.FILE_NOT_FOUND;
}
System.out.print(VortexInspector.inspect(handle));
return ExitStatus.OK;
} catch (IOException | RuntimeException e) {
System.err.println("error: " + describe(e));
System.err.println("error: " + CliHandles.describe(e));
e.printStackTrace(System.err);
return ExitStatus.ERROR;
}
}

private static String describe(Throwable t) {
StringBuilder sb = new StringBuilder();
Throwable cur = t;
while (cur != null) {
if (!sb.isEmpty()) {
sb.append(" -> ");
}
sb.append(cur.getClass().getSimpleName());
if (cur.getMessage() != null) {
sb.append(": ").append(cur.getMessage());
}
cur = cur.getCause();
}
return sb.toString();
}

private static VortexHandle open(String target) throws IOException {
if (target.startsWith("http://") || target.startsWith("https://")) {
try {
return VortexHttpReader.open(new URI(target));
} catch (URISyntaxException _) {
System.err.println("invalid URL: " + target);
return null;
}
}
Path path = Path.of(target);
if (!Files.exists(path)) {
System.err.println("file not found: " + path);
return null;
}
return VortexReader.open(path);
}
}
32 changes: 32 additions & 0 deletions cli/src/main/java/io/github/dfa1/vortex/cli/ProgressBar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.github.dfa1.vortex.cli;

/// Renders an in-place (carriage-return overwrite) progress line on stderr, shared by the
/// import/export subcommands. Matches the [io.github.dfa1.vortex.csv.ProgressListener] signature,
/// so it can be passed directly as `ProgressBar::render`.
@SuppressWarnings("java:S106") // progress UI is written to stderr by design
final class ProgressBar {

private static final int WIDTH = 30;

private ProgressBar() {
}

/// Renders a determinate progress bar: `[====> ] NN% done / total rows`. A non-positive
/// `total` reports 100%.
///
/// @param done rows processed so far
/// @param total total rows expected
static void render(long done, long total) {
int pct = total > 0 ? (int) (done * 100L / total) : 100;
int filled = pct * WIDTH / 100;
String bar = "=".repeat(filled) + (filled < WIDTH ? ">" : "") + " ".repeat(Math.max(0, WIDTH - 1 - filled));
System.err.printf("\r [%s] %3d%% %,d / %,d rows", bar, pct, done, total);
System.err.flush();
}

/// Clears the current progress line.
static void clear() {
System.err.printf("\r%-80s\r", "");
System.err.flush();
}
}
47 changes: 47 additions & 0 deletions cli/src/test/java/io/github/dfa1/vortex/cli/ProgressBarTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.github.dfa1.vortex.cli;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
import static org.assertj.core.api.Assertions.assertThat;

class ProgressBarTest {

@ParameterizedTest
@CsvSource({
// done, total, pct, hasMarker ('>' present while the bar is not full)
"0, 100, 0, true",
"50, 100, 50, true",
"100, 100, 100, false", // full bar: all '=', no '>'
"0, 0, 100, false", // non-positive total reports 100%
})
void render_showsPercentAndRowCounts(long done, long total, int pct, boolean hasMarker) {
// Given / When
CliTestSupport.Captured result = capture(() -> {
ProgressBar.render(done, total);
return 0;
});

// Then the line carries the percentage and done/total counts, and a '>' cursor only while
// the bar is partially filled
assertThat(result.stderr())
.contains(String.format("%3d%% %,d / %,d rows", pct, done, total));
assertThat(result.stderr().contains(">")).isEqualTo(hasMarker);
}

@Test
void clear_blanksTheLine() {
// Given / When
CliTestSupport.Captured result = capture(() -> {
ProgressBar.clear();
return 0;
});

// Then the carriage-return + 80-space pad overwrites whatever was on the line
assertThat(result.stderr())
.startsWith("\r")
.contains(" ".repeat(80));
}
}