Customization layer - getting started (4coder versions BEFORE 4.1)

Disclaimer

This article is for version of 4coder BEFORE 4.1.

Content

This article is a small introduction to the 4coder customization layer. The goal is to help new users to setup their custom layer, define their key bindings, set their hooks, and add keywords to the syntax highlighting.

Requirements

  • 4coder 4.0.x;
  • to know some C/Cpp;
  • have a compiler installed: Visual Studio on Windows, GCC or Clang on Mac and Linux.

Installation

Installing boils down to unzipping the archive where you want. It's a good idea to put it in a folder without spaces or parenthesis in the path.

On Windows, there are a few things to pay attention to:

  • If the path contains space(s):
  • The build script, buildsuper.bat, may fail to setup Visual Studio (by failing to call vcvarsall.bat), so you would need to do it yourself before trying to build the custom layer;
  • The working directory will need to be the 4coder directory to build the custom layer, otherwise the build script will not work;
  • If the path contains '(' or ')' the build script may fail;
  • Non ASCII characters seem to be fine.

Building

To build 4coder's default custom layer, double click on the buildsuper.bat file at the root of the 4coder directory, or launch it from a console. The later is preferred as you will see the eventual errors. You can pass a file name as the first argument to that script and the file will be compiled instead of the default one (which is 4coder_default_bindings.cpp).

If you launch buildsuper.bat from a path that is not the 4coder installation directory, you need to specify the path to the custom layer code file, even to use the default one. For example, if 4coder is in D:\4coder\ but you compile from D:\, you need to compile with this command

D:\> D:\4coder\buildsuper.bat D:\4coder\4coder_default_bindings.cpp

On windows, the build script will try to call vcvarsall.bat for you, if it hasn't been called yet. If you have several versions of Visual Studio installed, the version that will be used depends to the order defined in the file windows\scripts\setup_cl_generic.bat. Supported version are Visual Studio 2010 and up.

If the compile was successful you should have a custom_4coder.dll and custom_4coder.pdb in the current working directory. If you didn't compile from the 4coder folder, you'll need to copy the dll and pdb in the 4coder directory.

You can't rebuild the custom layer while you're using 4coder as the linker won't be able to overwrite the dll and 4coder doesn't try to dynamically reload the dll.

Don't forget to rebuild after making changes in the code.

Making it easier to update

To make updating to a new version of 4coder as simple as dropping the new files in the directory and not caring about overwriting your modifications, make a copy of 4coder_default_bindings.cpp (called custom_layer.cpp in the rest of this article) and only edit the copy. The custom code doesn't need to be at a specific location as long as you pass the correct file path to the build script. Creating a build.bat file to call the build script with the correct file name will make it easy to compile your custom code.

For Windows: build.bat

@echo off
buildsuper.bat custom_layer.cpp

For linux (and Mac ?): build.sh

#!/bin/bash
./buildsuper.sh custom_layer.cpp

Key bindings

The "entry point" for the custom layer is the get_bindings function in your custom layer file.

First method

The easiest way to change some key bindings is to add a few lines in the get_bindings function, between the begin_bind_helper and end_bind_helper calls, and after the default_keys (or mac_default_keys on mac) call.

Bindings in 4coder are grouped in different keymaps, which are explained a little more in the next section, and to correctly set a binding you need to place it in a keymap using the bind function between begin_map and end_map calls.

In the following example, CTRL + ALT + o is bound to change the active panel and CTRL + k to kill the current buffer.

extern "C" int32_t
get_bindings(void *data, int32_t size){
    Bind_Helper context_ = begin_bind_helper(data, size);
    Bind_Helper *context = &context_;
    
    set_all_default_hooks(context);
#if defined(__APPLE__) && defined(__MACH__)
    mac_default_keys(context);
#else
    default_keys(context);
#endif

    /* Start of new code */
    begin_map( context, mapid_global ); {
        bind( context, 'k', MDFR_CTRL, kill_buffer );
        bind( context, 'o', MDFR_CTRL | MDFR_ALT, change_active_panel );
    } end_map( context );
    /* End of new code */
    
    int32_t result = end_bind_helper(context);
    return(result);
}

