femto - project showcase
Femto is a minimal CLI text editor made in C with zero dependencies inspired by Antirez’s kilo editor. This retrospective contains all noteworthy progress made within this project starting from the idea until the ‘end’.
I’ve created this post about a year ago, but never published it due to missing content/relevant information. Coming back a year later I see this from a different perspective. This is a good rough overview before diving into the C source. The testing framework cilicon might be a good inspiration for unit-testing with C, too.
General information
Features:
- open, create, save text files
- navigation
- edition
Bugs:
- bad line wrapping
- unknown escape sequences were printed to screen (e.g. Shift+Arrow key)
- rare unhandeld glitches while editing
Personal project outcome:
- deal with terminal’s raw mode
- use terminal as framebuffer
- CLI dialogs
- CI testing a C project
- develop C unit testing lib Cilicon
- improve gdb skills to find and fix memory leaks
Motivation
- active C project to gain more experience
- zero dependencies
- text editors have a lot of feature potential (highlighting, tabs, views, macros,…)
- coping with the terminal’s raw mode can be handy for further projects
Internal structure
Femto consists of seven components:
- Session:
- provides abstract editor API
- navigation
- edition
- Screen:
- render framebuffer
- update status bar
- set cursors
- Buffer:
- data+length structure
- used as framebuffer
- Terminal:
- control raw mode
- provide user input, size and cursor position
- FileIO:
- load file to internal representation
- save file from internal representation
- UI: Dialogs for user interaction
- Helper
- logger
- math functions (min,max,clamp, poor man’s log10)
Session
The session type contains the state of the running application:
typedef struct {
/* Name of the read file */
char *filename;
/* Dimensions of the terminal */
TerminalSize terminal_size;
/* Position on the screen */
TerminalPosition cursor_position;
/* Offset between file content and screen */
TerminalPosition offset;
/* Array of lines which hold the content */
Line *lines;
size_t line_count;
/* Total length of all line lengths combined */
size_t content_length;
/* Dirty flag */
bool dirty;
} Session;
Any action a user makes affects this state.
- navigation updates
cursor_position
andoffset
- edition updates the same as navigation plus
lines
,line_count
,content_length
anddirty
. - save/load file updates
filename
anddirty
Screen
- update output on terminal
- generate status bar
Buffer
A simple data+length structure with functions to write more convenient to the data:
typedef struct
{
char *data;
unsigned length;
}Buffer;
This struture is currently only used by the screen module as framebuffer.
Terminal
- enable/Disable Raw mode
- determine terminal resolution
- determine cursor position
- get user input and replace it with a pseudo key if compound
It’s purpose overlaps with the screen module. Both are somehow manipulating the terminal’s behaviour.
FileIO
FileIO uses the filename given in the session object and stores the file’s content into it.
UI
Handle dialogs including getting the users input, generating the right status bar and trigger a refresh. It comes with two dialogs:
- safe file
- quit (do you really discard unsaved changes?)
Internally it has a generic dialog function which could be used to add more.
Helper
The helper has two major responisibilities:
- logging
- provide shortcuts for often used mathematical functions
The logger has a printf
alike formatter function which automatically includes the module name and line number where the logger gets called.
This is accomplished by hiding the real formatter function behind a preprocessor directive which adds the module name and line number in place.
cilicon - unit test library
Cilicon is a syntactic sugar library which ‘frames’ a unit test using concepts like ‘it’, ‘suite’ and ‘expect_…’ inspired by ruby’s rspec.
Each C module has it’s own test suite represented in an extra test file. For
example the module buffer
has it’s own test_suite_buffer
with the file
buffer_test.c
:
void test_create_buffer()
{
TEST_IT_NAME("creates a buffer");
Buffer *buf = fe_create_buffer();
if( ! expect_not_null( buf )) return;
fe_free_buffer(buf);
TEST_OK
}
void test_append_to_buffer()
{
TEST_IT_NAME("appends to a buffer");
const char *test_string = "const char *test_string";
Buffer *buf = fe_create_buffer();
if( ! expect_not_null( buf )) return;
fe_append_to_buffer(buf, test_string, strlen(test_string));
if( ! expect_b_eq( (void*) test_string, buf->data, strlen( test_string ), buf->length )) goto cleanup;
TEST_OK;
cleanup:
fe_free_buffer(buf);
}
void test_suite_buffer()
{
TEST_SUITE_NAME("Buffer");
test_create_buffer();
test_append_to_buffer();
}
Which outputs the following on success:
---- Buffer ----
it creates a buffer OK
it appends to a buffer OK
ALL PASSED
And on failure:
---- Buffer ----
it creates a buffer OK
it appends to a buffer FAIL
Expected equal buffers, got unequal.
1 of 2 expects failed.
FATALITY
Thoughts
- Having some convenience features on writing tests is key for C since there is nothing one can build from with zero dependencies.
Goto
is bad. Isn’t it? But there are cases wheregoto
is better than for example wet code. Big C projects like the linux kernel or qemu usegoto
for implementing clean ups. For example qemu’s img_create.- A year later, I’m not using femto at all. I though about using it for commit messages or at least when administrating my servers to let new use-cases and valuable feature ideas evolve, but nope. But that is ok.