aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2025-07-21 00:19:52 +0300
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2025-07-21 00:33:13 +0300
commit48dcfd22f9c2db5cf6745eaec0ff28895858c638 (patch)
tree0eb2c9658df557b8418d4bbdc8b9aa25fb15cd6d
parent9903c408f2ffc7a42f4fcead54eadc567c0f3669 (diff)
downloadsciteco-48dcfd22f9c2db5cf6745eaec0ff28895858c638.tar.gz
support <==> and <===> for printing octal and hexadecimal numbers
* These are famously in DEC TECO-11, but also in Video TECO. * The implementation is tricky. They need to use lookahead states, but this would be inacceptable during interactive execution. Therefore only if executing from the end of the command line `==` and `===` are allowed to print multiple values. The number is therefore also not popped form the stack immediately but only peeked. It's popped only when it has been decided that the command has ended. * This may break existing macros that use multiple `=` in a row to print multiple values from the stack. You will now e.g. have to insert whitespace to separate such `=` commands.
-rw-r--r--TODO1
-rw-r--r--doc/sciteco.1.in4
-rw-r--r--src/cmdline.h7
-rw-r--r--src/core-commands.c224
-rw-r--r--src/expressions.c2
-rw-r--r--src/sciteco.h6
-rw-r--r--tests/testsuite.at11
7 files changed, 207 insertions, 48 deletions
diff --git a/TODO b/TODO
index 6f044b5..b2aab0f 100644
--- a/TODO
+++ b/TODO
@@ -344,6 +344,7 @@ Features:
The DEC behavior could be achieved by always searching till the end
of the buffer, but excluding all matches beyond the target range.
* ^A, :Gq, T, ^T and stdio in general
+ * :=, :==, :=== to inhibit the linefeed.
* ^W was an immediate action command to repaint the screen.
This could be a regular command to allow refreshing in long loops.
Video TECO had ET for the same purpose.
diff --git a/doc/sciteco.1.in b/doc/sciteco.1.in
index c7ad1a9..82c3376 100644
--- a/doc/sciteco.1.in
+++ b/doc/sciteco.1.in
@@ -141,6 +141,10 @@ Batch mode does not have these restrictions.
.IP \(bu
A few commands that modify the command line are only available
in interactive mode.
+.IP \(bu
+A few commands like \fB^C\fP are disallowed in interactive mode
+when run from the command-line macro or behave slightly
+differently compared to batch mode (e.g. \fB$$\fP or \fB==\fP).
.RE
.
.LP
diff --git a/src/cmdline.h b/src/cmdline.h
index ebdf1e1..1a57db5 100644
--- a/src/cmdline.h
+++ b/src/cmdline.h
@@ -84,6 +84,13 @@ teco_cmdline_keymacro_c(gchar key, GError **error)
return TRUE;
}
+/** Check whether we are executing directly from the end of the command line. */
+static inline gboolean
+teco_cmdline_is_executing(teco_machine_main_t *ctx)
+{
+ return ctx == &teco_cmdline.machine && ctx->macro_pc == teco_cmdline.effective_len;
+}
+
extern gboolean teco_quit_requested;
/*
diff --git a/src/core-commands.c b/src/core-commands.c
index 7222a9f..2ccb8ec 100644
--- a/src/core-commands.c
+++ b/src/core-commands.c
@@ -67,6 +67,23 @@ gboolean teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_mach
##__VA_ARGS__ \
)
+/**
+ * @class TECO_DEFINE_STATE_START
+ * @implements TECO_DEFINE_STATE_COMMAND
+ * @ingroup states
+ *
+ * Base state for everything where a new command can begin
+ * (the start state itself and all lookahead states).
+ */
+#define TECO_DEFINE_STATE_START(NAME, ...) \
+ TECO_DEFINE_STATE_COMMAND(NAME, \
+ .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */ \
+ .is_start = TRUE, \
+ .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE, \
+ ##__VA_ARGS__ \
+ )
+
+static teco_state_t *teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error);
static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error);
static teco_state_t *teco_state_ctlc_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error);
@@ -543,34 +560,173 @@ teco_state_start_cmdline_pop(teco_machine_main_t *ctx, GError **error)
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CMDLINE, "");
}
-/*$ "=" print
- * <n>= -- Show value as message
+/**
+ * Print number from stack in the given radix.
+ *
+ * It must be popped manually, so we can call it multiple times
+ * on the same number.
+ */
+static gboolean
+teco_print(guint radix, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+ if (!teco_expressions_args()) {
+ teco_error_argexpected_set(error, "=");
+ return FALSE;
+ }
+ /*
+ * FIXME: There should be a raw output function,
+ * also to allow output without trailing LF.
+ * Perhaps repurpose teco_expressions_format().
+ */
+ const gchar *fmt = "%" TECO_INT_MODIFIER "d";
+ switch (radix) {
+ case 8: fmt = "%" TECO_INT_MODIFIER "o"; break;
+ case 16: fmt = "%" TECO_INT_MODIFIER "X"; break;
+ }
+ teco_interface_msg(TECO_MSG_USER, fmt, teco_expressions_peek_num(0));
+ return TRUE;
+}
+
+/*$ "=" "==" "===" print
+ * <n>= -- Print integer as message
+ * <n>==
+ * <n>===
*
* Shows integer <n> as a message in the message line and/or
* on the console.
- * It is currently always formatted as a decimal integer and
- * shown with the user-message severity.
+ * One \(lq=\(rq formats the integer as a signed decimal number,
+ * \(lq==\(rq formats as an unsigned octal number and
+ * \(lq===\(rq as an unsigned hexadecimal number.
+ * It is logged with the user-message severity.
* The command fails if <n> is not given.
+ *
+ * A noteworthy quirk is that \(lq==\(rq and \(lq===\(rq
+ * will print 2 or 3 numbers in succession when executed
+ * from interactive mode at the end of the command line
+ * in order to guarantee immediate feedback.
+ *
+ * If you want to print multiple values from the stack,
+ * you have to put the \(lq=\(rq into a pass-through loop
+ * or separate the commands with whitespace.
*/
-/**
- * @todo perhaps care about current radix
- * @todo colon-modifier to suppress line-break on console?
+/*
+ * In order to imitate TECO-11 closely, we apply the lookahead
+ * strategy -- `=` and `==` are not executed immediately but only
+ * when a non-`=` character is parsed (cf. `$$` and `^C^C`).
+ * However, this would be very annoying during interactive
+ * execution, therefore we still print the number immediately
+ * and perhaps multiple times:
+ * Typing `===` prints the number first in decimal,
+ * then octal and finally in hexadecimal.
+ * This won't happen e.g. in a loop that is closed on the command-line.
+ *
+ * FIXME: Support colon-modifier to suppress line-break on console.
*/
-static void
-teco_state_start_print(teco_machine_main_t *ctx, GError **error)
+TECO_DECLARE_STATE(teco_state_print_decimal);
+TECO_DECLARE_STATE(teco_state_print_octal);
+
+static gboolean
+teco_state_print_decimal_initial(teco_machine_main_t *ctx, GError **error)
{
- if (!teco_expressions_eval(FALSE, error))
- return;
- if (!teco_expressions_args()) {
- teco_error_argexpected_set(error, "=");
- return;
+ if (ctx->flags.mode > TECO_MODE_NORMAL || !teco_cmdline_is_executing(ctx))
+ return TRUE;
+ /*
+ * Interactive invocation:
+ * don't yet pop number as we may have to print it repeatedly
+ */
+ return teco_print(10, error);
+}
+
+static teco_state_t *
+teco_state_print_decimal_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+{
+ if (chr == '=')
+ return &teco_state_print_octal;
+
+ if (ctx->flags.mode == TECO_MODE_NORMAL) {
+ if (!teco_cmdline_is_executing(ctx) && !teco_print(10, error))
+ return NULL;
+ teco_expressions_pop_num(0);
}
- teco_int_t v;
- if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
- return;
- teco_interface_msg(TECO_MSG_USER, "%" TECO_INT_FORMAT, v);
+ return teco_state_start_input(ctx, chr, error);
+}
+
+/*
+ * Due to the deferred nature of `=`,
+ * it is valid to end in this state as well.
+ */
+static gboolean
+teco_state_print_decimal_end_of_macro(teco_machine_main_t *ctx, GError **error)
+{
+ if (teco_cmdline_is_executing(ctx) || ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+ if (!teco_print(10, error))
+ return FALSE;
+ teco_expressions_pop_num(0);
+ return TRUE;
+}
+
+TECO_DEFINE_STATE_START(teco_state_print_decimal,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_print_decimal_initial,
+ .end_of_macro_cb = (teco_state_end_of_macro_cb_t)
+ teco_state_print_decimal_end_of_macro
+);
+
+static gboolean
+teco_state_print_octal_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL || !teco_cmdline_is_executing(ctx))
+ return TRUE;
+ /*
+ * Interactive invocation:
+ * don't yet pop number as we may have to print it repeatedly
+ */
+ return teco_print(8, error);
+}
+
+static teco_state_t *
+teco_state_print_octal_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+{
+ if (chr == '=') {
+ if (ctx->flags.mode == TECO_MODE_NORMAL) {
+ if (!teco_print(16, error))
+ return NULL;
+ teco_expressions_pop_num(0);
+ }
+ return &teco_state_start;
+ }
+
+ if (ctx->flags.mode == TECO_MODE_NORMAL) {
+ if (!teco_cmdline_is_executing(ctx) && !teco_print(8, error))
+ return NULL;
+ teco_expressions_pop_num(0);
+ }
+ return teco_state_start_input(ctx, chr, error);
+}
+
+/*
+ * Due to the deferred nature of `==`,
+ * it is valid to end in this state as well.
+ */
+static gboolean
+teco_state_print_octal_end_of_macro(teco_machine_main_t *ctx, GError **error)
+{
+ if (teco_cmdline_is_executing(ctx) || ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+ if (!teco_print(8, error))
+ return FALSE;
+ teco_expressions_pop_num(0);
+ return TRUE;
}
+TECO_DEFINE_STATE_START(teco_state_print_octal,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_print_octal_initial,
+ .end_of_macro_cb = (teco_state_end_of_macro_cb_t)
+ teco_state_print_octal_end_of_macro
+);
+
/*$ A
* [n]A -> code -- Get character code from buffer
* -A -> code
@@ -651,6 +807,7 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
.modifier_colon = 1},
['X'] = {&teco_state_copytoqreg,
.modifier_at = TRUE, .modifier_colon = 1},
+ ['='] = {&teco_state_print_decimal},
/*
* Arithmetics
@@ -703,7 +860,6 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
.modifier_colon = 1},
['D'] = {&teco_state_start, teco_state_start_delete_chars,
.modifier_colon = 1},
- ['='] = {&teco_state_start, teco_state_start_print},
['A'] = {&teco_state_start, teco_state_start_get}
};
@@ -899,11 +1055,7 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
teco_ascii_toupper(chr), error);
}
-TECO_DEFINE_STATE_COMMAND(teco_state_start,
- .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */
- .is_start = TRUE,
- .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE
-);
+TECO_DEFINE_STATE_START(teco_state_start);
/*$ "F<" ":F<"
* F< -- Go to loop start or jump to beginning of macro
@@ -1787,14 +1939,8 @@ teco_state_escape_end_of_macro(teco_machine_t *ctx, GError **error)
return teco_expressions_discard_args(error);
}
-TECO_DEFINE_STATE_COMMAND(teco_state_escape,
- .end_of_macro_cb = teco_state_escape_end_of_macro,
- /*
- * The state should behave like teco_state_start
- * when it comes to function key macro masking.
- */
- .is_start = TRUE,
- .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE
+TECO_DEFINE_STATE_START(teco_state_escape,
+ .end_of_macro_cb = teco_state_escape_end_of_macro
);
/*
@@ -1827,18 +1973,8 @@ teco_state_ctlc_initial(teco_machine_main_t *ctx, GError **error)
return TRUE;
}
-TECO_DEFINE_STATE_COMMAND(teco_state_ctlc,
- .initial_cb = (teco_state_initial_cb_t)teco_state_ctlc_initial,
- /*
- * At the end of a macro, "return" is allowed, but basically a no-op.
- */
- .end_of_macro_cb = NULL,
- /*
- * The state should behave like teco_state_start
- * when it comes to function key macro masking.
- */
- .is_start = TRUE,
- .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE
+TECO_DEFINE_STATE_START(teco_state_ctlc,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_ctlc_initial
);
static teco_state_t *
diff --git a/src/expressions.c b/src/expressions.c
index f802c6e..ede54d2 100644
--- a/src/expressions.c
+++ b/src/expressions.c
@@ -387,7 +387,7 @@ teco_expressions_clear(void)
* @param buffer The output buffer of at least TECO_EXPRESSIONS_FORMAT_LEN characters.
* The output string will be null-terminated.
* @param number The number to format.
- * @param table The local Q-Register table that contains the appropriate radix register (^R).
+ * @param qreg The radix register (^R).
* @return A pointer into buffer to the beginning of the formatted number.
*/
gchar *
diff --git a/src/sciteco.h b/src/sciteco.h
index 2dc4749..40a3548 100644
--- a/src/sciteco.h
+++ b/src/sciteco.h
@@ -25,10 +25,12 @@
#if TECO_INTEGER == 32
typedef gint32 teco_int_t;
-#define TECO_INT_FORMAT G_GINT32_FORMAT
+#define TECO_INT_MODIFIER G_GINT32_MODIFIER
+#define TECO_INT_FORMAT G_GINT32_FORMAT
#elif TECO_INTEGER == 64
typedef gint64 teco_int_t;
-#define TECO_INT_FORMAT G_GINT64_FORMAT
+#define TECO_INT_MODIFIER G_GINT64_MODIFIER
+#define TECO_INT_FORMAT G_GINT64_FORMAT
#else
#error Invalid TECO integer storage size
#endif
diff --git a/tests/testsuite.at b/tests/testsuite.at
index a199f62..f7a8423 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -52,7 +52,7 @@ TE_CHECK([[2%a,%a - 3"N(0/0)' $]], 0, ignore, ignore)
# c) The empty "list" element is equivalent to 0, so
# "1,,2" is equivalent to "1,0,2" and (1,) to (1,0).
TE_CHECK([[(1,) "~|(0/0)']], 0, ignore, ignore)
-TE_CHECK([[1,(2)==]], 0, ignore, ignore)
+TE_CHECK([[1,(2)= =]], 0, ignore, ignore)
AT_CLEANUP
AT_SETUP([Exit status])
@@ -174,6 +174,15 @@ TE_CHECK([[(]]TE_MAXINT64[[)-(]]TE_MAXINT64[[)"N(0/0)']], 0, ignore, ignore)
TE_CHECK([[(]]TE_MININT64[[)-(]]TE_MININT64[[)"N(0/0)']], 0, ignore, ignore)
AT_CLEANUP
+AT_SETUP([Printing numbers])
+# Must print only one hexadecimal number.
+TE_CHECK([[255===]], 0, stdout, ignore)
+AT_FAIL_IF([test `$GREP -v "^Info:" stdout | wc -l` -ne 1], 0, ignore, ignore)
+# Will print a decimal, octal and 2 hexadecimal numbers.
+TE_CHECK_CMDLINE([[2<255===>]], 0, stdout, ignore)
+AT_FAIL_IF([test `$GREP -v "^Info:" stdout | wc -l` -ne 4], 0, ignore, ignore)
+AT_CLEANUP
+
AT_SETUP([Convert between line and glyph positions])
TE_CHECK([[@I/1^J2^J3/J 2^QC :^Q-3"N(0/0)']], 0, ignore, ignore)
AT_CLEANUP