better UX for gomuks built-in commands

Fixes #157
This commit is contained in:
Henry Hiles 2026-11-06 10:41:19 -05:00
commit dbc42b0ec2
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
2 changed files with 267 additions and 75 deletions

View file

@ -6,6 +6,7 @@ import "package:nexus/models/content/image_pack.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/content/poll_start.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/models/text_command.dart";
part "composer_draft.freezed.dart";
@ -14,6 +15,7 @@ class const ComposerDraft({
// Formatted like ":id#name#", see the composer's tagTextFormatter
final String text = "",
final IList<Tag> tags = const .empty(),
final IList<TextCommand> textCommands = const .empty(),
final int? relatedEvent,
final RelationType relationType = .reply,

View file

@ -0,0 +1,48 @@
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
enum TextCommandGroup { messageType, format }
// Prefixes gomuks parses out of sent text, in the order it expects them
enum TextCommand(final String description, {final TextCommandGroup? group}) {
me("Send as an emote", group: .messageType),
notice("Send as a notice", group: .messageType),
unencrypted("Send unencrypted, even if the room is encrypted"),
rawinputbody("Use the text as the body as-is, rather than from the HTML"),
plain("Send without any formatting", group: .format),
html("Send with only HTML, without markdown", group: .format),
htmlmd("Send with both HTML and markdown", group: .format),
rainbow("Send in rainbow colors, with markdown", group: .format);
String get prefix => "/$name";
bool completes(String text) =>
text.startsWith("/") && prefix.startsWith(text.toLowerCase());
// Replaces any command from the same group, as gomuks only uses one
IList<TextCommand> addTo(IList<TextCommand> commands) => .new(
commands
.where(
(other) => other != this && (group == null || other.group != group),
)
.toIList()
.add(this)
.sortedBy((command) => command.index),
);
static String prefixOf(IList<TextCommand> commands) =>
commands.map((command) => "${command.prefix} ").join();
static ({IList<TextCommand> commands, String text}) parse(
String text, [
IList<TextCommand> commands = const .empty(),
]) => switch (values.firstWhereOrNull(
(command) => text.toLowerCase().startsWith("${command.prefix} "),
)) {
final command? => parse(
text.substring(command.prefix.length + 1),
command.addTo(commands),
),
null => (commands: commands, text: text),
};
}