From a033ffb260c4339ac3e3c034adaa2da94495b3f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 20:27:29 +0000 Subject: [PATCH 1/7] Show full script paths when hovering the script list Paths in the script list are truncated, so files with a common prefix, such as inventory_sorting_main.js and inventory_sorting_test.js, or files in subdirectories could not be told apart. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/container/ContainerScreenTerminalScripting.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index 34ead3a3e..1a442046e 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -293,8 +293,12 @@ protected void renderScriptPaths(GuiGraphics guiGraphics, int mouseX, int mouseY Font.DisplayMode.NORMAL ); - // If hovering, render removal button + // If hovering, show the full path, as it may be truncated, and render removal button if (hovering) { + // Don't draw over an open dialog + if (pendingScriptRemovalDialog == null) { + this.setTooltipForNextRenderPass(Component.literal(path.toString())); + } guiGraphics.pose().pushPose(); float scale = 0.4F; int size = (int) (Images.ERROR.getWidth() * scale); From 7d277e613e39f0da9d5cc623aeefa11a2310c9ee Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 20:42:45 +0000 Subject: [PATCH 2/7] Don't let file watcher reloads discard unflushed script changes When the file watcher reloaded a disk directory, for example because the server just wrote or deleted a script there, it replaced all scripts in memory with the disk contents. Changes that were not flushed to disk yet were lost, such as the new file of a rename when the old one was deleted first. Keep unflushed changes when reloading. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../core/network/ScriptingData.java | 17 +++++++++- .../core/network/ScriptingDataTest.java | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java b/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java index 9ee70381b..ad7d352e8 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java +++ b/src/main/java/org/cyclops/integratedscripting/core/network/ScriptingData.java @@ -49,7 +49,8 @@ public class ScriptingData implements IScriptingData { private final Int2ObjectMap> diskScripts = new Int2ObjectAVLTreeMap<>(); private final Path rootPath; - private final Set> dirtyPaths = Sets.newHashSet(); + // Accessed by both the server thread and the file watcher thread + private final Set> dirtyPaths = Sets.newConcurrentHashSet(); private final Int2ObjectMap> scriptChangeListeners = new Int2ObjectAVLTreeMap<>(); private final Map pathWatchers = Maps.newHashMap(); private final Map pathWatchersReverse = Maps.newHashMap(); @@ -155,6 +156,20 @@ protected void initializeDiskScripts(int diskId) { } } } + + // Keep changes that were not flushed to disk yet, as the disk contents are outdated for these + Map scriptsMemory = diskScripts.get(diskId); + for (Pair dirtyPath : dirtyPaths) { + if (dirtyPath.getLeft() == diskId) { + String scriptMemory = scriptsMemory == null ? null : scriptsMemory.get(dirtyPath.getRight()); + if (scriptMemory == null) { + scripts.remove(dirtyPath.getRight()); + } else { + scripts.put(dirtyPath.getRight(), scriptMemory); + } + } + } + this.setScripts(diskId, scripts, ChangeLocation.DISK); } diff --git a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java index 0935f5786..709878384 100644 --- a/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java +++ b/src/test/java/org/cyclops/integratedscripting/core/network/ScriptingDataTest.java @@ -226,6 +226,38 @@ public void testFlushKeepsFilePermissions() throws IOException { } } + @Test + public void testDiskReloadKeepsUnflushedChanges() throws IOException { + Path rootPath = Files.createTempDirectory("integratedscripting-test"); + ScriptingData scriptingData = new ScriptingData(rootPath); + try { + Path diskPath = rootPath.resolve("scripting-disks").resolve("654"); + Files.createDirectories(diskPath); + Files.writeString(diskPath.resolve("old.js"), "old"); + Files.writeString(diskPath.resolve("other.js"), "other"); + scriptingData.initialize(); + + // Rename old.js to new.js in memory, without flushing it to disk yet + scriptingData.setScript(654, Path.of("new.js"), "old", IScriptingData.ChangeLocation.MEMORY); + scriptingData.setScript(654, Path.of("old.js"), null, IScriptingData.ChangeLocation.MEMORY); + + // A file watcher reload, for example caused by another file, must not undo these changes + scriptingData.initializeDiskScripts(654); + assertThat(scriptingData.getScripts(654).get(Path.of("new.js")), equalTo("old")); + assertThat(scriptingData.getScripts(654).get(Path.of("old.js")), nullValue()); + assertThat(scriptingData.getScripts(654).get(Path.of("other.js")), equalTo("other")); + + // After flushing, the disk contents are used again + scriptingData.tick(); + scriptingData.initializeDiskScripts(654); + assertThat(scriptingData.getScripts(654).get(Path.of("new.js")), equalTo("old")); + assertFalse(Files.exists(diskPath.resolve("old.js"))); + } finally { + scriptingData.close(); + FileUtils.deleteDirectory(rootPath.toFile()); + } + } + private static void awaitCondition(Condition condition) throws InterruptedException { assertTrue("Timed out waiting for condition", awaitCondition(condition, AWAIT_TIMEOUT_MS)); } From 3d7939745d9e5d1a5bf54ecca40994449de7f3b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 20:43:15 +0000 Subject: [PATCH 3/7] Allow renaming scripts in the scripting terminal New scripts were always named script0.js, script1.js and so on, and could only be renamed or moved into directories through the file system, which players on a server can't access. Right-click a script to rename it, which also allows moving it into a directory or changing its language through its extension. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/WidgetDialog.java | 41 +++++++- .../ContainerScreenTerminalScripting.java | 98 ++++++++++++++++--- .../container/ContainerTerminalScripting.java | 29 ++++++ .../TerminalScriptingRenameScriptPacket.java | 68 +++++++++++++ .../proxy/CommonProxy.java | 2 + .../integratedscripting/lang/en_us.json | 5 + 6 files changed, 227 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingRenameScriptPacket.java diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetDialog.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetDialog.java index b7d77701c..b4a33396b 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetDialog.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetDialog.java @@ -5,6 +5,7 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.narration.NarratedElementType; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -16,6 +17,8 @@ import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.integratedscripting.Reference; +import javax.annotation.Nullable; + /** * Shows a confirm/cancel dialog * @author rubensworks @@ -32,10 +35,21 @@ public class WidgetDialog extends AbstractWidget implements GuiEventListener { private final Component message; private final ButtonText buttonConfirm; private final ButtonText buttonCancel; + @Nullable + private final EditBox input; public WidgetDialog(Font font, int x, int y, Screen parent, Component title, Component message, Component confirm, Component cancel, Button.OnPress confirmCallback, Button.OnPress cancelCallback) { + this(font, x, y, parent, title, message, confirm, cancel, confirmCallback, cancelCallback, null); + } + + /** + * @param inputValue If not null, a text input is shown with this initial value. + */ + public WidgetDialog(Font font, int x, int y, Screen parent, + Component title, Component message, Component confirm, Component cancel, + Button.OnPress confirmCallback, Button.OnPress cancelCallback, @Nullable String inputValue) { super(x, y, WIDTH, HEIGHT, message); this.font = font; this.parent = parent; @@ -44,6 +58,14 @@ public WidgetDialog(Font font, int x, int y, Screen parent, this.buttonConfirm = new ButtonText(x + 50, y + HEIGHT - 15 - 5, 50, 15, confirm, confirm, confirmCallback, true); this.buttonCancel = new ButtonText(x + WIDTH - 50 - 50, y + HEIGHT - 15 - 5, 50, 15, cancel, cancel, cancelCallback, true); + if (inputValue != null) { + this.input = new EditBox(font, x + 10, y + 31, WIDTH - 20, 12, message); + this.input.setMaxLength(256); + this.input.setValue(inputValue); + this.input.setFocused(true); + } else { + this.input = null; + } } protected void drawBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { @@ -65,16 +87,33 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo guiGraphics.drawCenteredString(font, title, this.getX() + this.width / 2, this.getY() + 4, 16777215); FormattedCharSequence formattedcharsequence = message.getVisualOrderText(); - guiGraphics.drawString(font, message, (this.getX() + this.width / 2 - font.width(formattedcharsequence) / 2), (this.getY() + 25), 4210752, false); + guiGraphics.drawString(font, message, (this.getX() + this.width / 2 - font.width(formattedcharsequence) / 2), (this.getY() + (this.input != null ? 19 : 25)), 4210752, false); + if (this.input != null) { + this.input.render(guiGraphics, mouseX, mouseY, partialTicks); + } } @Override public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { return this.buttonConfirm.mouseClicked(mouseX, mouseY, mouseButton) || this.buttonCancel.mouseClicked(mouseX, mouseY, mouseButton) + || (this.input != null && this.input.mouseClicked(mouseX, mouseY, mouseButton)) || super.mouseClicked(mouseX, mouseY, mouseButton); } + @Nullable + public EditBox getInput() { + return input; + } + + public void confirm() { + this.buttonConfirm.onPress(); + } + + public void cancel() { + this.buttonCancel.onPress(); + } + @Override protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { narrationElementOutput.add(NarratedElementType.TITLE, message); diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index 1a442046e..1e997b9bd 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -1,6 +1,7 @@ package org.cyclops.integratedscripting.client.gui.container; import com.google.common.base.Strings; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.mojang.blaze3d.systems.RenderSystem; import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap; @@ -10,6 +11,7 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceLocation; @@ -41,9 +43,11 @@ import org.cyclops.integratedscripting.inventory.container.ContainerTerminalScripting; import org.cyclops.integratedscripting.network.packet.TerminalScriptingDeleteScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingModifiedScriptPacket; +import org.cyclops.integratedscripting.network.packet.TerminalScriptingRenameScriptPacket; import org.lwjgl.glfw.GLFW; import javax.annotation.Nullable; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Collections; import java.util.List; @@ -77,7 +81,7 @@ public class ContainerScreenTerminalScripting extends ContainerScreenExtended= this.leftPos + PATHS_X + PATHS_WIDTH - (int) (Images.ERROR.getWidth() * 0.4F) - 1) { this.fieldDisk.playDownSound(Minecraft.getInstance().getSoundManager()); // Show confirmation dialog - pendingScriptRemovalDialog = new WidgetDialog(font, leftPos + getBaseXSize() / 2 - WidgetDialog.WIDTH / 2, topPos + 50, this, + pendingDialog = new WidgetDialog(font, leftPos + getBaseXSize() / 2 - WidgetDialog.WIDTH / 2, topPos + 50, this, Component.translatable("gui.integratedscripting.removal_dialog.title"), Component.translatable("gui.integratedscripting.removal_dialog.message", hoveredScriptPath.toString()), Component.translatable("gui.integratedscripting.removal_dialog.delete"), Component.translatable("gui.integratedscripting.removal_dialog.keep"), (b) -> { - removeWidget(pendingScriptRemovalDialog); - pendingScriptRemovalDialog = null; + removeWidget(pendingDialog); + pendingDialog = null; this.removeScript(hoveredScriptPath); }, (b) -> { - removeWidget(pendingScriptRemovalDialog); - pendingScriptRemovalDialog = null; + removeWidget(pendingDialog); + pendingDialog = null; }); - addRenderableWidget(pendingScriptRemovalDialog); + addRenderableWidget(pendingDialog); return true; } else { @@ -518,6 +529,53 @@ private void onSelectionModified() { getMenu().setSelection(selected); } + private void openRenameDialog(Path path) { + pendingDialog = new WidgetDialog(font, leftPos + getBaseXSize() / 2 - WidgetDialog.WIDTH / 2, topPos + 50, this, + Component.translatable("gui.integratedscripting.rename_dialog.title"), + Component.translatable("gui.integratedscripting.rename_dialog.message"), + Component.translatable("gui.integratedscripting.rename_dialog.rename"), + Component.translatable("gui.integratedscripting.rename_dialog.cancel"), + (b) -> { + String name = pendingDialog.getInput().getValue(); + removeWidget(pendingDialog); + pendingDialog = null; + this.renameScript(path, name); + }, + (b) -> { + removeWidget(pendingDialog); + pendingDialog = null; + }, path.toString()); + addRenderableWidget(pendingDialog); + } + + private void renameScript(Path from, String name) { + int disk = getMenu().getActiveDisk(); + Map scripts = getActiveScripts(); + Path to; + try { + to = Path.of(name.trim()); + } catch (InvalidPathException e) { + return; + } + if (scripts == null || to.toString().isEmpty() || scripts.containsKey(to) || !scripts.containsKey(from)) { + return; + } + + // Apply the rename locally, so an open script stays open with its editor state + String script = scripts.get(from); + boolean active = from.equals(getMenu().getActiveScriptPath()); + getMenu().setLastScript(disk, to, script); + getMenu().setLastScript(disk, from, null); + TextHistory history = scriptHistories.remove(Pair.of(disk, from)); + if (history != null) { + scriptHistories.put(Pair.of(disk, to), history); + } + if (active) { + getMenu().setActiveScriptPath(to); + } + IntegratedScripting._instance.getPacketHandler().sendToServer(new TerminalScriptingRenameScriptPacket(disk, from, to)); + } + private void removeScript(Path path) { IntegratedScripting._instance.getPacketHandler() .sendToServer(new TerminalScriptingDeleteScriptPacket(getMenu().getActiveDisk(), path)); @@ -534,7 +592,7 @@ public boolean mouseScrolled(double mouseX, double mouseY, double mouseZ, double @Override public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double offsetX, double offsetY) { // Make active dialog consume all input - if (pendingScriptRemovalDialog != null) { + if (pendingDialog != null) { return false; } @@ -547,7 +605,17 @@ public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, doubl @Override public boolean keyPressed(int typedChar, int keyCode, int modifiers) { // Make active dialog consume all input - if (typedChar != GLFW.GLFW_KEY_ESCAPE && pendingScriptRemovalDialog != null) { + if (pendingDialog != null && pendingDialog.getInput() != null) { + if (typedChar == GLFW.GLFW_KEY_ESCAPE) { + pendingDialog.cancel(); + } else if (typedChar == GLFW.GLFW_KEY_ENTER || typedChar == GLFW.GLFW_KEY_KP_ENTER) { + pendingDialog.confirm(); + } else { + pendingDialog.getInput().keyPressed(typedChar, keyCode, modifiers); + } + return true; + } + if (typedChar != GLFW.GLFW_KEY_ESCAPE && pendingDialog != null) { return false; } @@ -621,8 +689,8 @@ private void navigate() { @Override public boolean charTyped(char p_94683_, int p_94684_) { // Make active dialog consume all input - if (pendingScriptRemovalDialog != null) { - return false; + if (pendingDialog != null) { + return pendingDialog.getInput() != null && pendingDialog.getInput().charTyped(p_94683_, p_94684_); } if (this.fieldNavigate != null && this.fieldNavigate.isFocused()) { return this.fieldNavigate.charTyped(p_94683_, p_94684_); diff --git a/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java index 47e87e303..334c22dea 100644 --- a/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java @@ -336,6 +336,35 @@ public void setServerScript(int disk, Path path, @Nullable String script) { ScriptingNetworkHelpers.getScriptingData().setScript(disk, path, script, IScriptingData.ChangeLocation.MEMORY); } + /** + * Rename a script on the server. + * The client already applied the rename to its own state, so it is corrected if the rename fails. + */ + public void renameServerScript(int disk, Path from, Path to) { + IScriptingData scriptingData = ScriptingNetworkHelpers.getScriptingData(); + String script = scriptingData.getScripts(disk).get(from); + if (script != null && !scriptingData.getScripts(disk).containsKey(to)) { + scriptingData.setScript(disk, to, script, IScriptingData.ChangeLocation.MEMORY); + // Only remove the original if the new path was accepted + if (script.equals(scriptingData.getScripts(disk).get(to))) { + scriptingData.setScript(disk, from, null, IScriptingData.ChangeLocation.MEMORY); + } + } + + // Send the actual state of both paths, which also corrects the client if the rename failed + for (Path path : new Path[]{from, to}) { + String value = scriptingData.getScripts(disk).get(path); + setLastScript(disk, path, value); + if (value != null) { + IntegratedScripting._instance.getPacketHandler() + .sendToPlayer(new TerminalScriptingModifiedScriptPacket(disk, path, value), (ServerPlayer) player); + } else { + IntegratedScripting._instance.getPacketHandler() + .sendToPlayer(new TerminalScriptingDeleteScriptPacket(disk, path), (ServerPlayer) player); + } + } + } + public void createNewServerScript(int disk) { // Determine unique script name Set existingScripts = ScriptingNetworkHelpers.getScriptingData().getScripts(disk).keySet(); diff --git a/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingRenameScriptPacket.java b/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingRenameScriptPacket.java new file mode 100644 index 000000000..4701fd614 --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingRenameScriptPacket.java @@ -0,0 +1,68 @@ +package org.cyclops.integratedscripting.network.packet; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.cyclops.cyclopscore.network.CodecField; +import org.cyclops.cyclopscore.network.PacketCodec; +import org.cyclops.integratedscripting.Reference; +import org.cyclops.integratedscripting.inventory.container.ContainerTerminalScripting; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; + +/** + * Packet for renaming a script from client to server. + * @author rubensworks + * + */ +public class TerminalScriptingRenameScriptPacket extends PacketCodec { + + public static final Type ID = new Type<>(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "terminal_scripting_rename_script")); + public static final StreamCodec CODEC = getCodec(TerminalScriptingRenameScriptPacket::new); + + @CodecField + private int disk; + @CodecField + private String from; + @CodecField + private String to; + + public TerminalScriptingRenameScriptPacket() { + super(ID); + } + + public TerminalScriptingRenameScriptPacket(int disk, Path from, Path to) { + super(ID); + this.disk = disk; + this.from = from.toString(); + this.to = to.toString(); + } + + @Override + public boolean isAsync() { + return false; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void actionClient(Level world, Player player) { + + } + + @Override + public void actionServer(Level world, ServerPlayer player) { + if(player.containerMenu instanceof ContainerTerminalScripting container) { + try { + container.renameServerScript(disk, Path.of(from), Path.of(to)); + } catch (InvalidPathException e) { + // Ignore invalid paths + } + } + } +} diff --git a/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java b/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java index 9f579a612..64eed3f3d 100644 --- a/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java +++ b/src/main/java/org/cyclops/integratedscripting/proxy/CommonProxy.java @@ -7,6 +7,7 @@ import org.cyclops.integratedscripting.network.packet.TerminalScriptingCreateNewScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingDeleteScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingModifiedScriptPacket; +import org.cyclops.integratedscripting.network.packet.TerminalScriptingRenameScriptPacket; import org.cyclops.integratedscripting.network.packet.TerminalScriptingSyntaxErrorPacket; /** @@ -29,5 +30,6 @@ public void registerPacketHandlers(PacketHandler packetHandler) { packetHandler.register(TerminalScriptingCreateNewScriptPacket.ID, TerminalScriptingCreateNewScriptPacket.CODEC); packetHandler.register(TerminalScriptingDeleteScriptPacket.ID, TerminalScriptingDeleteScriptPacket.CODEC); packetHandler.register(TerminalScriptingSyntaxErrorPacket.ID, TerminalScriptingSyntaxErrorPacket.CODEC); + packetHandler.register(TerminalScriptingRenameScriptPacket.ID, TerminalScriptingRenameScriptPacket.CODEC); } } diff --git a/src/main/resources/assets/integratedscripting/lang/en_us.json b/src/main/resources/assets/integratedscripting/lang/en_us.json index 56190e8aa..bd7fc084d 100644 --- a/src/main/resources/assets/integratedscripting/lang/en_us.json +++ b/src/main/resources/assets/integratedscripting/lang/en_us.json @@ -32,6 +32,11 @@ "gui.integratedscripting.removal_dialog.message": "Do you want to delete \"%s\"?", "gui.integratedscripting.removal_dialog.delete": "Delete", "gui.integratedscripting.removal_dialog.keep": "Keep", + "gui.integratedscripting.rename_dialog.title": "Rename script", + "gui.integratedscripting.rename_dialog.message": "New path, such as dir/name.js:", + "gui.integratedscripting.rename_dialog.rename": "Rename", + "gui.integratedscripting.rename_dialog.cancel": "Cancel", + "gui.integratedscripting.rename_hint": "Right-click to rename", "gui.integratedscripting.error.invalid_member": "Select a valid function name or variable name inside the script editor to bind with.", "gui.integratedscripting.error.invalid_member.current": "Current selection: \"%s\"", From 3882807b09a39c6d996431fd056967d281e76f42 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 21:03:57 +0000 Subject: [PATCH 4/7] Add a focus mode to enlarge the script editor The editor only uses a small part of the screen, showing about a dozen wrapped lines. Add a button to show only the editor, using most of the screen. Escape or the button returns to the normal terminal. See #44 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../ContainerScreenTerminalScripting.java | 76 ++++++++++++++++++- .../integratedscripting/lang/en_us.json | 2 + 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index 91c6fb58c..add670f57 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -10,6 +10,8 @@ import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; @@ -73,6 +75,7 @@ public class ContainerScreenTerminalScripting extends ContainerScreenExtended { if (key == GLFW.GLFW_KEY_F || key == GLFW.GLFW_KEY_G) { @@ -163,7 +174,15 @@ public int getTotalRows() { buttonCreateFile = new ButtonText(this.leftPos + 19, this.topPos + 222, 56, 10, Component.translatable("gui.integratedscripting.create_file"), Component.literal("+"), (button) -> getMenu().createNewFile(), true); - addRenderableWidget(buttonCreateFile); + if (!focusMode) { + addRenderableWidget(buttonCreateFile); + } + + Component focusLabel = Component.translatable(focusMode ? "gui.integratedscripting.focus_mode.exit" : "gui.integratedscripting.focus_mode.enter"); + buttonFocus = new ButtonText(focusMode ? this.width - FOCUS_MARGIN - 12 : this.leftPos + SCRIPT_X + SCRIPT_WIDTH - 11, focusMode ? FOCUS_MARGIN - 12 : this.topPos + 5, 12, 10, + focusLabel, Component.literal(focusMode ? "><" : "<>"), (button) -> this.toggleFocusMode(), true); + buttonFocus.setTooltip(Tooltip.create(focusLabel)); + addRenderableWidget(buttonFocus); // Restore the open script in the new editor if (getMenu().getActiveScriptPath() != null) { @@ -228,8 +247,44 @@ protected int getErrorY() { return 139; } + private void toggleFocusMode() { + this.focusMode = !this.focusMode; + // Recreates the widgets, which keeps the open script + this.rebuildWidgets(); + } + + @Override + protected void drawCurrentScreen(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { + if (focusMode) { + // Only show the editor, without the container contents + guiGraphics.fill(textArea.getX() - 2, textArea.getY() - 2, textArea.getX() + textArea.getWidth() + 2, textArea.getY() + textArea.getHeight() + 2, Helpers.RGBAToInt(85, 85, 85, 255)); + guiGraphics.fill(textArea.getX() - 1, textArea.getY() - 1, textArea.getX() + textArea.getWidth() + 1, textArea.getY() + textArea.getHeight() + 1, Helpers.RGBAToInt(224, 224, 224, 255)); + Path path = getMenu().getActiveScriptPath(); + if (path != null) { + guiGraphics.drawString(font, path.toString(), FOCUS_MARGIN, FOCUS_MARGIN - 11, 16777215); + } + for (net.minecraft.client.gui.components.Renderable renderable : this.renderables) { + renderable.render(guiGraphics, mouseX, mouseY, partialTicks); + } + } else { + super.drawCurrentScreen(guiGraphics, mouseX, mouseY, partialTicks); + } + } + + @Override + protected void renderTooltip(GuiGraphics guiGraphics, int x, int y) { + // Slots are hidden in focus mode + if (!focusMode) { + super.renderTooltip(guiGraphics, x, y); + } + } + @Override protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { + // Only the editor is shown in focus mode + if (focusMode) { + return; + } super.renderBg(guiGraphics, partialTicks, mouseX, mouseY); RenderHelpers.bindTexture(this.texture); fieldDisk.render(guiGraphics, mouseX, mouseY, partialTicks); @@ -366,6 +421,17 @@ public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { return pendingDialog.mouseClicked(mouseX, mouseY, mouseButton); } + // Only the editor widgets are visible in focus mode, so don't let clicks reach the container + if (focusMode) { + for (GuiEventListener child : this.children()) { + if (child.mouseClicked(mouseX, mouseY, mouseButton)) { + this.setFocused(child); + return true; + } + } + return true; + } + // Handle script path clicks Path hoveredScriptPath = getHoveredScriptPath(mouseX, mouseY); if (hoveredScriptPath != null && mouseButton == 1) { @@ -645,6 +711,12 @@ public boolean keyPressed(int typedChar, int keyCode, int modifiers) { return true; } + // Leave focus mode instead of closing the screen + if (focusMode && typedChar == GLFW.GLFW_KEY_ESCAPE && pendingDialog == null) { + this.toggleFocusMode(); + return true; + } + if (textArea.isFocused()) { boolean ret = textArea.keyPressed(typedChar, keyCode, modifiers); if (typedChar != GLFW.GLFW_KEY_ESCAPE) { diff --git a/src/main/resources/assets/integratedscripting/lang/en_us.json b/src/main/resources/assets/integratedscripting/lang/en_us.json index bd7fc084d..4fe011b8d 100644 --- a/src/main/resources/assets/integratedscripting/lang/en_us.json +++ b/src/main/resources/assets/integratedscripting/lang/en_us.json @@ -27,6 +27,8 @@ "gui.integratedscripting.script": "Script", "gui.integratedscripting.create_file": "Create File", "gui.integratedscripting.find": "Find", + "gui.integratedscripting.focus_mode.enter": "Enlarge editor", + "gui.integratedscripting.focus_mode.exit": "Back to normal size (Escape)", "gui.integratedscripting.goto_line": "Go to line", "gui.integratedscripting.removal_dialog.title": "Confirm script deletion", "gui.integratedscripting.removal_dialog.message": "Do you want to delete \"%s\"?", From 3635125c33b88979ca441eec9f791bf35388daac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 06:54:01 +0000 Subject: [PATCH 5/7] Show the focus mode button in the editor toolbar The focus mode button overlapped the new toolbar. Show it as the last toolbar button with its own icons, keep the toolbar available in focus mode, and list renaming and focus mode in the shortcut overview. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../ContainerScreenTerminalScripting.java | 21 +++++++++++------- .../client/gui/image/ScriptImages.java | 2 ++ .../integratedscripting/lang/en_us.json | 2 +- .../textures/gui/icons.png | Bin 1308 -> 1385 bytes 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index c5dc6ed09..458db0035 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -89,7 +89,7 @@ public class ContainerScreenTerminalScripting extends ContainerScreenExtended toolbarEditButtons = Lists.newArrayList(); private ButtonIcon buttonUndo; private ButtonIcon buttonRedo; - private ButtonText buttonFocus; + private ButtonIcon buttonFocus; private boolean focusMode; private WidgetDialog pendingDialog; @Nullable @@ -183,13 +183,11 @@ public int getTotalRows() { addRenderableWidget(buttonCreateFile); } - Component focusLabel = Component.translatable(focusMode ? "gui.integratedscripting.focus_mode.exit" : "gui.integratedscripting.focus_mode.enter"); - buttonFocus = new ButtonText(focusMode ? this.width - FOCUS_MARGIN - 12 : this.leftPos + SCRIPT_X + SCRIPT_WIDTH - 11, focusMode ? FOCUS_MARGIN - 12 : this.topPos + 5, 12, 10, - focusLabel, Component.literal(focusMode ? "><" : "<>"), (button) -> this.toggleFocusMode(), true); - buttonFocus.setTooltip(Tooltip.create(focusLabel)); - addRenderableWidget(buttonFocus); - - this.addToolbar(this.leftPos + SCRIPT_X + SCRIPT_WIDTH, this.topPos + 5); + if (focusMode) { + this.addToolbar(this.width - FOCUS_MARGIN, FOCUS_MARGIN - 12); + } else { + this.addToolbar(this.leftPos + SCRIPT_X + SCRIPT_WIDTH, this.topPos + 5); + } // Restore the open script in the new editor if (getMenu().getActiveScriptPath() != null) { @@ -205,6 +203,13 @@ public int getTotalRows() { * @param y The top of the toolbar. */ private void addToolbar(int right, int y) { + Component focusLabel = Component.translatable(focusMode ? "gui.integratedscripting.focus_mode.exit" : "gui.integratedscripting.focus_mode.enter"); + buttonFocus = new ButtonIcon(right - 10, y, 10, 10, focusLabel, (button) -> this.toggleFocusMode(), + focusMode ? ScriptImages.ACTION_FOCUS_EXIT : ScriptImages.ACTION_FOCUS_ENTER); + buttonFocus.setTooltip(Tooltip.create(focusLabel)); + addRenderableWidget(buttonFocus); + right -= 10; + String modifier = Minecraft.ON_OSX ? "Cmd" : "Ctrl"; this.toolbarEditButtons.clear(); this.buttonUndo = this.addToolbarButton(right - 60, y, ScriptImages.ACTION_UNDO, Component.translatable("gui.integratedscripting.action.undo", modifier), () -> this.textArea.undo()); diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/image/ScriptImages.java b/src/main/java/org/cyclops/integratedscripting/client/gui/image/ScriptImages.java index d89dc99ac..c45b087c2 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/image/ScriptImages.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/image/ScriptImages.java @@ -22,5 +22,7 @@ public class ScriptImages { public static final Image ACTION_GOTO_LINE = new Image(ICONS, 24, 16, 8, 8); public static final Image ACTION_COMMENT = new Image(ICONS, 32, 16, 8, 8); public static final Image ACTION_HELP = new Image(ICONS, 40, 16, 8, 8); + public static final Image ACTION_FOCUS_ENTER = new Image(ICONS, 48, 16, 8, 8); + public static final Image ACTION_FOCUS_EXIT = new Image(ICONS, 56, 16, 8, 8); } diff --git a/src/main/resources/assets/integratedscripting/lang/en_us.json b/src/main/resources/assets/integratedscripting/lang/en_us.json index bf02f76dd..3853d44b1 100644 --- a/src/main/resources/assets/integratedscripting/lang/en_us.json +++ b/src/main/resources/assets/integratedscripting/lang/en_us.json @@ -41,7 +41,7 @@ "gui.integratedscripting.action.goto_line": "Go to line (%s+G)", "gui.integratedscripting.action.comment": "Toggle comment (%s+/)", "gui.integratedscripting.action.help": "Keyboard shortcuts", - "gui.integratedscripting.shortcuts": "%1$s+Z, %1$s+Y: undo, redo\n%1$s+F, %1$s+G: find, go to line\n%1$s+/: toggle comment\nTab, Shift+Tab: indent, unindent\nAlt+Up, Alt+Down: move lines\nPage Up, Page Down: move a page", + "gui.integratedscripting.shortcuts": "%1$s+Z, %1$s+Y: undo, redo\n%1$s+F, %1$s+G: find, go to line\n%1$s+/: toggle comment\nTab, Shift+Tab: indent, unindent\nAlt+Up, Alt+Down: move lines\nPage Up, Page Down: move a page\nEscape: close the search bar, leave focus mode\nRight-click a script: rename it", "gui.integratedscripting.removal_dialog.title": "Confirm script deletion", "gui.integratedscripting.removal_dialog.message": "Do you want to delete \"%s\"?", "gui.integratedscripting.removal_dialog.delete": "Delete", diff --git a/src/main/resources/assets/integratedscripting/textures/gui/icons.png b/src/main/resources/assets/integratedscripting/textures/gui/icons.png index 1f651635da162406373400346dea3011b3f3f7fb..2e6e22a11a012549ed15117c05b752513c9c2814 100644 GIT binary patch literal 1385 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJD%15X#nkcv5P@0=~XoW5vC}lfrH+W!`_Z+DP81?9BPuDtC(0Oav$Cv?lIbEr0us z-}1{_W<39STVAiCqT_2?EEBWU_>GwPP89vn1Su?!;{pb7qeY?K5hd(-ddG?)m zf0wVHe4Dw#_TLNL1JnES=l*>t`t$L51{a37{<91^@4v6F@0(Wr_v57cY{?HrxpvGE z_rzlU%jEJFG0VqIzq`Bq{k%UG@85q2OsLpd_PCs}K}?>d$$+r|h&I@NYIyhY-X>9&;O-;;C`;9_qoqC z^R7+*dHKtGhp211xBJ)cFHzli@BP-l@w=sdPSu{ee2$;`WKHW@^Bu<;x5_<;etqrx zm1LQ1Y`3cx|1+ajU}`=S_uYliDvP1v z;e!Z<-N9X&ja)g0-)bIQn>&X~qcQBxMaJ?%^%*BwU7nu1SUEqvZQ}g?mb2fVwLf@O zV8JuFQ0BheoQ=2NhRv7duhIX0d2XQ$zrMb{IJ>>At?c{$#}z+5|Gs|oWB>W}@qa1~ z{C}Azv2OOC;_E(j4}R}9{<-k=yZm~F2k(CtF-$xD6R4=~@5A;T_4Oyd%FT~wZ)g8d zeCLb6gY)m>xBdIjeCLl$1IK~$g+>gQzyCeGzxeXTe>KzpUnxHDz4VjJfiGJhe0qOp zrDxuw!{;|uRa8`bFS@tBK+~Wme)Z3M#s)EQCdU~J4nTCn*YCpH@4p{+wtTIsy=wIR z_uoXzvVV6zeJy@nz{hO&eh0h3{Dk*2`{$~?O4}@HHv8<+r#kW0U-@01o%j82zlrZb zRr%o$W`^s3^4m}TsUmay;|8GbFNfDXpSC$}efV>=_eMcw?mp`8#{r`KILH&J`e^=i9H`vpfQduu?Z=Jfu_qYd--#w3hdN!@N=HV5w%3H7U zw%<;a+JF1)vhVV34|B6^b<=-V?7k~k{@7o)e&@QKKeP1=nxt>N|K9#{$!vSO@3%fx z)Xq9Ez3lVyjnAj;kGa0|yamtyc`PNx#lP>otq}U4y!-0q8&CCqt?w0j4H6yWAJqKb6Mw<&;$VDO#XBL From eff9318e0f0ef899bfa4d282d82c4b0a52c02e69 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 07:07:13 +0000 Subject: [PATCH 6/7] Keep the full English terminal title next to the toolbar With the focus mode button in the toolbar, the margin before the toolbar cut off the last letters of the English title, while it fits. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../client/gui/container/ContainerScreenTerminalScripting.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index a4ebaf957..953b008c7 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -473,7 +473,7 @@ protected Path getHoveredScriptPath(double mouseX, double mouseY) { protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { // Shorten the title if it would overlap with the toolbar, as translations can be long Component title = this.title; - int maxTitleWidth = this.toolbarLeft - this.leftPos - this.titleLabelX - 2; + int maxTitleWidth = this.toolbarLeft - this.leftPos - this.titleLabelX - 1; if (this.font.width(title) > maxTitleWidth) { title = Component.literal(this.font.plainSubstrByWidth(title.getString(), maxTitleWidth - this.font.width("...")) + "..."); } From db61a8e8fddbce0d01fc618ee11537e42209033f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 07:57:42 +0000 Subject: [PATCH 7/7] Document focus mode and renaming keys in the info book These were listed in the removed shortcuts button, so they now go in the info book section with the other shortcuts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- src/main/resources/assets/integratedscripting/lang/en_us.json | 1 + .../resources/data/integratedscripting/info/scripting_info.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/resources/assets/integratedscripting/lang/en_us.json b/src/main/resources/assets/integratedscripting/lang/en_us.json index 88329a244..6137484e4 100644 --- a/src/main/resources/assets/integratedscripting/lang/en_us.json +++ b/src/main/resources/assets/integratedscripting/lang/en_us.json @@ -146,6 +146,7 @@ "info_book.integratedscripting.terminal_shortcuts.text1": "The script editor of the &lScripting Terminal&r has the keyboard shortcuts shown below. You can change them in the &oControls&r settings, under &oIntegrated Scripting&r. Most of these actions are also available as buttons above the editor.", "info_book.integratedscripting.terminal_shortcuts.text2": "Some keys can not be changed: &oTab&r and &oShift+Tab&r indent and unindent the selected lines, &oPage Up&r and &oPage Down&r move by a page, and &oCtrl+Shift+Z&r (&oCmd+Shift+Z&r on Mac) also redoes.", "info_book.integratedscripting.terminal_shortcuts.text3": "While finding text or going to a line, &oEnter&r and &oShift+Enter&r go to the next and previous match, and &oEscape&r closes the search bar.", + "info_book.integratedscripting.terminal_shortcuts.text4": "In focus mode, &oEscape&r returns to the normal terminal. To rename a script, right-click it in the script list.", "info_book.integratedscripting.writing": "Writing Scripts", diff --git a/src/main/resources/data/integratedscripting/info/scripting_info.xml b/src/main/resources/data/integratedscripting/info/scripting_info.xml index 84b7818ef..94d6191ad 100644 --- a/src/main/resources/data/integratedscripting/info/scripting_info.xml +++ b/src/main/resources/data/integratedscripting/info/scripting_info.xml @@ -59,6 +59,7 @@ info_book.integratedscripting.terminal_shortcuts.text1 info_book.integratedscripting.terminal_shortcuts.text2 info_book.integratedscripting.terminal_shortcuts.text3 + info_book.integratedscripting.terminal_shortcuts.text4 key.integratedscripting.undo key.integratedscripting.redo key.integratedscripting.find