Klavgen handwired keyboard generator

Klavgen is a handwired keyboard generator written in Python, on top of CadQuery. It produces keyboards (like on the left) and makes handwiring easy and organized via switch holders (on the right):

Klavyl Switch holders after soldering

Note: Klavgen is heavily work-in-progress and lots of stuff mey not be working. If you experience issues, please submit an Github issue.

Benefits of building a keyboard with Klavgen:

  • Generates all the shells and support structures you need for a keyboard, in a way that’s optimized for home FDM printing with no supports
  • Is low height: only 11 mm in the default configuration)
  • Uses Kailh hotswap sockets
  • Makes it easy to solder things, and requires no glue whatsoever
  • Uses uninsulated wires, thus no tedious wire stripping
  • Makes it easy to reposition the keys after creation
  • Produces a sturdy build that doesn’t flex when typed on

The last 5 benefits are achieved by using switch holders which organize the wires, hold the Kailh sockets and the diodes, support the switches, enable soldering everything in place, and allow moving keys later (they even have wire slack so you can space keys further apart).

As of now:

  • Code is still quite messy and not fully parameterized
  • Configuring a keyboard requires code, it’s not as easy as YAML
  • Docs are scarse (as you’ll see below)

Example generated keyboards


Klavyl is my personal keyboard, a modification to Redox. It uses most of Klavgen’s capabilities, including screw holes, manually defined case outlines, Pro Micro and TRRS jack cutouts and holders, and palm rests. Check out the repo for the full config.



The Redox keyboard, as generated by Klavgen based on its Keyboard Layout Editor config:



The Ergodox keyboard, as generated by Klavgen based on its Keyboard Layout Editor config:



The Atreus keyboard, as generated by Klavgen based on its Keyboard Layout Editor config:



1. Install CadQuery

Install Miniconda and create an environment for CadQuery, following the official instructions.

2. Get comfortable using CadQuery

Get familiar with either of CadQuery’s UIs: CQ-editor or jupyter-cadquery (which I prefer).

Ensure that you know how to view objects returned from code, using show_object() in CQ-editor or show() from jupyter_cadquery.cadquery.

3. Download Klavgen

Klavgen is not yet packaged, so you need to download and use it locally within a project (clone or download and extract the ZIP file; example assumes cloning).

git clone [email protected]:klavgen/klavgen.git

Start your project into the klavgen repo directory. This is where you’ll have to write your scripts for now, since you need to import the klavgen package (whic is in a sub-dir):


CadQuery is very finicky when it comes to filleting (the fillet() command), and sometimes when it comes to shelling (the shell() command). Klavgen tries to provide helpful messages when errors around these are provided, please follow them. In general, it is highly recommended to always use a Patch object to define the outline of your case without small features.

If rendering fails in a shell, you should try render_case() with debug=True, which skips the shell step.

The 2 most important fillet settings, CaseConfig.side_fillet and CaseConfig.palm_rests_top_fillet are disabled by default, which makes default keyboards ugly.

These are the main gotchas:

  • Shelling fails because your case is not continuous (e.g. the thumb cluster doesn’t connect to the main body), has very sharp corners, or has small features.
  • Filleting fails because the object has features smaller than the fillet size.
  • Fillering succeeds but produces an invalid shape that later on causes erros when operated on (e.g. via a .cut()).


Note that all the show() commands below are meant to illustrate the use in Jupyter Lab. If you use CQ-editor, the commands may differ.

1. Generate a 1-key keyboard case

Run the following code to generate a single-key keyboard:

from klavgen import *