Remarks

  • The curly braces after begin_map and before end_map are not needed. They only add an visual indentation level to the bind calls.
  • If the default_keys call had setup some bindings with CRTL + ALT + o or CTRL + k they will be overwritten, meaning pressing CTRL + ALT + o will only do what you asked, and not what default_keys asked. It's actually a bit more complicated as keymaps can be inherited. Instead of using mapid_global, you might need to use mapid_file or default_code_map.
  • You can't set the same key binding to two different function.
  • You can set two different binding to the same function.
  • The available modifier are MDFR_NONE, MDFR_CTRL, MDFR_ALT, MDFR_CMND (only on Mac), MDFR_SHIFT.
  • You can combine several of them with the bitwise OR operator, for example MDFR_CTRL | MDFR_ALT.
  • If you want to bind an upper case letter, you need to specify the upper case letter as the second parameter and not use MDFR_SHIFT. For example bind( context, 'K', MDFR_NONE, kill_buffer ); will bind K to kill_buffer.
  • To bind a character that is not an ASCII character (for example 'é', 'è', 'ç'...) you can use the get_key_code function. For example: bind( context, get_key_code( "é" ), MDFR_ALT, execute_any_cli );.
  • Key that aren't an ASCII character are defined in 4coder_API\4coder_keycodes.h. Here is a list of them as of version 4.0.30.
  • key_back, key_up, key_down, key_left, key_right, key_del, key_insert, key_home, key_end, key_page_up, key_page_down, key_esc;
  • key_f1, key_f2, key_f3, key_f4, key_f5, key_f6, key_f7, key_f8, key_f9, key_f10, key_f11, key_f12, key_f13, key_f14, key_f15, key_f16;
  • key_mouse_left, key_mouse_right, key_mouse_left_release, key_mouse_right_release, key_mouse_wheel, key_mouse_move;
  • key_animate, key_click_activate_view, key_click_deactivate_view.

Second method

A slightly more involved way of setting up your key bindings is to copy the content of the default_keys function in your custom layer code, and keep or change what you need. default_keys is located in 4coder_remapping_commands.cpp but it doesn't do much apart from calling fill_keys_default which is located in 4coder_generated\remapping.h.

Copy the fill_keys_default function (or fill_keys_mac_default on Mac) in your custom layer file, change the function name (custom_keys in this article) and modify the bindings to suit your needs. In the get_bindings function, change the call to default_keys (or mac_default_keys on Mac) to the name you chose.

Here is a simple example of custom_layer.cpp

#include "4coder_default_include.cpp"

void custom_keys(Bind_Helper *context){
    
    begin_map(context, mapid_global); {
        bind(context, 'p', MDFR_CTRL, open_panel_vsplit );
        ...
    } end_map(context);
    
    begin_map(context, mapid_file); {
        bind_vanilla_keys(context, write_character);
        ...
    } end_map(context);
    
    begin_map(context, default_code_map); {
        inherit_map(context, mapid_file);
        bind(context, key_right, MDFR_CTRL, seek_alphanumeric_or_camel_right);
        ...
    } end_map(context);

    begin_map(context, default_lister_ui_map); {
        bind_vanilla_keys(context, lister__write_character);
        bind(context, key_esc, MDFR_NONE, lister__quit);
        bind(context, '\n', MDFR_NONE, lister__activate);
        bind(context, '\t', MDFR_NONE, lister__activate);
        bind(context, key_back, MDFR_NONE, lister__backspace_text_field);
        bind(context, key_up, MDFR_NONE, lister__move_up);
        bind(context, key_page_up, MDFR_NONE, lister__move_up);
        bind(context, key_down, MDFR_NONE, lister__move_down);
        bind(context, key_page_down, MDFR_NONE, lister__move_down);
        bind(context, key_mouse_wheel, MDFR_NONE, lister__wheel_scroll);
        bind(context, key_mouse_left, MDFR_NONE, lister__mouse_press);
        bind(context, key_mouse_left_release, MDFR_NONE, lister__mouse_release);
        bind(context, key_mouse_move, MDFR_NONE, lister__repaint);
        bind(context, key_animate, MDFR_NONE, lister__repaint);
    } end_map(context);
}

extern "C" int32_t
get_bindings(void *data, int32_t size){
    Bind_Helper context_ = begin_bind_helper(data, size);
    Bind_Helper *context = &context_;
    
    set_all_default_hooks(context);
    
#if defined(__APPLE__) && defined(__MACH__)
    custom_keys_mac(context);
#else
    custom_keys(context);
#endif
    
    int32_t result = end_bind_helper(context);
    return(result);
}

Remark

If you are using the default_file_settings hook (hooks are explained in another section), you need to define mapid_file and default_code_map, even if they are empty, otherwise 4coder will crash on startup. See those Notes for 4coder customizers on build 4.0.29.

begin_map(context, mapid_file);
end_map(context);
begin_map(context, default_code_map);
inherit_map(context, mapid_file);
end_map(context);

