diff --git a/doc/events/file_transfer.md b/doc/events/file_transfer.md new file mode 100644 index 000000000..f68289c34 --- /dev/null +++ b/doc/events/file_transfer.md @@ -0,0 +1,41 @@ +--- +module: [kind=event] file_transfer +--- + +The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer. + +This event contains a single argument, that in turn has a single method @{TransferredFiles.getFiles|getFiles}. This +returns the list of files that are being transferred. Each file is a @{fs.BinaryReadHandle|binary file handle} with an +additional @{TransferredFile.getName|getName} method. + +## Return values +1. @{string}: The event name +2. @{TransferredFiles}: The list of transferred files. + +## Example +Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file. + +```lua +local _, files = os.pullEvent("file_transfer") +for _, file in ipairs(files.getFiles()) do + -- Seek to the end of the file to get its size, then go back to the beginning. + local size = file.seek("end") + file.seek("set", 0) + + print(file.getName() .. " " .. file.getSize()) +end +``` + +## Example +Save each transferred file to the computer's storage. + +```lua +local _, files = os.pullEvent("file_transfer") +for _, file in ipairs(files.getFiles()) do + local handle = fs.open(file.getName(), "wb") + handle.write(file.readAll()) + + handle.close() + file.close() +end +``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f693343ad..3d7dcd42b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,12 +18,12 @@ jqwik = "1.7.0" junit = "5.9.1" # Build tools -cctJavadoc = "1.5.1" +cctJavadoc = "1.5.2" checkstyle = "8.25" # There's a reason we're pinned on an ancient version, but I can't remember what it is. curseForgeGradle = "1.0.11" forgeGradle = "5.1.+" githubRelease = "2.2.12" -illuaminate = "0.1.0-3-g0f40379" +illuaminate = "0.1.0-7-g2a5a89c" librarian = "1.+" minotaur = "2.+" mixinGradle = "0.7.+" diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index c6417240b..ae597d531 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -82,6 +82,8 @@ public final class ComputerCraft public static int monitorWidth = 8; public static int monitorHeight = 6; + public static int uploadNagDelay = 5; + public static final class TurtleUpgrades { public static TurtleModem wirelessModemNormal; diff --git a/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java b/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java index a4e5ac313..6082771ee 100644 --- a/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java +++ b/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java @@ -17,30 +17,35 @@ import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; import dan200.computercraft.shared.computer.upload.FileUpload; import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.network.NetworkHandler; -import dan200.computercraft.shared.network.server.ContinueUploadMessage; import dan200.computercraft.shared.network.server.UploadFileMessage; import net.minecraft.client.gui.screen.inventory.ContainerScreen; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Util; import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TextFormatting; import net.minecraft.util.text.TranslationTextComponent; import org.lwjgl.glfw.GLFW; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; public abstract class ComputerScreenBase extends ContainerScreen { private static final ITextComponent OK = new TranslationTextComponent( "gui.ok" ); - private static final ITextComponent CANCEL = new TranslationTextComponent( "gui.cancel" ); - private static final ITextComponent OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite_button" ); + private static final ITextComponent NO_RESPONSE_TITLE = new TranslationTextComponent( "gui.computercraft.upload.no_response" ); + private static final ITextComponent NO_RESPONSE_MSG = new TranslationTextComponent( "gui.computercraft.upload.no_response.msg", + new StringTextComponent( "import" ).withStyle( TextFormatting.DARK_GRAY ) ); protected WidgetTerminal terminal; protected Terminal terminalData; @@ -49,11 +54,15 @@ public abstract class ComputerScreenBase extend protected final int sidebarYOffset; + private long uploadNagDeadline = Long.MAX_VALUE; + private final ItemStack displayStack; + public ComputerScreenBase( T container, PlayerInventory player, ITextComponent title, int sidebarYOffset ) { super( container, player, title ); terminalData = container.getTerminal(); family = container.getFamily(); + displayStack = container.getDisplayStack(); input = new ClientInputHandler( menu ); this.sidebarYOffset = sidebarYOffset; } @@ -83,6 +92,13 @@ public abstract class ComputerScreenBase extend { super.tick(); terminal.update(); + + if( uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline ) + { + new ItemToast( minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN ) + .showOrReplace( minecraft.getToasts() ); + uploadNagDeadline = Long.MAX_VALUE; + } } @Override @@ -194,41 +210,29 @@ public abstract class ComputerScreenBase extend if( toUpload.size() > 0 ) UploadFileMessage.send( menu, toUpload, NetworkHandler::sendToServer ); } - public void uploadResult( UploadResult result, ITextComponent message ) + public void uploadResult( UploadResult result, @Nullable ITextComponent message ) { switch( result ) { - case SUCCESS: - alert( UploadResult.SUCCESS_TITLE, message ); + case QUEUED: + { + if( ComputerCraft.uploadNagDelay > 0 ) + { + uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos( ComputerCraft.uploadNagDelay ); + } break; + } + case CONSUMED: + { + uploadNagDeadline = Long.MAX_VALUE; + break; + } case ERROR: alert( UploadResult.FAILED_TITLE, message ); break; - case CONFIRM_OVERWRITE: - OptionScreen.show( - minecraft, UploadResult.UPLOAD_OVERWRITE, message, - Arrays.asList( - OptionScreen.newButton( CANCEL, b -> cancelUpload() ), - OptionScreen.newButton( OVERWRITE, b -> continueUpload() ) - ), - this::cancelUpload - ); - break; } } - private void continueUpload() - { - if( minecraft.screen instanceof OptionScreen ) ((OptionScreen) minecraft.screen).disable(); - NetworkHandler.sendToServer( new ContinueUploadMessage( menu, true ) ); - } - - private void cancelUpload() - { - minecraft.setScreen( this ); - NetworkHandler.sendToServer( new ContinueUploadMessage( menu, false ) ); - } - private void alert( ITextComponent title, ITextComponent message ) { OptionScreen.show( minecraft, title, message, diff --git a/src/main/java/dan200/computercraft/client/gui/ItemToast.java b/src/main/java/dan200/computercraft/client/gui/ItemToast.java new file mode 100644 index 000000000..59474a40b --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/ItemToast.java @@ -0,0 +1,150 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.client.gui; + +import com.mojang.blaze3d.matrix.MatrixStack; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.toasts.IToast; +import net.minecraft.client.gui.toasts.ToastGui; +import net.minecraft.item.ItemStack; +import net.minecraft.util.IReorderingProcessor; +import net.minecraft.util.text.ITextComponent; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A {@link IToast} implementation which displays an arbitrary message along with an optional {@link ItemStack}. + */ +public class ItemToast implements IToast +{ + public static final Object TRANSFER_NO_RESPONSE_TOKEN = new Object(); + + private static final long DISPLAY_TIME = 7000L; + private static final int MAX_LINE_SIZE = 200; + + private static final int IMAGE_SIZE = 16; + private static final int LINE_SPACING = 10; + private static final int MARGIN = 8; + + private final ItemStack stack; + private final ITextComponent title; + private final List message; + private final Object token; + private final int width; + + private boolean isNew = true; + private long firstDisplay; + + public ItemToast( Minecraft minecraft, ItemStack stack, ITextComponent title, ITextComponent message, Object token ) + { + this.stack = stack; + this.title = title; + this.token = token; + + FontRenderer font = minecraft.font; + this.message = font.split( message, MAX_LINE_SIZE ); + width = Math.max( MAX_LINE_SIZE, this.message.stream().mapToInt( font::width ).max().orElse( MAX_LINE_SIZE ) ) + MARGIN * 3 + IMAGE_SIZE; + } + + public void showOrReplace( ToastGui toasts ) + { + ItemToast existing = toasts.getToast( ItemToast.class, getToken() ); + if( existing != null ) + { + existing.isNew = true; + } + else + { + toasts.addToast( this ); + } + } + + @Override + public int width() + { + return width; + } + + @Override + public int height() + { + return MARGIN * 2 + LINE_SPACING + message.size() * LINE_SPACING; + } + + @Nonnull + @Override + public Object getToken() + { + return token; + } + + @Nonnull + @Override + public Visibility render( @Nonnull MatrixStack transform, @Nonnull ToastGui component, long time ) + { + if( isNew ) + { + + firstDisplay = time; + isNew = false; + } + + component.getMinecraft().getTextureManager().bind( TEXTURE ); + RenderSystem.color3f( 1.0F, 1.0F, 1.0F ); + + if( width == 160 && message.size() <= 1 ) + { + component.blit( transform, 0, 0, 0, 64, width, height() ); + } + else + { + + int height = height(); + + int bottom = Math.min( 4, height - 28 ); + renderBackgroundRow( transform, component, width, 0, 0, 28 ); + + for( int i = 28; i < height - bottom; i += 10 ) + { + renderBackgroundRow( transform, component, width, 16, i, Math.min( 16, height - i - bottom ) ); + } + + renderBackgroundRow( transform, component, width, 32 - bottom, height - bottom, bottom ); + } + + int textX = MARGIN; + if( !stack.isEmpty() ) + { + textX += MARGIN + IMAGE_SIZE; + component.getMinecraft().getItemRenderer().renderAndDecorateFakeItem( stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE ); + } + + component.getMinecraft().font.draw( transform, title, textX, MARGIN, 0xff500050 ); + for( int i = 0; i < message.size(); ++i ) + { + component.getMinecraft().font.draw( transform, message.get( i ), textX, (float) (LINE_SPACING + (i + 1) * LINE_SPACING), 0xff000000 ); + } + + return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE; + } + + private static void renderBackgroundRow( MatrixStack transform, ToastGui component, int x, int u, int y, int height ) + { + int leftOffset = 5; + int rightOffset = Math.min( 60, x - leftOffset ); + + component.blit( transform, 0, y, 0, 32 + u, leftOffset, height ); + for( int k = leftOffset; k < x - rightOffset; k += 64 ) + { + component.blit( transform, k, y, 32, 32 + u, Math.min( 64, x - k - rightOffset ), height ); + } + + component.blit( transform, x - rightOffset, y, 160 - rightOffset, 32 + u, rightOffset, height ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/ByteBufferChannel.java b/src/main/java/dan200/computercraft/core/apis/handles/ByteBufferChannel.java new file mode 100644 index 000000000..b7568e8da --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/ByteBufferChannel.java @@ -0,0 +1,99 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.apis.handles; + +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; +import java.util.Objects; + +/** + * A seekable, readable byte channel which is backed by a {@link ByteBuffer}. + */ +public class ByteBufferChannel implements SeekableByteChannel +{ + private boolean closed = false; + private int position = 0; + + private final ByteBuffer backing; + + public ByteBufferChannel( ByteBuffer backing ) + { + this.backing = backing; + } + + @Override + public int read( ByteBuffer destination ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + Objects.requireNonNull( destination, "destination" ); + + if( position >= backing.limit() ) return -1; + + int remaining = Math.min( backing.limit() - position, destination.remaining() ); + + // TODO: Switch to Java 17 methods on 1.18.x + ByteBuffer slice = backing.slice(); + slice.position( position ); + slice.limit( position + remaining ); + destination.put( slice ); + position += remaining; + return remaining; + } + + @Override + public int write( ByteBuffer src ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public long position() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return position; + } + + @Override + public SeekableByteChannel position( long newPosition ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + if( newPosition < 0 || newPosition > Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "Position out of bounds" ); + } + position = (int) newPosition; + return this; + } + + @Override + public long size() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return backing.limit(); + } + + @Override + public SeekableByteChannel truncate( long size ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public boolean isOpen() + { + return !closed; + } + + @Override + public void close() + { + closed = true; + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java index 131bbb551..3e46fb1b9 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java @@ -293,6 +293,7 @@ public final class ResourceMount implements IMount try { for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager ); + CONTENTS_CACHE.invalidateAll(); } finally { diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java index 882c4849f..9b2e3a832 100644 --- a/src/main/java/dan200/computercraft/shared/Config.java +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -17,7 +17,6 @@ import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.AddressRuleConfig; import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; import net.minecraftforge.common.ForgeConfigSpec; -import net.minecraftforge.common.ForgeConfigSpec.Builder; import net.minecraftforge.common.ForgeConfigSpec.ConfigValue; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.ModLoadingContext; @@ -86,6 +85,7 @@ public final class Config private static final ConfigValue monitorRenderer; private static final ConfigValue monitorDistance; + private static final ConfigValue uploadNagDelay; private static final ForgeConfigSpec serverSpec; private static final ForgeConfigSpec clientSpec; @@ -94,7 +94,7 @@ public final class Config static { - Builder builder = new Builder(); + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); { // General computers computerSpaceLimit = builder @@ -282,13 +282,17 @@ public final class Config serverSpec = builder.build(); - Builder clientBuilder = new Builder(); + ForgeConfigSpec.Builder clientBuilder = new ForgeConfigSpec.Builder(); monitorRenderer = clientBuilder .comment( "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers." ) .defineEnum( "monitor_renderer", MonitorRenderer.BEST ); monitorDistance = clientBuilder .comment( "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors." ) .defineInRange( "monitor_distance", 64, 16, 1024 ); + uploadNagDelay = clientBuilder + .comment( "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable." ) + .defineInRange( "upload_nag_delay", ComputerCraft.uploadNagDelay, 0, 60 ); + clientSpec = clientBuilder.build(); } @@ -357,6 +361,7 @@ public final class Config // Client ComputerCraft.monitorRenderer = monitorRenderer.get(); ComputerCraft.monitorDistanceSq = monitorDistance.get() * monitorDistance.get(); + ComputerCraft.uploadNagDelay = uploadNagDelay.get(); } @SubscribeEvent diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index c25a99152..a71235e74 100644 --- a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -28,6 +28,7 @@ import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.inventory.container.Container; import net.minecraft.inventory.container.INamedContainerProvider; +import net.minecraft.item.ItemStack; import net.minecraft.network.play.server.SPlayerPositionLookPacket; import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.IFormattableTextComponent; @@ -225,7 +226,7 @@ public final class CommandComputerCraft .executes( context -> { ServerPlayerEntity player = context.getSource().getPlayerOrException(); ServerComputer computer = getComputerArgument( context, "computer" ); - new ComputerContainerData( computer ).open( player, new INamedContainerProvider() + new ComputerContainerData( computer, ItemStack.EMPTY ).open( player, new INamedContainerProvider() { @Nonnull @Override diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java index d19a74440..01b87baa3 100644 --- a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java @@ -146,7 +146,11 @@ public abstract class TileComputerBase extends TileGeneric implements IComputerT { ServerComputer computer = createServerComputer(); computer.turnOn(); - new ComputerContainerData( computer ).open( player, this ); + + ItemStack stack = getBlockState().getBlock() instanceof BlockComputerBase + ? ((BlockComputerBase) getBlockState().getBlock()).getItem( this ) + : ItemStack.EMPTY; + new ComputerContainerData( computer, stack ).open( player, this ); } return ActionResultType.SUCCESS; } diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java index 67bba7602..08109cf69 100644 --- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java @@ -9,6 +9,7 @@ import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.menu.ComputerMenu; +import dan200.computercraft.shared.computer.menu.ServerInputHandler; import dan200.computercraft.shared.computer.menu.ServerInputState; import dan200.computercraft.shared.network.client.TerminalState; import dan200.computercraft.shared.network.container.ComputerContainerData; @@ -16,6 +17,7 @@ import dan200.computercraft.shared.util.SingleIntArray; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.inventory.container.Container; import net.minecraft.inventory.container.ContainerType; +import net.minecraft.item.ItemStack; import net.minecraft.util.IIntArray; import net.minecraft.util.IntArray; @@ -30,10 +32,12 @@ public abstract class ContainerComputerBase extends Container implements Compute private final IIntArray data; private final @Nullable ServerComputer computer; - private final @Nullable ServerInputState input; + private final @Nullable ServerInputState input; private final @Nullable Terminal terminal; + private final ItemStack displayStack; + public ContainerComputerBase( ContainerType type, int id, Predicate canUse, ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData containerData @@ -46,8 +50,9 @@ public abstract class ContainerComputerBase extends Container implements Compute addDataSlots( data ); this.computer = computer; - input = computer == null ? null : new ServerInputState( this ); + input = computer == null ? null : new ServerInputState<>( this ); terminal = containerData == null ? null : containerData.terminal().create(); + displayStack = containerData == null ? null : containerData.displayStack(); } @Override @@ -75,7 +80,7 @@ public abstract class ContainerComputerBase extends Container implements Compute } @Override - public ServerInputState getInput() + public ServerInputHandler getInput() { if( input == null ) throw new UnsupportedOperationException( "Cannot access server computer on the client" ); return input; @@ -106,4 +111,15 @@ public abstract class ContainerComputerBase extends Container implements Compute super.removed( player ); if( input != null ) input.close(); } + + /** + * Get the stack associated with this container. + * + * @return The current stack. + */ + @Nonnull + public ItemStack getDisplayStack() + { + return displayStack; + } } diff --git a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java index e6f6bf8f2..815e21c6c 100644 --- a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java +++ b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java @@ -46,12 +46,4 @@ public interface ServerInputHandler extends InputHandler * @param uploadId The unique ID of this upload. */ void finishUpload( ServerPlayerEntity uploader, UUID uploadId ); - - /** - * Continue an upload. - * - * @param uploader The player uploading files. - * @param overwrite Whether the files should be overwritten or not. - */ - void confirmUpload( ServerPlayerEntity uploader, boolean overwrite ); } diff --git a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java index d8ee57b9f..9a05f0e11 100644 --- a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java +++ b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java @@ -6,13 +6,8 @@ package dan200.computercraft.shared.computer.menu; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.core.filesystem.FileSystem; -import dan200.computercraft.core.filesystem.FileSystemException; -import dan200.computercraft.core.filesystem.FileSystemWrapper; import dan200.computercraft.shared.computer.core.ServerComputer; -import dan200.computercraft.shared.computer.upload.FileSlice; -import dan200.computercraft.shared.computer.upload.FileUpload; -import dan200.computercraft.shared.computer.upload.UploadResult; +import dan200.computercraft.shared.computer.upload.*; import dan200.computercraft.shared.network.NetworkHandler; import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.client.UploadResultMessage; @@ -20,27 +15,24 @@ import it.unimi.dsi.fastutil.ints.IntIterator; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.inventory.container.Container; import net.minecraft.util.text.TranslationTextComponent; import javax.annotation.Nullable; -import java.io.IOException; -import java.nio.channels.WritableByteChannel; -import java.util.ArrayList; import java.util.List; -import java.util.StringJoiner; import java.util.UUID; -import java.util.function.Function; +import java.util.stream.Collectors; /** * The default concrete implementation of {@link ServerInputHandler}. *