keys = [
    Key(x=0, y=0, keycap_width=18, keycap_depth=18),

case_result = render_case(keys=keys)

The render_case() method is the method you’ll use most often while designing the keybaord. It returns a fairly complex RenderCaseResult object which tracks the full progress of the keyboard construction to help you in fixing issues.

Note: You can pass in a RenderCaseResult object as the result parameter. Then, render_case() will save intermediate objects to the passed-in variable while rendering is taking place, allowing you to observe what happend if rendering raises an exception.

Here, we’ll only focus on these 3 keys part in the result object:

  • case_result.top is the top plate (show(case_result.top))

  • case_result.bottom is the case bottom (show(case_result.bottom))

  • case_result.debug shows key and holder outlines, helping you debug designs. This part should not be saved or used.

Rendering all 3 shows you the bottom, top and the debug outline in the air ((show(case_result.top, case_result.bottom, case_result.debug)):

2. Save the 1-key keyboard case

You can now export the dummy “keyboard” you created, via the render_and_save_keyboard() method, which takes the same parameters as render_case().

keyboard_result = render_and_save_keyboard(keys=keys)

Now you should see a series of .stl files in your working directly. The files depend on the features your keyboard uses. For now you will only see the top plate, the case bottom, and the switch holder. For a complete keyboard, you may also see the controller holder, the TRRS jack holder, palm rest(s), and the palm rests connector.

The render_and_save_keyboard() method renders the case (and takes the same parameters as render_case()) and then saves all relevant components to .stl files. It returns an object of type RenderKeyboardResult. In the current example, that object contains only the .case_result, .top, .bottom, and .switch_holder items. See below for the full list of members.

3. Generate a more complex case

This is a more complex “keyboard”, with screw holes and a palm rest:

config = Config()

keys = [
    Key(x=0, y=0, keycap_width=18, keycap_depth=18)

screw_holes = [
    ScrewHole(x=13, y=13),
    ScrewHole(x=13, y=-13),
    ScrewHole(x=-13, y=-13),

palm_rests = [
            (13, -5),
            (13, -30),
            (-13, -30),
            (-13, -5)
        height=config.case_config.case_base_height + 2,
        connector_locations_x = [0]

case_result = render_case(keys=keys, palm_rests=palm_rests, screw_holes=screw_holes, config=config)

This time, besides the case_result.top and case_result.bottom objects, you can also check out the case_result.palm_rests[0] one to see the palm rest.

This is the result (show(result.top, result.bottom, result.palm_rests[0])):

Note that the palm rest is detachable. You can make it part of the bottom by changing the config to:

config = Config(case_config=CaseConfig(detachable_palm_rests=False))

Now the case_result.palm_rests object is None so don’t try to view it. Instead just check out the case bottom (show(result.bottom)):

4. Generate a keyboard with all of Klavgen’s features

Check out the example.py file, which builds a dummy keyboard using all of Klavgen’s features.

This is the result (show(keyboard_result.top, keyboard_result.bottom, keyboard_result.palm_rests[0])):


Use a Keyboard Layout Editor export

You can build a keyboard from a Keyboard Layout Editor layout. Do the followingf;

  1. Ensure the laytout is contiguous. If it’s a split keyboard, manually remove all the keys from one of the splits before exporting.

  2. Go to the “Raw Data” tab and click “Download JSON” on the bottom right.

  3. Run code like the following to generate the keyboard:

    case_result = generate_from_kle_json("<path to downloaded json file>")

The generate_from_kle_json() method also takes in a config, debug, and result parameters like render_case(). It returns a standard RenderCaseResult object.

To check the key positions this generated, look at case_result.keys.

Important constructs

Important configs

  • CaseConfig.detachable_palm_rests allows you to set whether palm rests should be detachable, or part of the case bottom.
  • CaseConfig.side_fillet sets a fillet on the case vertical edges. You should likely always use it in the final product, but it’s very prone to failure, see warning above.
  • CaseConfig.palm_rests_top_fillet sets a fillet on the palm rest top edges. You should likely always use it the final product if you have palm rests (so they don’t dig in your hands), but it’s very prone to failure, see warning above.
  • ScrewHoleConfig.screw_insert_hole_width sets the size of the hole for screws and defaults to a value suitable for melting inserts. If you don’t use inserts, you should lower the value.
  • KeyConfig.case_tile_margin, ControllerConfig.case_tile_margin and TrrsJackConfig.case_tile_margin is the size of the case generated around keys, controller holders, and TRRS jack holders. If you use a Patch to define your outlines, you can freely lower these.

The render_standard_components parameter

When render_standard_components is set to True in a call to render_case, all holders (switch, controller, TRRS jack) and palm rest connectors are returned in the resulting .standard_components object. These help you see what the final keyboard will look like.

This is what example.py looks like with standard components added (show(keyboard_result.top, keybaord_result.bottom, keyboard_result.palm_rests[0], keyboard_result.case_result.standard_components)):

The RenderCaseResult object returned from render_case()

This object contains the individual steps when constructing the case. They key outputs are 3:

  • case_result.top is the top plate
  • case_result.bottom is the case bottom
  • case_result.palm_rests is a list of palm rest objects
  • case_result.standard_components is a rendering of all holders (switch, controller, TRRS jack) and palm rest connectors (see above).

Additionally there are many intermediate objects are useful for troubleshooting and auditing when something is not going right.

The RenderKeyboardResult object returned from render_and_save_keyboard()

This object contains the final keyboard components:

  • keyboard_result.case_result is the RenderCaseResult returned from the inner render_case() method. Always present.
  • keyboard_result.top is the top plate (same as case_results.top). Always present.
  • keyboard_result.bottom is the case bottom (same as case_results.bottom). Always present.
  • keyboard_result.switch_holder is the switch holder. Always present.
  • keyboard_result.connector is the palm rests to case bottom connector. Present only if palm rests are defined and CaseConfig.detachable_palm_rests is True.
  • keyboard_result.controller_holder is the controller holder. Present only if a controller is defined.
  • keyboard_result.trrs_jack_holder is the TRRS jack holder. Present only if a TRRS jack is defined.
  • keyboard_result.palm_rests is a list of palm rest objects (same as case_results.palm_rests). Present only if at least one palm rest is defined.


View Github