Special binding function

bind_vanilla_keys allows to bind all character key (any key that adds a character in a file) to a function. For example:

  • bind_vanilla_keys( context, write_character ); will make each character key output it's character in the active buffer.
  • bind_vanilla_keys( context, hello ); will call the function hello each time a character key is pressed.

Key maps

Key maps allow you to have different sets of keys that will be active or inactive depending on the context. For instance different key bindings will be active if you are editing a code file or a plain text file. You can configure key maps as you want, but 4coder comes with some builtin.

  • mapid_nomap: an empty set;
  • mapid_global: bindings for operation not related to editing the content of a file. For example opening the file browser, closing a file, change the active panel;
  • mapid_file: bindings to edit the content of a file. For example typing characters, deleting characters, searching, moving around;
  • mapid_ui: I don't think this is actually used;
  • default_code_map: bindings that only apply to code editing. For example indentation, auto-completion, listing types;
  • default_lister_ui_map: bindings to navigate and type text in the lister UI (file browser, command list...).

You can use those key maps or define your own. But some feature of 4coder will depend on those key maps (see the remark in the previous section about default_file_settings.

Defining your map ids

You can define your own map ids. mapid_xxx are part of the Map_ID enum defined in 4coder_API\types.h. 4coder map ids start at (1 << 24). So your map ids should not overlap that. default_code_map, default_lister_ui_map and default_maps_count are defined in in 4coder_default_framework.h. You first id value should be at least equal to default_maps_count.

Creating a key map

As seen previously to select a key map to edit, you use the begin_map function. It's required to close a key map before starting to edit another one, by using the end_map function.

begin_map( context, mapid_global ); {
    bind( ... );
    bind( ... );
} end_map( context );

Inheriting a key map

When you open a key map, by default it will inherit the bindings from mapid_global, meaning all mapid_global bindings will be defined when you use the child key map, unless you overwrite them. If you want to inherit from another key map, you can use the inherit_map function. If you don't want to inherit anything, you need to inherit mapid_nomap.

begin_map( context, mapid_custom_map ); {
    inherit_map( context, mapid_nomap );
    bind( ... );
    bind( ... );
} end_map( context );

Using key maps

You can change the current key map used by a buffer with buffer_set_setting. Here is an example function that you can use to change the active key map.

void set_current_keymap( Application_Links* app, int map ) {
    
    unsigned int access = AccessAll;
    View_Summary view = get_active_view( app, access );
    Buffer_Summary buffer = get_buffer( app, view.buffer_id, access );
    
    if ( buffer.exists ) {
        buffer_set_setting( app, &buffer, BufferSetting_MapID, map );
    }
}

Custom function

To create a function that can be called with a key bind you need to create a function using the CUSTOM_COMMAND_SIG(name) macro defined in 4coder_API\types.h. The actual function signature is void name(struct Application_Links* app).

CUSTOM_COMMAND_SIG( hello ) {
    print_message( app, "Hello Dave.", sizeof( "Hello Dave." ) - 1 );
}
...
bind( context, 'h', MDFR_CTRL | MDFR_ALT, hello );

Hooks

4coder allows you to specify functions (hooks) that will be called when some events happen. get_bindings calls set_all_default_hooks, defined in 4coder_default_hooks.cpp, to setup the default behavior. You can replace those hooks with your functions. The function signatures for those hooks can be found in 4coder_API\types.h.

extern "C" int32_t
get_bindings(void *data, int32_t size){
    Bind_Helper context_ = begin_bind_helper(data, size);
    Bind_Helper *context = &context_;
    
    set_hook(context, hook_exit, default_exit);
    set_hook(context, hook_view_size_change, default_view_adjust);
    
    set_start_hook(context, default_start);
    set_open_file_hook(context, default_file_settings);
    set_new_file_hook(context, default_new_file);
    set_save_file_hook(context, default_file_save);
    
    set_end_file_hook(context, end_file_close_jump_list);
    
    set_command_caller(context, default_command_caller);
    set_render_caller(context, default_render_caller);
    set_input_filter(context, default_suppress_mouse_filter);
    set_scroll_rule(context, smooth_scroll_rule);
    set_buffer_name_resolver(context, default_buffer_name_resolution);
    
#if defined(__APPLE__) && defined(__MACH__)
    custom_keys_mac(context);
#else
    custom_keys(context);
#endif
    
    int32_t result = end_bind_helper(context);
    return(result);
}

Adding key words to the syntax highlighting

You can add key words that will be highlighted by 4coder, but only for C/Cpp files. Create a file (this needs to be in it's own file) and add one line per keyword in it, using the following syntax. Note that the type you set doesn't have any impact, all items will be the same color.

{ make_stafl( "u8", CPP_TOKEN_KEY_TYPE ) },
{ make_stafl( "u16", CPP_TOKEN_KEY_TYPE ) },
{ make_stafl( "u32", CPP_TOKEN_KEY_TYPE ) },
{ make_stafl( "u64", CPP_TOKEN_KEY_TYPE ) },

At the top of your custom layer file, BEFORE #include "4coder_default_include.cpp", define EXTRA_KEYWORDS to the filename where your keywords are.

#defines EXTRA_KEYWORDS "custom_keywords.h"
#include "4coder_default_include.cpp"

Modal customization layer

You can use the key map system to make a modal version of 4coder. Note that the key map changes are on the buffer, not at the application level. Here is a small example.

void set_current_keymap( Application_Links* app, int map ) {
    
    unsigned int access = AccessAll;
    View_Summary view = get_active_view( app, access );
    Buffer_Summary buffer = get_buffer( app, view.buffer_id, access );
    
    if ( buffer.exists ) {
        buffer_set_setting( app, &buffer, BufferSetting_MapID, map );
    }
}

enum modal_mapid {
    
    mapid_insert = default_maps_count,
    mapid_shared,
    mapid_delete,
};

CUSTOM_COMMAND_SIG( modal_to_global ) {
    
    set_current_keymap( app, mapid_global );
    
    Theme_Color colors[ ] = {
        { Stag_Cursor, 0xffff5533 },
        { Stag_At_Cursor, 0xff00aacc },
        { Stag_Margin_Active, 0xffff5533 },
    };
    
    set_theme_colors( app, colors, ArrayCount( colors ) );
}

CUSTOM_COMMAND_SIG( modal_enter_insert ) {
    
    set_current_keymap( app, mapid_insert );
    
    Theme_Color colors[ ] = {
        { Stag_Cursor, 0xff80ff80 },
        { Stag_At_Cursor, 0xff293134 },
        { Stag_Margin_Active, 0xff80ff80 },
    };
    
    set_theme_colors( app, colors, ArrayCount( colors ) );
}

CUSTOM_COMMAND_SIG( modal_enter_delete ) {
    
    set_current_keymap( app, mapid_delete );
    
    Theme_Color colors[ ] = {
        { Stag_Cursor, 0xffffff00 },
        { Stag_At_Cursor, 0xff0000ff },
    };
    
    set_theme_colors( app, colors, ArrayCount( colors ) );
}

CUSTOM_COMMAND_SIG( modal_delete_word_right ) {
    exec_command( app, delete_word );
    exec_command( app, modal_to_global );
}

CUSTOM_COMMAND_SIG( modal_delete_word_left ) {
    exec_command( app, backspace_word );
    exec_command( app, modal_to_global );
}

void custom_keys(Bind_Helper* context ) {
    
    begin_map( context, mapid_shared ); {
        
        inherit_map( context, mapid_nomap );
        
        bind( context, key_left, MDFR_NONE, move_left );
        bind( context, key_right, MDFR_NONE, move_right );
        bind( context, key_up, MDFR_NONE, move_up );
        bind( context, key_down, MDFR_NONE, move_down );
        
    } end_map( context );
    
    begin_map( context, mapid_global ); {
        
        inherit_map( context, mapid_shared );
        bind( context, '\t', MDFR_NONE, modal_enter_insert );
        bind( context, 'd', MDFR_NONE, modal_enter_delete );
        
    } end_map( context );
    
    begin_map( context, mapid_insert ); {
        
        inherit_map( context, mapid_shared );
        bind( context, key_esc, MDFR_NONE, modal_to_global );
        bind_vanilla_keys( context, write_character );
        
    } end_map( context );
    
    begin_map( context, mapid_delete ); {
        
        inherit_map( context, mapid_nomap );
        bind( context, key_esc, MDFR_NONE, modal_to_global );
        
        bind( context, key_right, MDFR_NONE, modal_delete_word_right );
        bind( context, key_left, MDFR_NONE, modal_delete_word_left );
        
    } end_map( context );
}

How do I do something ?

The best way to find out how to do something is to look in the code how similar things are done. Most of the functions you use are accessible in the source files (but he code for the core of 4coder isn't available). And remember that you can attach the debugger to 4coder to debug everything in the custom layer. The 4coder website contains the documentation for the API.

If you can't figure it out, you can ask for help in 4coder's handmade.network forum.


Edited by Simon Anciaux on Reason: Fixed my mistake