diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a24a837f9..bcae864ba 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -59,8 +59,8 @@ jmh = "1.37"
# Build tools
cctJavadoc = "1.8.4"
-checkstyle = "10.21.4"
-errorProne-core = "2.37.0"
+checkstyle = "10.23.1"
+errorProne-core = "2.38.0"
errorProne-plugin = "4.1.0"
fabric-loom = "1.10.4"
githubRelease = "2.5.2"
@@ -70,7 +70,7 @@ illuaminate = "0.1.0-83-g1131f68"
lwjgl = "3.3.3"
minotaur = "2.8.7"
modDevGradle = "2.0.78"
-nullAway = "0.12.4"
+nullAway = "0.12.7"
shadow = "8.3.1"
spotless = "7.0.2"
taskTree = "2.1.1"
diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java b/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java
new file mode 100644
index 000000000..f99a254bf
--- /dev/null
+++ b/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
+//
+// SPDX-License-Identifier: MPL-2.0
+
+package dan200.computercraft.client.gui;
+
+import org.lwjgl.glfw.GLFW;
+
+/**
+ * Supports for converting/translating key codes.
+ */
+public class KeyConverter {
+ /**
+ * GLFW's key events refer to the physical key code, rather than the "actual" key code (with keyboard layout
+ * applied).
+ *
+ * This makes sense for WASD-style input, but is a right pain for keyboard shortcuts — this function attempts to
+ * translate those keys back to their "actual" key code. See also
+ * this discussion on GLFW's GitHub.
+ *
+ * @param key The current key code.
+ * @param scanCode The current scan code.
+ * @return The translated key code.
+ */
+ public static int physicalToActual(int key, int scanCode) {
+ var name = GLFW.glfwGetKeyName(key, scanCode);
+ if (name == null || name.length() != 1) return key;
+
+ // If we've got a single character as the key name, treat that as the ASCII value of the key,
+ // and map that back to a key code.
+ var character = name.charAt(0);
+
+ // 0-9 and A-Z map directly to their GLFW key (they're the same ASCII code).
+ if ((character >= '0' && character <= '9') || (character >= 'A' && character <= 'Z')) return character;
+ // a-z map to GLFW_KEY_{A,Z}
+ if (character >= 'a' && character <= 'z') return GLFW.GLFW_KEY_A + (character - 'a');
+
+ return key;
+ }
+}
diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java
index 49110157e..055b2b13f 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java
@@ -4,6 +4,7 @@
package dan200.computercraft.client.gui.widgets;
+import dan200.computercraft.client.gui.KeyConverter;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
@@ -83,7 +84,7 @@ public class TerminalWidget extends AbstractWidget {
}
if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0) {
- switch (key) {
+ switch (KeyConverter.physicalToActual(key, scancode)) {
case GLFW.GLFW_KEY_T -> {
if (terminateTimer < 0) terminateTimer = 0;
}
@@ -119,7 +120,7 @@ public class TerminalWidget extends AbstractWidget {
computer.keyUp(key);
}
- switch (key) {
+ switch (KeyConverter.physicalToActual(key, scancode)) {
case GLFW.GLFW_KEY_T -> terminateTimer = -1;
case GLFW.GLFW_KEY_R -> rebootTimer = -1;
case GLFW.GLFW_KEY_S -> shutdownTimer = -1;
diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java
index 52ecac60c..51cf53b11 100644
--- a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java
+++ b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java
@@ -62,8 +62,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer whence, Optional offset) throws LuaException {
checkOpen();
@@ -179,6 +180,8 @@ public abstract class AbstractHandle {
* @throws LuaException If the file has been closed.
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} in the event of an error.
* @cc.since 1.80pr1
+ * @cc.changed 1.109.0 Binary-mode handles are now consistent with non-binary files, and return an empty string at
+ * the end of the file, rather than {@code nil}.
*/
public Object @Nullable [] readAll() throws LuaException {
checkOpen();
diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java
index 234127244..2916929f1 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java
@@ -120,7 +120,7 @@ public final class StringUtil {
var idx = 0;
var iterator = clipboard.codePoints().iterator();
- while (iterator.hasNext() && idx <= output.length) {
+ while (iterator.hasNext() && idx < output.length) {
var chr = unicodeToTerminal(iterator.next());
if (chr < 0) continue; // Strip out unconvertible characters
if (!isTypableChar(chr)) break; // Stop at untypable ones.
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
index b98ff8d75..f013a29e1 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
@@ -371,7 +371,7 @@ function toBlit(color)
local hex = color_hex_lookup[color]
if hex then return hex end
- if color < 0 or color > 0xffff then error("Colour out of range", 2) end
+ if color < 1 or color > 0xffff then error("Colour out of range", 2) end
return string.format("%x", math.floor(math.log(color, 2)))
end
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua
index 7be7aebaf..5b5678d63 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua
@@ -65,7 +65,7 @@ local function parse_color(color)
return expect(1, color, "number")
end
- if color < 0 or color > 0xffff then error("Colour out of range", 3) end
+ if color < 1 or color > 0xffff then error("Colour out of range", 3) end
return 2 ^ math.floor(math.log(color, 2))
end
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua
index 9faf3b5a9..2d8d91938 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua
@@ -238,7 +238,7 @@ local function lex_token(context, str, pos)
if end_pos then return tokens.STRING, end_pos end
context.report(errors.unfinished_long_string, pos, boundary_pos, boundary_pos - pos)
- return tokens.ERROR, #str
+ return tokens.STRING, #str
elseif pos + 1 == boundary_pos then -- Just a "["
return tokens.OSQUARE, pos
else -- Malformed long string, for instance "[="
@@ -260,7 +260,7 @@ local function lex_token(context, str, pos)
if end_pos then return tokens.COMMENT, end_pos end
context.report(errors.unfinished_long_comment, pos, boundary_pos, boundary_pos - comment_pos)
- return tokens.ERROR, #str
+ return tokens.COMMENT, #str
end
end
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua
index e05ec1766..0e7a131e8 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua
@@ -175,63 +175,62 @@ local function save(_sPath, fWrite)
return ok, err, fileerr
end
-local tKeywords = {
- ["and"] = true,
- ["break"] = true,
- ["do"] = true,
- ["else"] = true,
- ["elseif"] = true,
- ["end"] = true,
- ["false"] = true,
- ["for"] = true,
- ["function"] = true,
- ["if"] = true,
- ["in"] = true,
- ["local"] = true,
- ["nil"] = true,
- ["not"] = true,
- ["or"] = true,
- ["repeat"] = true,
- ["return"] = true,
- ["then"] = true,
- ["true"] = true,
- ["until"] = true,
- ["while"] = true,
-}
-local function tryWrite(sLine, regex, colour)
- local match = string.match(sLine, regex)
- if match then
- if type(colour) == "number" then
- term.setTextColour(colour)
- else
- term.setTextColour(colour(match))
- end
- term.write(match)
- term.setTextColour(textColour)
- return string.sub(sLine, #match + 1)
- end
- return nil
+local tokens = require "cc.internal.syntax.parser".tokens
+local lex_one = require "cc.internal.syntax.lexer".lex_one
+
+local token_colours = {
+ [tokens.STRING] = stringColour,
+ [tokens.COMMENT] = commentColour,
+ -- Keywords
+ [tokens.AND] = keywordColour,
+ [tokens.BREAK] = keywordColour,
+ [tokens.DO] = keywordColour,
+ [tokens.ELSE] = keywordColour,
+ [tokens.ELSEIF] = keywordColour,
+ [tokens.END] = keywordColour,
+ [tokens.FALSE] = keywordColour,
+ [tokens.FOR] = keywordColour,
+ [tokens.FUNCTION] = keywordColour,
+ [tokens.GOTO] = keywordColour,
+ [tokens.IF] = keywordColour,
+ [tokens.IN] = keywordColour,
+ [tokens.LOCAL] = keywordColour,
+ [tokens.NIL] = keywordColour,
+ [tokens.NOT] = keywordColour,
+ [tokens.OR] = keywordColour,
+ [tokens.REPEAT] = keywordColour,
+ [tokens.RETURN] = keywordColour,
+ [tokens.THEN] = keywordColour,
+ [tokens.TRUE] = keywordColour,
+ [tokens.UNTIL] = keywordColour,
+ [tokens.WHILE] = keywordColour,
+}
+-- Fill in the remaining tokens.
+for _, token in pairs(tokens) do
+ if not token_colours[token] then token_colours[token] = textColour end
end
-local function writeHighlighted(sLine)
- while #sLine > 0 do
- sLine =
- tryWrite(sLine, "^%-%-%[%[.-%]%]", commentColour) or
- tryWrite(sLine, "^%-%-.*", commentColour) or
- tryWrite(sLine, "^\"\"", stringColour) or
- tryWrite(sLine, "^\".-[^\\]\"", stringColour) or
- tryWrite(sLine, "^\'\'", stringColour) or
- tryWrite(sLine, "^\'.-[^\\]\'", stringColour) or
- tryWrite(sLine, "^%[%[.-%]%]", stringColour) or
- tryWrite(sLine, "^[%w_]+", function(match)
- if tKeywords[match] then
- return keywordColour
- end
- return textColour
- end) or
- tryWrite(sLine, "^[^%w_]", textColour)
+local lex_context = { line = function() end, report = function() end }
+
+local function writeHighlighted(line)
+ local pos, colour = 1, nil
+
+ while true do
+ local token, _, finish = lex_one(lex_context, line, pos)
+ if not token then break end
+
+ local new_colour = token_colours[token]
+ if new_colour ~= colour then
+ term.setTextColor(new_colour)
+ colour = new_colour
+ end
+
+ term.write(line:sub(pos, finish))
+ pos = finish + 1
end
+
+ term.write(line:sub(pos))
end
local tCompletions
@@ -352,7 +351,7 @@ local tMenuFuncs = {
if bReadOnly then
set_status("Access denied", false)
else
- local ok, _, fileerr = save(sPath, function(file)
+ local ok, _, fileerr = save(sPath, function(file)
for _, sLine in ipairs(tLines) do
file.write(sLine .. "\n")
end
@@ -547,7 +546,7 @@ local function acceptCompletion()
-- Append the completion
local sCompletion = tCompletions[nCompletion]
tLines[y] = tLines[y] .. sCompletion
- setCursor(x + #sCompletion , y)
+ setCursor(x + #sCompletion, y)
end
end
@@ -805,7 +804,7 @@ while bRunning do
-- Input text
local sLine = tLines[y]
tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x)
- setCursor(x + #param , y)
+ setCursor(x + #param, y)
end
elseif sEvent == "mouse_click" then
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua
index f3b9f7a08..a14e999ef 100644
--- a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua
+++ b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua
@@ -191,7 +191,7 @@ end
-- Show MOTD
if settings.get("motd.enable") then
- shell.run("motd")
+ shell.run("/rom/programs/motd")
end
-- Run the user created startup, either from disk drives or the root
diff --git a/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java b/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java
new file mode 100644
index 000000000..69e9d3c07
--- /dev/null
+++ b/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
+//
+// SPDX-License-Identifier: MPL-2.0
+
+package dan200.computercraft.core.util;
+
+import dan200.computercraft.api.lua.LuaValues;
+import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
+class StringUtilTest {
+ @ParameterizedTest
+ @ValueSource(strings = { "hello\nworld", "hello\n\rworld", "hello\rworld" })
+ public void getClipboardString_returns_a_single_line(String input) {
+ var result = StringUtil.getClipboardString(input);
+ assertEquals(LuaValues.encode("hello"), result);
+ }
+
+ @Test
+ public void getClipboardString_limits_length() {
+ var input = "abcdefghijklmnop".repeat(50);
+ var result = StringUtil.getClipboardString(input);
+ assertEquals(StringUtil.MAX_PASTE_LENGTH, result.limit());
+
+ assertEquals(
+ LuaValues.encode(input.substring(0, StringUtil.MAX_PASTE_LENGTH)),
+ result
+ );
+ }
+}
diff --git a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua
index 46c77dcf8..8f50b0d56 100644
--- a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua
+++ b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua
@@ -94,6 +94,7 @@ describe("The colors library", function()
end)
it("errors on out-of-range colours", function()
+ expect.error(colors.toBlit, 0):eq("Colour out of range")
expect.error(colors.toBlit, -120):eq("Colour out of range")
expect.error(colors.toBlit, 0x10000):eq("Colour out of range")
end)
diff --git a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua
index 89e4e2993..4ad3d797c 100644
--- a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua
+++ b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua
@@ -59,6 +59,16 @@ describe("The window library", function()
expect.error(w.setTextColour, nil):eq("bad argument #1 (number expected, got nil)")
expect.error(w.setTextColour, -5):eq("Colour out of range")
+ expect.error(w.setTextColour, 0):eq("Colour out of range")
+ expect.error(w.setTextColour, 0x10000):eq("Colour out of range")
+ end)
+
+ it("accepts valid colours", function()
+ local w = mk()
+ for i = 0, 15 do
+ w.setBackgroundColour(2 ^ i)
+ expect(w.getBackgroundColour()):eq(2 ^ i)
+ end
end)
it("supports invalid combined colours", function()
diff --git a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md
index 9d5af0531..e73c4f62f 100644
--- a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md
+++ b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md
@@ -67,7 +67,7 @@ This comment was never finished.
1 | --[=[
| ^^^^^ Comment was started here.
We expected a closing delimiter (]=]) somewhere after this comment was started.
-1:1-1:5 ERROR --[=[
+1:1-1:5 COMMENT --[=[
```
Nested comments are rejected, just as Lua 5.1 does:
@@ -191,7 +191,7 @@ This string was never finished.
1 | return [[
| ^^ String was started here.
We expected a closing delimiter (]]) somewhere after this string was started.
-1:8-1:9 ERROR [[
+1:8-1:9 STRING [[
```
We also handle malformed opening strings: