dropterm

View on GitHub

A Yakuake-style dropdown terminal for Wayland: press a key, a real terminal rolls down from the top of the screen, press it again and it goes away.

screenshot

It began as a panel plugin for the noctalia desktop shell. noctalia v5 was rewritten from QML to native C++ with a sandboxed Luau plugin API that cannot host a terminal — no PTY, no canvas to draw a cell grid on, and no raw keyboard input (the details). Rather than lose the terminal, it took ownership of its own window.

So it is now a standalone application that depends on no shell at all: it draws on its own wlr-layer-shell surface, the same mechanism bars and docks use. It needs a compositor supporting layer-shell (Hyprland, sway, river, niri…) and a key bound to dropterm toggle. Nothing else.

Using the old noctalia v4 plugin? It is still there and still works: the v1.1.0 tag, or the noctalia-v4 branch for fixes. Everything from v2.0.0 on is the standalone application described here.

Features

  • Tabbed terminal sessions, persisting across open/close
  • Interactive scrollbar with drag and click-to-jump
  • Built on libvterm-neovim for complete terminal emulation
  • Drops down flush beneath a bar without hardcoding the bar's height
  • Click outside to dismiss; no shell or compositor plugin required

How it works

The terminal itself is Qt Quick over libvterm: PtyIFace runs the shell on a pty, VTermBridge turns its output into a screen model, and TextRender paints the cell grid. That part was carried over intact from the plugin — what changed is who owns the window. Instead of being drawn into a host shell's panel slot, dropterm creates its own layer-shell surface via LayerShellQt.

Two details of that surface are load-bearing, and both are easy to get wrong:

  • The surface spans the whole usable output, not just the terminal. A surface sized to the terminal alone is never told about clicks that land elsewhere, so outside-click dismissal would be impossible. The area around the terminal is transparent and simply catches those clicks. Anchoring all four edges with an exclusive zone of 0 makes the compositor size the surface to the output minus everyone else's exclusive zones, which is why the terminal sits flush under a bar with no configured offset.
  • Keyboard interactivity is exclusive. Under focus-follows-mouse, an on-demand surface loses the keyboard the moment the pointer drifts off it, so typing would silently go elsewhere. Exclusive keeps input here while the terminal is mapped. Compositor keybinds still take priority, so the toggle works.

The consequence is that the dropdown is modal: while it is open, clicks elsewhere dismiss it rather than interacting, and typing goes to the terminal. That is the trade-off for outside-click dismissal, and it matches how shell panels normally behave.

Installation

Dependencies: Qt 6 (qtbase, qtdeclarative, qtwayland), LayerShellQt, libvterm-neovim, CMake, pkg-config.

NixOS (flake + home-manager)

The repository is still named noctalia-dropdown-terminal for the benefit of anyone already depending on it; the binary and the module are dropterm.

inputs.dropterm.url = "github:ajunca/noctalia-dropdown-terminal";
imports = [ inputs.dropterm.homeManagerModules.default ];

programs.dropterm = {
  enable = true;
  settings = {
    widthPercent = 0.6;
    heightPercent = 0.3;
    fontFamily = "Hack";
    fontSize = 10.5;
  };
};

No QML import path wiring is needed any more — the QML is compiled into the binary.

Manual (non-Nix)

cmake -S src -B build -DCMAKE_INSTALL_PREFIX=/usr/local
cmake --build build
cmake --install build

Usage

dropterm is a single binary. The first invocation starts it; later invocations are remote controls over a per-user socket in $XDG_RUNTIME_DIR.

dropterm toggle   # default
dropterm show
dropterm hide
dropterm reload   # re-read configuration, including theme.conf
dropterm settings # open the settings window

Bind the toggle in your compositor. Hyprland:

bind = , F12, exec, dropterm toggle

Configuration

Settings live in $XDG_CONFIG_HOME/dropterm/dropterm.conf (INI, read with QSettings, so keys go under [General]). The Nix module writes this for you.

KeyDefaultMeaning
widthPercent0.6Terminal width as a fraction of the usable area (0.2–1.0)
heightPercent0.3Terminal height as a fraction of the usable area (0.15–1.0)
fontFamilyHackTerminal font
fontSize10.5Point size
shellProgram(empty)Shell to run; empty uses the shell from passwd
foreground#ebebebDefault text colour
background#000000Terminal background
backgroundOpacity0.92Background alpha (0.0–1.0)
cornerRadius8Radius of the two free (bottom) corners
animationMs180Roll-down duration; 0 for instant

Two bits of polish are compositor rules rather than application settings, because an application can neither blur what is behind it nor decide how the compositor animates its surface appearing. Hyprland:

layerrule = blur, dropterm     # blur behind the terminal
layerrule = noanim, dropterm   # drop the compositor's fade on open/close

The second is worth explaining: dropterm animates the roll-down itself, inside its own surface. A compositor that also fades the surface in on map layers one animation over the other. Suppressing it leaves just the movement. Both rules are namespace-matched, so they affect nothing else.

Where a setting comes from

Each key is resolved from the first of these that provides it:

filewritten by
1~/.config/dropterm/dropterm.confdropterm itself, when you change something
2~/.config/dropterm/theme.confa desktop theme engine (optional)
3~/.config/dropterm/defaults.confwhatever installed dropterm (optional)
4built-in defaults

Only the first is ever written by dropterm. The settings window shows which layer each colour came from, and clicking that label drops your override so the key follows the layers below it again.

Following a desktop colour scheme

dropterm knows nothing about any particular desktop. The contract is two things, and anything that can do them will work — matugen, pywal, a theme engine, or a shell script in a wallpaper hook:

  1. Write ~/.config/dropterm/theme.conf:

    [General]
    foreground=#e8e1db
    background=#15130f
    
  2. Run dropterm reload.

reload only ever talks to a running terminal; if none is running it does nothing rather than starting one, so it is safe from a hook that fires often.

A template for noctalia ships at share/dropterm/templates/dropterm.conf. Point its template engine at it:

[theme.templates.user.dropterm]
input_path  = ".../share/dropterm/templates/dropterm.conf"
output_path = "~/.config/dropterm/theme.conf"
post_hook   = "dropterm reload"

The template uses {{colors.terminal_foreground.default.hex}}-style variables. Other engines use different variable names, so adapt the two lines; the output format is what matters, and it is only an INI file with two keys.

Because your own choices sit above the theme layer, picking a colour in the settings window stops that key following the scheme, and reverting it resumes.

Keyboard shortcuts

ShortcutAction
Ctrl+Shift+CCopy
Ctrl+Shift+VPaste
Shift+InsertPaste
Ctrl+Shift+T / NNew tab
Ctrl+Shift+WClose tab
Ctrl+TabNext tab
Ctrl+Shift+TabPrevious tab

Why it left noctalia

noctalia v5 is a ground-up rewrite from QML/Quickshell to native C++ with a sandboxed Luau plugin API. A terminal cannot be built on it:

  1. No PTY. Nothing in the codebase opens one; plugins get line-streamed process output only. That rules out job control, interactive programs, terminal resize and signal delivery.
  2. No canvas. Plugin UI is fixed declarative primitives, with no custom-drawing surface for a cell grid.
  3. No raw keyboard. capture_keys delivers only key chords declared up front in the manifest, and cannot conditionally consume them. A terminal needs every keystroke.
  4. No native code loading. Plugins are Luau only, so the existing C++ terminal could not be loaded even if the above were solved.

Embedding an external terminal in a panel is not a way out either: Wayland has no cross-client surface embedding.

Standalone sidesteps all of it, and the terminal no longer depends on any particular shell.

License

MIT