Display

RMK has built-in support for OLED and other small displays through the DisplayProcessor. It subscribes to keyboard state events and redraws the screen automatically whenever something changes.

Supported Drivers

DriverChip(s)Feature flag
SSD1306SSD1306ssd1306
oled-asyncSH1106, SH1107, SH1108, SSD1309oled_async

Supported Sizes

DriverSupported resolutions
SSD1306128x64, 128x32, 96x16, 72x40, 64x48
SH1106128x64
SH110764x128, 128x128
SH110864x160, 96x160, 128x160, 160x160
SSD1309128x64

All drivers support 0, 90, 180 and 270 degree rotation.

Built-in Renderers

RMK ships two renderers out of the box:

  • LogoRenderer — displays the RMK logo. Used by default when you don't specify a renderer.
  • OledRenderer — full keyboard status screen: layer, WPM, modifier indicators, Caps/Num Lock, battery level, BLE status, and split keyboard connection state. Layout adapts automatically between landscape and portrait orientations.

Configuration

For keyboard.toml users, see the Display Configuration reference for all available options.

Rust API

For use_rust keyboards, initialize the display manually and pass it to DisplayProcessor:

use rmk::display::DisplayProcessor;
use rmk::core_traits::Runnable;

// SSD1306 via ssd1306 crate
use ssd1306::{I2CDisplayInterface, Ssd1306Async, prelude::*};

let interface = I2CDisplayInterface::new(i2c);
let display = Ssd1306Async::new(interface, DisplaySize128x32, DisplayRotation::Rotate0)
    .into_buffered_graphics_mode();

// Default: LogoRenderer
let mut oled = DisplayProcessor::new(display);

// Or use the built-in OledRenderer
use rmk::display::OledRenderer;
let mut oled = DisplayProcessor::with_renderer(display, OledRenderer::default());

run_all!(matrix, oled).await;

SH1106 / oled-async

use oled_async::Builder;
use oled_async::displays::sh1106::Sh1106_128_64;
use oled_async::displayrotation::DisplayRotation;
use display_interface_i2c::I2CInterface;
use rmk::display::DisplayProcessor;

let interface = I2CInterface::new(i2c, 0x3C, 0x40);
let display = Builder::new(Sh1106_128_64 {})
    .with_rotation(DisplayRotation::Rotate0)
    .connect(interface)
    .into();

let mut oled = DisplayProcessor::new(display);

Render Intervals

use embassy_time::Duration;

// Enable animation polling (redraw every 33 ms even without events)
let mut oled = DisplayProcessor::new(display)
    .with_render_interval(Duration::from_millis(33));

// Override the minimum time between event-driven renders (default: 33 ms)
let mut oled = DisplayProcessor::new(display)
    .with_min_render_interval(Duration::from_millis(10));

Custom Renderers

Implement DisplayRenderer<C> for your color type (BinaryColor for monochrome OLEDs):

use core::fmt::Write as _;
use embedded_graphics::{
    mono_font::{ascii::FONT_6X10, MonoTextStyle},
    pixelcolor::BinaryColor,
    prelude::*,
    text::Text,
};
use rmk::display::{DisplayRenderer, RenderContext};

pub struct MyRenderer;

impl DisplayRenderer<BinaryColor> for MyRenderer {
    fn render<D: DrawTarget<Color = BinaryColor>>(&mut self, ctx: &RenderContext, display: &mut D) {
        display.clear(BinaryColor::Off).ok();

        let style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
        let mut line: heapless::String<32> = heapless::String::new();

        write!(&mut line, "Layer {}  WPM {}", ctx.layer, ctx.wpm).ok();
        Text::new(&line, Point::new(0, 10), style).draw(display).ok();
    }
}

Then pass it to DisplayProcessor::with_renderer:

let mut oled = DisplayProcessor::with_renderer(display, MyRenderer);

Or reference it in keyboard.toml (the crate must be a dependency of your keyboard crate):

[display]
renderer = "my_crate::MyRenderer"

RenderContext Fields

The ctx argument passed to render carries a snapshot of the current keyboard state:

FieldTypeDescription
layeru8Current active layer index
wpmu16Words-per-minute estimate
caps_lockboolCaps Lock indicator state
num_lockboolNum Lock indicator state
modifiersModifierCombinationActive modifier keys (Shift, Ctrl, Alt, GUI)
key_pressedboolWhether a key is currently held down
key_press_latchboolTrue if a key was pressed since the last render; cleared after each render
sleepingboolWhether the keyboard is in sleep mode
batteryBatteryStateEventBattery charge level and state

Feature-gated fields (require the corresponding RMK feature to be enabled):

FieldFeatureDescription
ble_status_bleBLE connection profile and state
central_connectedsplitWhether the central is connected (peripheral side)
peripherals_connectedsplitPer-peripheral connection state array
peripheral_batteriessplit + _blePer-peripheral battery state array
Tip

key_press_latch vs key_pressed Use key_press_latch when you want to react to a new key press — it stays true even if the key was released before the render ran. Use key_pressed to reflect the real-time held state (e.g. to display a held-key animation).

Custom Display Drivers

If your display chip is not natively supported, implement DisplayDriver for your display type:

use embedded_graphics::prelude::*;
use embedded_graphics::pixelcolor::BinaryColor;
use rmk::display::DisplayDriver;

struct MyDisplay { /* ... */ }

impl DrawTarget for MyDisplay {
    type Color = BinaryColor;
    type Error = core::convert::Infallible;

    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
    where
        I: IntoIterator<Item = Pixel<Self::Color>>,
    {
        // Write pixels to framebuffer
        Ok(())
    }
}

impl OriginDimensions for MyDisplay {
    fn size(&self) -> Size { Size::new(128, 32) }
}

impl DisplayDriver for MyDisplay {
    async fn init(&mut self) {
        // Initialize display hardware
    }

    async fn flush(&mut self) {
        // Flush framebuffer to display
    }
}