Edit the function key messages as their file

The messages were twelve boxes in a dialog, which meant no labels, no
search-and-pounce set, and no way to read a file somebody published. They are
now the file itself: right-click any function key button under the entry
window, or the two buttons in Config ▸ Keyer and messages, and the text of an
N1MM .mc file opens in a plain editor with Import, Export and Back to the
defaults.

MessageFile reads that format by N1MM's rules. A line starting with # is a
comment; every other line is a label, a comma and the message, with && standing
for one & in a label. Which key a line belongs to is decided by where it is and
nothing else: the first twelve lines are F1 to F12 while running, the next
twelve the same keys while searching, and a file that stops part way through the
second twelve leaves the rest sending what they send while running. A blank line
in the middle is a key with nothing in it, which is what the manual says; the
newline that ends the last line is not, which it does not say, but no .mc file
in the world means its final newline as a message.

Two things fall out of this. The buttons now say what the file says, and they
change when the operator moves between running and searching, so the labels are
the documentation N1MM's manual suggests writing them as. And the search set is
real: ESM's F2 while searching can be a different message from the one it sends
while running, which is how the file was always meant to be used.

Settings keep the file text and carry the twelve stored messages into it on
first read, with the labels this program used then, so nobody loses what they
had typed.

The editor is deliberately a text box rather than a grid of fields: importing,
exporting and editing are then the same thing, and the comments an operator
writes in the file survive being edited here. The telnet window's buttons can
use the same editor later.

Running it caught two: Save as the default button ate the Enter key that a
multi-line editor needs, and the button labels did not follow the move between
run and search until they were redrawn rather than only rebuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 08:13:24 +00:00
parent 0c440ed865
commit cf0166f1b2
14 changed files with 498 additions and 129 deletions

View File

@@ -64,4 +64,32 @@ public class SettingsTests : IDisposable
Assert.Equal("dxc.example.net", settings.ClusterHost);
Assert.Equal(7300, settings.ClusterPort);
}
[Fact]
public void TheTwelveMessagesAnOlderFileHoldsBecomeAFunctionKeyFile()
{
Settings settings = Load(
"""
{
"CwMessages": [ "CQ TEST {MYCALL}", "5NN {EXCH}" ]
}
""");
Assert.Empty(settings.CwMessages);
Assert.Equal("F1 CQ,CQ TEST {MYCALL}\nF2 Exch,5NN {EXCH}", settings.CwMessageFile);
}
[Fact]
public void AFileTextThatIsAlreadyThereIsLeftAlone()
{
Settings settings = Load(
"""
{
"CwMessageFile": "F1 CQ,CQ",
"CwMessages": [ "old" ]
}
""");
Assert.Equal("F1 CQ,CQ", settings.CwMessageFile);
}
}

View File

@@ -0,0 +1,95 @@
namespace Nonemm.Session.Tests;
public class MessageFileTests
{
private static string Lines(int count, string what) =>
string.Join('\n', Enumerable.Range(1, count).Select(at => $"F{at} {what},{what} {at}"));
[Fact]
public void ALabelAndAMessageAreSplitOnTheFirstComma()
{
MessageFile file = MessageFile.Parse("F1 CQ, CQ TEST {MYCALL}, AGAIN");
FunctionKey key = file.Keys(isRunning: true)[0];
Assert.Equal("F1 CQ", key.Label);
Assert.Equal("CQ TEST {MYCALL}, AGAIN", key.Message);
}
[Fact]
public void CommentLinesAreNotKeys()
{
MessageFile file = MessageFile.Parse("# CW messages\nF1 CQ,CQ TEST\n#and another\nF2 Exch,5NN");
Assert.Equal("CQ TEST", file.Keys(isRunning: true)[0].Message);
Assert.Equal("5NN", file.Keys(isRunning: true)[1].Message);
}
[Fact]
public void TwoAmpersandsInALabelStandForOne()
{
Assert.Equal("S&P", MessageFile.Parse("S&&P,TEST").Keys(isRunning: true)[0].Label);
}
[Fact]
public void ALineWithNoCommaIsAllMessage()
{
FunctionKey key = MessageFile.Parse("CQ TEST").Keys(isRunning: true)[0];
Assert.Equal("", key.Label);
Assert.Equal("CQ TEST", key.Message);
}
[Fact]
public void AFileOfTwelveLinesSendsTheSameMessagesEitherWay()
{
MessageFile file = MessageFile.Parse(Lines(12, "RUN"));
Assert.Equal("RUN 1", file.Keys(isRunning: true)[0].Message);
Assert.Equal("RUN 1", file.Keys(isRunning: false)[0].Message);
}
[Fact]
public void TheSecondTwelveLinesAreTheSearchingKeys()
{
MessageFile file = MessageFile.Parse(Lines(12, "RUN") + "\n" + Lines(12, "SNP"));
Assert.Equal("RUN 2", file.Keys(isRunning: true)[1].Message);
Assert.Equal("SNP 2", file.Keys(isRunning: false)[1].Message);
}
[Fact]
public void AFileThatStopsPartWayThroughFallsBackToTheRunningMessage()
{
MessageFile file = MessageFile.Parse(Lines(12, "RUN") + "\n" + Lines(3, "SNP"));
Assert.Equal("SNP 3", file.Keys(isRunning: false)[2].Message);
Assert.Equal("RUN 4", file.Keys(isRunning: false)[3].Message);
}
[Fact]
public void AShortFileLeavesTheRestOfTheKeysEmpty()
{
MessageFile file = MessageFile.Parse("F1 CQ,CQ TEST");
Assert.Equal("", file.Keys(isRunning: true)[1].Message);
Assert.Equal(12, file.Keys(isRunning: true).Count);
}
[Fact]
public void TheNewlineThatEndsTheFileIsNotABlankKey()
{
MessageFile file = MessageFile.Parse(Lines(12, "RUN") + "\nF1 SNP,SNP CQ\n");
Assert.Equal("SNP CQ", file.Keys(isRunning: false)[0].Message);
Assert.Equal("RUN 2", file.Keys(isRunning: false)[1].Message);
}
[Fact]
public void ABlankLineInTheMiddleIsAKeyWithNothingInIt()
{
MessageFile file = MessageFile.Parse("F1 CQ,CQ TEST\n\nF3 TU,TU");
Assert.Equal("", file.Keys(isRunning: true)[1].Message);
Assert.Equal("TU", file.Keys(isRunning: true)[2].Message);
}
}