* This keeps track of the current key and mouse state, and releases them when the container is closed. + * + * @param The type of container this server input belongs to. */ -public class ServerInputState implements ServerInputHandler +public class ServerInputState implements ServerInputHandler { - private static final String LIST_PREFIX = "\n \u2022 "; - - private final ComputerMenu owner; + private final T owner; private final IntSet keysDown = new IntOpenHashSet( 4 ); private int lastMouseX; @@ -50,7 +42,7 @@ public class ServerInputState implements ServerInputHandler private @Nullable UUID toUploadId; private @Nullable List toUpload; - public ServerInputState( ComputerMenu owner ) + public ServerInputState( T owner ) { this.owner = owner; } @@ -160,91 +152,31 @@ public class ServerInputState implements ServerInputHandler return; } - NetworkMessage message = finishUpload( false ); + NetworkMessage message = finishUpload( uploader ); NetworkHandler.sendToPlayer( uploader, message ); } - @Override - public void confirmUpload( ServerPlayerEntity uploader, boolean overwrite ) - { - if( toUploadId == null || toUpload == null || toUpload.isEmpty() ) - { - ComputerCraft.log.warn( "Invalid finishUpload call, skipping." ); - return; - } - - NetworkMessage message = finishUpload( true ); - NetworkHandler.sendToPlayer( uploader, message ); - } - - private UploadResultMessage finishUpload( boolean forceOverwrite ) + private UploadResultMessage finishUpload( ServerPlayerEntity player ) { ServerComputer computer = owner.getComputer(); - if( toUpload == null ) return UploadResultMessage.COMPUTER_OFF; - - FileSystem fs = computer.getComputer().getAPIEnvironment().getFileSystem(); + if( toUpload == null ) + { + return UploadResultMessage.error( owner, UploadResult.COMPUTER_OFF_MSG ); + } for( FileUpload upload : toUpload ) { if( !upload.checksumMatches() ) { ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() ); - return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) ); + return UploadResultMessage.error( owner, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) ); } } - try - { - List overwrite = new ArrayList<>(); - List files = toUpload; - toUpload = null; - for( FileUpload upload : files ) - { - if( !fs.exists( upload.getName() ) ) continue; - if( fs.isDir( upload.getName() ) ) - { - return new UploadResultMessage( - UploadResult.ERROR, - new TranslationTextComponent( "gui.computercraft.upload.failed.overwrite_dir", upload.getName() ) - ); - } - - overwrite.add( upload.getName() ); - } - - if( !overwrite.isEmpty() && !forceOverwrite ) - { - StringJoiner joiner = new StringJoiner( LIST_PREFIX, LIST_PREFIX, "" ); - for( String value : overwrite ) joiner.add( value ); - toUpload = files; - return new UploadResultMessage( - UploadResult.CONFIRM_OVERWRITE, - new TranslationTextComponent( "gui.computercraft.upload.overwrite.detail", joiner.toString() ) - ); - } - - long availableSpace = fs.getFreeSpace( "/" ); - long neededSpace = 0; - for( FileUpload upload : files ) neededSpace += Math.max( 512, upload.getBytes().remaining() ); - if( neededSpace > availableSpace ) return UploadResultMessage.OUT_OF_SPACE; - - for( FileUpload file : files ) - { - try( FileSystemWrapper channel = fs.openForWrite( file.getName(), false, Function.identity() ) ) - { - channel.get().write( file.getBytes() ); - } - } - - return new UploadResultMessage( - UploadResult.SUCCESS, new TranslationTextComponent( "gui.computercraft.upload.success.msg", files.size() ) - ); - } - catch( FileSystemException | IOException e ) - { - ComputerCraft.log.error( "Error uploading files", e ); - return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.generic", e.getMessage() ) ); - } + computer.queueEvent( "file_transfer", new Object[] { + new TransferredFiles( player, owner, toUpload.stream().map( x -> new TransferredFile( x.getName(), x.getBytes() ) ).collect( Collectors.toList() ) ), + } ); + return UploadResultMessage.queued( owner ); } public void close() diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java new file mode 100644 index 000000000..51c32494e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java @@ -0,0 +1,53 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.computer.upload; + +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.ByteBufferChannel; +import dan200.computercraft.core.asm.ObjectSource; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Optional; + +/** + * A binary file handle that has been transferred to this computer. + *

+ * This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard + * {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file. + * + * @cc.module [kind=event] file_transfer.TransferredFile + * @see BinaryReadableHandle + */ +public class TransferredFile implements ObjectSource +{ + private final String name; + private final BinaryReadableHandle handle; + + public TransferredFile( String name, ByteBuffer contents ) + { + this.name = name; + handle = BinaryReadableHandle.of( new ByteBufferChannel( contents ) ); + } + + /** + * Get the name of this file being transferred. + * + * @return The file's name. + */ + @LuaFunction + public final String getName() + { + return name; + } + + @Override + public Iterable getExtra() + { + return Collections.singleton( handle ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFiles.java b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFiles.java new file mode 100644 index 000000000..c4ea7f2a8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFiles.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.computer.upload; + +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.UploadResultMessage; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.inventory.container.Container; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A list of files that have been transferred to this computer. + * + * @cc.module [kind=event] file_transfer.TransferredFiles + */ +public class TransferredFiles +{ + private final ServerPlayerEntity player; + private final Container container; + private final AtomicBoolean consumed = new AtomicBoolean( false ); + + private final List files; + + public TransferredFiles( ServerPlayerEntity player, Container container, List files ) + { + this.player = player; + this.container = container; + this.files = files; + } + + /** + * All the files that are being transferred to this computer. + * + * @return The list of files. + */ + @LuaFunction + public final List getFiles() + { + consumed(); + return files; + } + + private void consumed() + { + if( consumed.getAndSet( true ) ) return; + + if( player.isAlive() && player.containerMenu == container ) + { + NetworkHandler.sendToPlayer( player, UploadResultMessage.consumed( container ) ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java b/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java index 622fe8ad6..d785080cd 100644 --- a/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java +++ b/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java @@ -10,16 +10,13 @@ import net.minecraft.util.text.TranslationTextComponent; public enum UploadResult { - SUCCESS, - ERROR, - CONFIRM_OVERWRITE; + QUEUED, + CONSUMED, + ERROR; public static final ITextComponent SUCCESS_TITLE = new TranslationTextComponent( "gui.computercraft.upload.success" ); public static final ITextComponent FAILED_TITLE = new TranslationTextComponent( "gui.computercraft.upload.failed" ); public static final ITextComponent COMPUTER_OFF_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.computer_off" ); - public static final ITextComponent OUT_OF_SPACE_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.out_of_space" ); public static final ITextComponent TOO_MUCH_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.too_much" ); - - public static final ITextComponent UPLOAD_OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite" ); } diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java index 9dc79a00b..663f38caf 100644 --- a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java +++ b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java @@ -50,7 +50,6 @@ public final class NetworkHandler registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage.class, KeyEventServerMessage::new ); registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage.class, MouseEventServerMessage::new ); registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, UploadFileMessage.class, UploadFileMessage::new ); - registerMainThread( 5, NetworkDirection.PLAY_TO_SERVER, ContinueUploadMessage.class, ContinueUploadMessage::new ); // Client messages registerMainThread( 10, NetworkDirection.PLAY_TO_CLIENT, ChatTableClientMessage.class, ChatTableClientMessage::new ); diff --git a/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java b/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java index 877d02de7..1cccc2014 100644 --- a/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java +++ b/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java @@ -11,37 +11,55 @@ import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.network.NetworkMessage; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.inventory.container.Container; import net.minecraft.network.PacketBuffer; import net.minecraft.util.text.ITextComponent; import net.minecraftforge.fml.network.NetworkEvent; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class UploadResultMessage implements NetworkMessage { - public static final UploadResultMessage COMPUTER_OFF = new UploadResultMessage( UploadResult.ERROR, UploadResult.COMPUTER_OFF_MSG ); - public static final UploadResultMessage OUT_OF_SPACE = new UploadResultMessage( UploadResult.ERROR, UploadResult.OUT_OF_SPACE_MSG ); - + private final int containerId; private final UploadResult result; - private final ITextComponent message; + private final ITextComponent errorMessage; - public UploadResultMessage( UploadResult result, ITextComponent message ) + private UploadResultMessage( Container container, UploadResult result, @Nullable ITextComponent errorMessage ) { + containerId = container.containerId; this.result = result; - this.message = message; + this.errorMessage = errorMessage; + } + + public static UploadResultMessage queued( Container container ) + { + return new UploadResultMessage( container, UploadResult.QUEUED, null ); + } + + public static UploadResultMessage consumed( Container container ) + { + return new UploadResultMessage( container, UploadResult.CONSUMED, null ); + } + + public static UploadResultMessage error( Container container, ITextComponent errorMessage ) + { + return new UploadResultMessage( container, UploadResult.ERROR, errorMessage ); } public UploadResultMessage( @Nonnull PacketBuffer buf ) { + containerId = buf.readVarInt(); result = buf.readEnum( UploadResult.class ); - message = buf.readComponent(); + errorMessage = result == UploadResult.ERROR ? buf.readComponent() : null; } @Override public void toBytes( @Nonnull PacketBuffer buf ) { + buf.writeVarInt( containerId ); buf.writeEnum( result ); - buf.writeComponent( message ); + if( result == UploadResult.ERROR ) buf.writeComponent( errorMessage ); } @Override @@ -50,9 +68,9 @@ public class UploadResultMessage implements NetworkMessage Minecraft minecraft = Minecraft.getInstance(); Screen screen = OptionScreen.unwrap( minecraft.screen ); - if( screen instanceof ComputerScreenBase ) + if( screen instanceof ComputerScreenBase && ((ComputerScreenBase) screen).getMenu().containerId == containerId ) { - ((ComputerScreenBase) screen).uploadResult( result, message ); + ((ComputerScreenBase) screen).uploadResult( result, errorMessage ); } } } diff --git a/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java index 926029ec2..162268dc3 100644 --- a/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java +++ b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java @@ -8,23 +8,29 @@ package dan200.computercraft.shared.network.container; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.network.client.TerminalState; +import net.minecraft.item.ItemStack; import net.minecraft.network.PacketBuffer; +import javax.annotation.Nonnull; + public class ComputerContainerData implements ContainerData { private final ComputerFamily family; private final TerminalState terminal; + private final ItemStack displayStack; - public ComputerContainerData( ServerComputer computer ) + public ComputerContainerData( ServerComputer computer, @Nonnull ItemStack displayStack ) { family = computer.getFamily(); terminal = computer.getTerminalState(); + this.displayStack = displayStack; } public ComputerContainerData( PacketBuffer buf ) { family = buf.readEnum( ComputerFamily.class ); terminal = new TerminalState( buf ); + displayStack = buf.readItem(); } @Override @@ -32,6 +38,7 @@ public class ComputerContainerData implements ContainerData { buf.writeEnum( family ); terminal.write( buf ); + buf.writeItemStack( displayStack, true ); } public ComputerFamily family() @@ -43,4 +50,15 @@ public class ComputerContainerData implements ContainerData { return terminal; } + + /** + * Get a stack associated with this menu. This may be displayed on the client. + * + * @return The stack associated with this menu. + */ + @Nonnull + public ItemStack displayStack() + { + return displayStack; + } } diff --git a/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java b/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java deleted file mode 100644 index 4b4973643..000000000 --- a/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of ComputerCraft - http://www.computercraft.info - * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. - * Send enquiries to dratcliffe@gmail.com - */ -package dan200.computercraft.shared.network.server; - -import dan200.computercraft.shared.computer.menu.ComputerMenu; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.inventory.container.Container; -import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.network.NetworkEvent; - -import javax.annotation.Nonnull; - -public class ContinueUploadMessage extends ComputerServerMessage -{ - private final boolean overwrite; - - public ContinueUploadMessage( Container menu, boolean overwrite ) - { - super( menu ); - this.overwrite = overwrite; - } - - public ContinueUploadMessage( @Nonnull PacketBuffer buf ) - { - super( buf ); - overwrite = buf.readBoolean(); - } - - @Override - public void toBytes( @Nonnull PacketBuffer buf ) - { - super.toBytes( buf ); - buf.writeBoolean( overwrite ); - } - - @Override - protected void handle( NetworkEvent.Context context, @Nonnull ComputerMenu container ) - { - ServerPlayerEntity player = context.getSender(); - if( player != null ) container.getInput().confirmUpload( player, overwrite ); - } -} diff --git a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java index a067cb8c2..e1c3987a3 100644 --- a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java +++ b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java @@ -168,7 +168,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I if( !stop ) { boolean isTypingOnly = hand == Hand.OFF_HAND; - new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) ); + new ComputerContainerData( computer, stack ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) ); } } return new ActionResult<>( ActionResultType.SUCCESS, stack ); diff --git a/src/main/resources/assets/computercraft/lang/en_us.json b/src/main/resources/assets/computercraft/lang/en_us.json index f40d1d927..33de587af 100644 --- a/src/main/resources/assets/computercraft/lang/en_us.json +++ b/src/main/resources/assets/computercraft/lang/en_us.json @@ -119,16 +119,13 @@ "gui.computercraft.upload.success": "Upload Succeeded", "gui.computercraft.upload.success.msg": "%d files uploaded.", "gui.computercraft.upload.failed": "Upload Failed", - "gui.computercraft.upload.failed.out_of_space": "Not enough space on the computer for these files.", "gui.computercraft.upload.failed.computer_off": "You must turn the computer on before uploading files.", "gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.", "gui.computercraft.upload.failed.name_too_long": "File names are too long to be uploaded.", "gui.computercraft.upload.failed.too_many_files": "Cannot upload this many files.", - "gui.computercraft.upload.failed.overwrite_dir": "Cannot upload %s, as there is already a directory with the same name.", "gui.computercraft.upload.failed.generic": "Uploading files failed (%s)", "gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.", - "gui.computercraft.upload.overwrite": "Files would be overwritten", - "gui.computercraft.upload.overwrite.detail": "The following files will be overwritten when uploading. Continue?%s", - "gui.computercraft.upload.overwrite_button": "Overwrite", + "gui.computercraft.upload.no_response": "Transferring Files", + "gui.computercraft.upload.no_response.msg": "Your computer has not used your transferred files. You may need to run the %s program and try again.", "gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close." } diff --git a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua index 6daab3f8b..4e59e1ad9 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -211,6 +211,8 @@ local function tabulateCommon(bPaged, ...) end print() end + + local previous_colour = term.getTextColour() for _, t in ipairs(tAll) do if type(t) == "table" then if #t > 0 then @@ -220,6 +222,7 @@ local function tabulateCommon(bPaged, ...) term.setTextColor(t) end end + term.setTextColor(previous_colour) end --[[- Prints tables in a structured form. diff --git a/src/main/resources/data/computercraft/lua/rom/modules/internal/cc/import.lua b/src/main/resources/data/computercraft/lua/rom/modules/internal/cc/import.lua new file mode 100644 index 000000000..2b8f26db0 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/internal/cc/import.lua @@ -0,0 +1,70 @@ +-- Internal module for handling file uploads. This has NO stability guarantees, +-- and so SHOULD NOT be relyed on in user code. + +local completion = require "cc.completion" + +return function(files) + local overwrite = {} + for _, file in pairs(files) do + local filename = file.getName() + local path = shell.resolve(filename) + if fs.exists(path) then + if fs.isDir(path) then + return nil, filename .. " is already a directory." + end + + overwrite[#overwrite + 1] = filename + end + end + + if #overwrite > 0 then + table.sort(overwrite) + printError("The following files will be overwritten:") + textutils.pagedTabulate(colours.cyan, overwrite) + + while true do + io.write("Overwrite? (yes/no) ") + local input = read(nil, nil, function(t) + return completion.choice(t, { "yes", "no" }) + end) + if not input then return end + + input = input:lower() + if input == "" or input == "yes" or input == "y" then + break + elseif input == "no" or input == "n" then + return + end + end + end + + for _, file in pairs(files) do + local filename = file.getName() + print("Transferring " .. filename) + + local path = shell.resolve(filename) + local handle, err = fs.open(path, "wb") + if not handle then return nil, err end + + -- Write the file without loading it all into memory. This uses the same buffer size + -- as BinaryReadHandle. It would be really nice to have a way to do this without + -- multiple copies. + while true do + local chunk = file.read(8192) + if not chunk then break end + + local ok, err = pcall(handle.write, chunk) + if not ok then + handle.close() + + -- Probably an out-of-space issue, just bail. + if err:sub(1, 7) == "pcall: " then err = err:sub(8) end + return nil, "Failed to write file (" .. err .. "). File may be corrupted" + end + end + + handle.close() + end + + return true +end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua b/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua index 59de7153d..51a5b4fd5 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua @@ -331,9 +331,8 @@ while #tProcesses > 0 do resizeWindows() redrawMenu() - elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" then - -- Keyboard event - -- Passthrough to current process + elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then + -- Basic input, just passthrough to current process resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n)) if cullProcess(nCurrentProcess) then setMenuVisible(#tProcesses >= 2) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/import.lua b/src/main/resources/data/computercraft/lua/rom/programs/import.lua new file mode 100644 index 000000000..54c073864 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/programs/import.lua @@ -0,0 +1,24 @@ +require "cc.completion" + +print("Drop files to transfer them to this computer") + +local files +while true do + local event, arg = os.pullEvent() + if event == "file_transfer" then + files = arg.getFiles() + break + elseif event == "key" and arg == keys.q then + return + end +end + +if #files == 0 then + printError("No files to transfer") + return +end + +package.path = package.path .. "/rom/modules/internal/?.lua" + +local ok, err = require("cc.import")(files) +if not ok and err then printError(err) end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua index 19c1bb02d..d4e150790 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua @@ -10,7 +10,6 @@ -- -- @module[module] shell -local expect = dofile("rom/modules/main/cc/expect.lua").expect local make_package = dofile("rom/modules/main/cc/require.lua").make local multishell = multishell @@ -35,6 +34,14 @@ local function createShellEnv(dir) return env end +-- Set up a dummy require based on the current shell, for loading some of our internal dependencies. +local require +do + local env = setmetatable(createShellEnv("/rom/modules/internal"), { __index = _ENV }) + require = env.require +end +local expect = require("cc.expect").expect + -- Colours local promptColour, textColour, bgColour if term.isColour() then @@ -591,6 +598,13 @@ if #tArgs > 0 then shell.run(...) else + local function show_prompt() + term.setBackgroundColor(bgColour) + term.setTextColour(promptColour) + write(shell.dir() .. "> ") + term.setTextColour(textColour) + end + -- "shell" -- Print the header term.setBackgroundColor(bgColour) @@ -607,21 +621,49 @@ else local tCommandHistory = {} while not bExit do term.redirect(parentTerm) - term.setBackgroundColor(bgColour) - term.setTextColour(promptColour) - write(shell.dir() .. "> ") - term.setTextColour(textColour) + show_prompt() - local sLine - if settings.get("shell.autocomplete") then - sLine = read(nil, tCommandHistory, shell.complete) - else - sLine = read(nil, tCommandHistory) + local complete + if settings.get("shell.autocomplete") then complete = shell.complete end + + local ok, result + local co = coroutine.create(read) + assert(coroutine.resume(co, nil, tCommandHistory, complete)) + + while coroutine.status(co) ~= "dead" do + local event = table.pack(os.pullEvent()) + if event[1] == "file_transfer" then + -- Abandon the current prompt + local _, h = term.getSize() + local _, y = term.getCursorPos() + if y == h then + term.scroll(1) + term.setCursorPos(1, y) + else + term.setCursorPos(1, y + 1) + end + term.setCursorBlink(false) + + -- Run the import script with the provided files + local ok, err = require("cc.import")(event[2].getFiles()) + if not ok and err then printError(err) end + + -- And attempt to restore the prompt. + show_prompt() + term.setCursorBlink(true) + event = { "term_resize", n = 1 } -- Nasty hack to force read() to redraw. + end + + if result == nil or event[1] == result or event[1] == "terminate" then + ok, result = coroutine.resume(co, table.unpack(event, 1, event.n)) + if not ok then error(result, 0) end + end end - if sLine:match("%S") and tCommandHistory[#tCommandHistory] ~= sLine then - table.insert(tCommandHistory, sLine) + + if result:match("%S") and tCommandHistory[#tCommandHistory] ~= result then + table.insert(tCommandHistory, result) end - shell.run(sLine) + shell.run(result) end end diff --git a/src/test/java/dan200/computercraft/support/CustomMatchers.java b/src/test/java/dan200/computercraft/support/CustomMatchers.java index 2388fe060..7a1d4e71a 100644 --- a/src/test/java/dan200/computercraft/support/CustomMatchers.java +++ b/src/test/java/dan200/computercraft/support/CustomMatchers.java @@ -6,13 +6,12 @@ package dan200.computercraft.support; import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; -import static org.hamcrest.Matchers.contains; - public class CustomMatchers { /** @@ -27,6 +26,7 @@ public class CustomMatchers */ public static Matcher> containsWith( List items, Function> matcher ) { - return contains( items.stream().map( matcher ).collect( Collectors.toList() ) ); + // The explicit type argument should be redundant, but it appears some Java compilers require it. + return Matchers.contains( items.stream().map( matcher ).collect( Collectors.toList() ) ); } } diff --git a/src/test/resources/test-rom/spec/programs/shell_spec.lua b/src/test/resources/test-rom/spec/programs/shell_spec.lua index fae7b85d1..0e88983ae 100644 --- a/src/test/resources/test-rom/spec/programs/shell_spec.lua +++ b/src/test/resources/test-rom/spec/programs/shell_spec.lua @@ -1,3 +1,5 @@ +local with_window = require "test_helpers".with_window + describe("The shell", function() describe("require", function() it("validates arguments", function() @@ -101,4 +103,48 @@ describe("The shell", function() expect.error(shell.switchTab, nil):eq("bad argument #1 (expected number, got nil)") end) end) + + describe("file uploads", function() + local function create_file(name, contents) + local did_read = false + return { + getName = function() return name end, + read = function() + if did_read then return end + did_read = true + return contents + end, + close = function() end, + } + end + local function create_files(files) return { getFiles = function() return files end } end + + it("suspends the read prompt", function() + fs.delete("tmp.txt") + + local win = with_window(32, 5, function() + local queue = { + { "shell" }, + { "paste", "xyz" }, + { "file_transfer", create_files { create_file("transfer.txt", "empty file") } }, + } + local co = coroutine.create(shell.run) + for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end + end) + + expect(win.getCursorBlink()):eq(true) + + local lines = {} + for i = 1, 5 do lines[i] = win.getLine(i):gsub(" +$", "") end + expect(lines):same { + "CraftOS 1.8", + "> xyz", + "Transferring transfer.txt", + "> xyz", + "", + } + + expect({ win.getCursorPos() }):same { 6, 4 } + end) + end) end)