-
Notifications
You must be signed in to change notification settings - Fork 0
/
cli.c
310 lines (298 loc) · 10.7 KB
/
cli.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/**
* @file cli.c
* Implements a generic console that can read command strings. Command
* strings are not handled in this file.
* Interface-specific (UART, SPI, etc...) handling should be done in
* another file, this file abstracts it.
*
* This code assumes that the connected terminal emulates a VT-100.
*/
/* XDCtools Header files */
#include <xdc/runtime/System.h>
#include <xdc/std.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include "cli.h"
#include "commands.h"
#define CLI_EMPTY_LINELEN -1 // Signifies an "unused" history buffer.
#define CLI_PROMPT "-> "
// functions to handle sub cases for the CLI.
static void cli_handle_return(CLIContext *context);
static void cli_handle_esc(CLIContext *context);
static void cli_handle_backspace(CLIContext *context);
// Helper functions
static void move_line_index(CLIContext *context, bool forwards);
/**
* Initializes memory for a CLI context.
* @param context: CLI context to init.
*/
void cli_context_init(CLIContext *context) {
int i;
context->cursor = NULL;
context->line_idx = 0;
for (i = 0; i < CLI_BUFCNT; i++) {
// Set the line length
context->lines[i].len = CLI_EMPTY_LINELEN;
}
}
/**
* CLI Printf function. Same syntax as printf.
* @param context CLI context to print to
* @param format printf style format string
*/
void cli_printf(CLIContext *context, const char *format, ...) {
va_list args;
int num_print;
char output_buf[PRINT_BUFLEN];
// Init variable args list.
va_start(args, format);
// Print to buffer and destroy varargs list.
/*
* TODO: ideally, we'd implement printf with a circular buffer, so we can
* print a string of any length.
* Note: vsnprintf uses the heap.
*/
num_print = vsnprintf(output_buf, PRINT_BUFLEN, format, args);
va_end(args);
// Write the shorter value between the buffer size and num_print
context->cli_write(output_buf,
num_print > PRINT_BUFLEN ? PRINT_BUFLEN : num_print);
}
/**
* Runs the embedded CLI for this program. The provided context exposes read
* and write functions for a communication interface.
* @param context: CLI context for I/O.
*/
void start_cli(CLIContext *context) {
CLI_Line *current_line;
char input;
/*
* CLI loop, reads a line of data then handles it according to
* avaliable targets.
*/
while (1) {
// Print prompt.
context->cli_write(CLI_PROMPT, sizeof(CLI_PROMPT) - 1);
current_line = &(context->lines[context->line_idx]);
// Initialize cursor location to start of buffer.
context->cursor = current_line->line_buf;
// Zero length of buffer.
current_line->len = 0;
/*
* Set history entry after this one to unused.
* This is why we need one more entry in history buffer than
* we actually use.
*/
move_line_index(context, true);
context->lines[context->line_idx].len = CLI_EMPTY_LINELEN;
// Revert change to current line idx.
move_line_index(context, false);
// Read data until a LF is found.
do {
context->cli_read(&input, 1);
switch (input) {
case '\r': // CR, or enter on most consoles.
cli_handle_return(context);
break;
case '\b':
cli_handle_backspace(context);
break;
case '\x1b':
cli_handle_esc(context);
// Required because some escape sequences update the line idx.
current_line = &(context->lines[context->line_idx]);
break;
default:
if (current_line->len >= CLI_MAX_LINE - 1) {
break; // Do not echo any more data, nor write to buffer.
}
// Simply echo character, and set in buffer
context->cli_write(&input, 1);
*(context->cursor) = input;
// Move to next location in buffer
(context->cursor)++;
/*
* If we have added to the end of the buffer, increase
* line length
*/
if (context->cursor - current_line->line_buf >
current_line->len) {
current_line->len =
context->cursor - current_line->line_buf;
}
break;
}
} while (input != '\r');
}
}
/**
* Handles a return character ('\r') on the CLI.
* Will submit the null terminated command string to a CLI handler function.
* Following return of handler, will advance CLI history buffer to next entry
* and return.
* @param context: CLI context to use for this command.
*/
static void cli_handle_return(CLIContext *context) {
CLI_Line *current_line = &context->lines[context->line_idx];
// Write a newline and carriage return to the console.
context->cli_write("\r\n", 2);
// Null terminate the command.
current_line->line_buf[current_line->len] = '\0';
/**
* If line is empty, do not advance to next line buffer or
* handle command.
* Else, advance to the next index in the line buffer.
*/
if (current_line->len == 0) {
current_line->len = CLI_EMPTY_LINELEN;
return;
}
move_line_index(context, true);
// handle command.
handle_command(context, current_line->line_buf);
}
/**
* Handles a backspace ('\b') character on the commandline.
* If the cursor is at the end of the current statement and there are
* characters to remove, deletes a character from the screen and buffer, and
* moves the buffer pointer as well as cursor backwards.
* @param context: CLI context to use
*/
static void cli_handle_backspace(CLIContext *context) {
CLI_Line *current_line = &context->lines[context->line_idx];
/*
* Backspace: send backspace and space to clear
* character. Edit the value in the command buffer as well.
*/
/*
* Ensure that the cursor is at the end of the line,
* and there is data to delete.
*/
if (context->cursor - current_line->line_buf == current_line->len &&
context->cursor != current_line->line_buf) {
// This moves the cursor back, writes a space, and moves it again
// Effectively, this clears the character before the cursor.
context->cli_write("\b\x20\b", 3);
// Move cursor back, and nullify the character there.
context->cursor--;
*(context->cursor) = '\0';
// Lower the line length.
current_line->len--;
}
}
/**
* Handles an escape character (typically will resolve to a control code) on
* the commandline.
* @param context: Context escape character recieved on.
*/
static void cli_handle_esc(CLIContext *context) {
CLI_Line *current_line = &context->lines[context->line_idx];
char esc_buf[2];
/**
* Escape sequence. Read more characters from the
* input to see if we can handle it.
*/
context->cli_read(esc_buf, sizeof(esc_buf));
if (esc_buf[0] != '[') {
/**
* Not an escape sequence we understand. Print the buffer
* contents to the terminal and bail.
*/
context->cli_write(esc_buf, sizeof(esc_buf));
return;
}
/*
* If the escape sequence starts with '\x1b]',
* switch on next char.
*/
switch (esc_buf[1]) {
case 'A': // Up arrow
/**
* If enough history exists, move the CLI commandline
* to the prior command.
*/
move_line_index(context, false);
if (context->lines[context->line_idx].len != CLI_EMPTY_LINELEN) {
// Update the line buffer and current line.
current_line = &(context->lines[context->line_idx]);
// Set cursor to end of line buffer.
context->cursor = ¤t_line->line_buf[current_line->len];
// Clear line of console and write line from history.
// Clear line, and reset cursor.
context->cli_write("\x1b[2K\r", 5);
// Write prompt
context->cli_write(CLI_PROMPT, sizeof(CLI_PROMPT) - 1);
context->cli_write(current_line->line_buf, current_line->len);
} else {
// Reset current line idx.
move_line_index(context, true);
}
break;
case 'B': // Down arrow
/**
* If enough history exists, move the CLI commandline
* to the next command.
*/
move_line_index(context, true);
if (context->lines[context->line_idx].len != CLI_EMPTY_LINELEN) {
// Update the line buffer, cursor, and current line.
current_line = &(context->lines[context->line_idx]);
// Set cursor to end of line buffer.
context->cursor = ¤t_line->line_buf[current_line->len];
// Clear line of console and write line from history.
// Clear line, and reset cursor.
context->cli_write("\x1b[2K\r", 5);
// Write prompt
context->cli_write(CLI_PROMPT, sizeof(CLI_PROMPT) - 1);
context->cli_write(current_line->line_buf, current_line->len);
} else {
// Reset current line idx.
move_line_index(context, false);
}
break;
case 'C': // Right arrow
if (context->cursor - current_line->line_buf != current_line->len) {
// Move cursor and print the forward control seq.
(context->cursor)++;
context->cli_write("\x1b", 1);
context->cli_write(esc_buf, sizeof(esc_buf));
}
break;
case 'D': // Left arrow
if (context->cursor != current_line->line_buf) {
// Move cursor and print the backward control seq.
(context->cursor)--;
context->cli_write("\x1b", 1);
context->cli_write(esc_buf, sizeof(esc_buf));
}
break;
default:
// Ignore other escape sequences.
break;
}
}
/**
* Moves the index of the CLI line history buffer.
* Sets the index only. Does not edit remainder of context.
* @param context: CLI context to manipulate
* @param forwards: should the index be moved one buffer forwards or back?
*/
static void move_line_index(CLIContext *context, bool forwards) {
int new_idx;
/*
* This function simulates modulo, since we are aware exactly how far
* forwards or back we are moving the index, and our modulo is constant.
*/
if (forwards) {
new_idx = context->line_idx + 1;
if (new_idx == CLI_BUFCNT)
new_idx = 0;
context->line_idx = new_idx;
} else {
new_idx = context->line_idx - 1;
if (new_idx < 0)
new_idx = CLI_BUFCNT - 1;
context->line_idx = new_idx;
}
}