npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

statusline

v0.2.0

Published

Universal status line for i3bar, swaybar, waybar, polybar, lemonbar, tmux and the terminal

Readme

statusline

Universal status line in Node.js. One config, one set of blocks, and whichever bar you happen to be running today.

Write a block once and it works in i3bar, swaybar, waybar, polybar, lemonbar, tmux, or straight in your terminal. Moving between window managers does not mean rewriting your bar.

Screenshot

Installation

npm install -g statusline

Then try it without configuring anything:

statusline cli

That prints one line and exits. On the first run it writes a default config to ~/.statusline.conf for you to edit.

Output commands

Each command renders the same blocks for a different consumer.

| Command | For | Behaviour | | --- | --- | --- | | statusline cli | scripts, prompts, a quick look | prints one line and exits, or --watch to keep going | | statusline tui | your terminal | redraws in place, and blocks are clickable | | statusline i3status | i3bar, swaybar | i3bar protocol with click events | | statusline waybar | waybar | JSON lines for a custom module | | statusline tmux | tmux | one line with tmux colour directives | | statusline lemonbar | lemonbar, polybar | one line with %{F} colour directives | | statusline click | bars without click events | sends a click to a named block |

Global flags: --config <name> picks a named config, and every command takes --interval <ms>. cli takes --width <n> and --plain; tmux and waybar take --region <left|center|right>, and waybar also takes --blocks <a,b,c>. A command given a region or a block list never registers the other blocks at all, so a module costs only what it draws.

i3 and sway

In ~/.config/i3/config or ~/.config/sway/config:

bar {
    status_command statusline i3status
}

waybar

In ~/.config/waybar/config:

"custom/statusline": {
    "exec": "statusline waybar --region right",
    "return-type": "json"
}

Then add "custom/statusline" to modules-right. waybar puts whole modules into its own left, center and right lists, so to fill all three, declare the module three times with a different --region each.

tmux

In ~/.tmux.conf:

set -g status-left "#(statusline tmux --region left)"
set -g status-right "#(statusline tmux --region right)"

tmux has a separate option per side rather than one line, which is what --region is for.

lemonbar and polybar

statusline lemonbar | lemonbar

Terminal

statusline tui

Redraws in place and accepts mouse clicks, using the same click handling the bar uses. q or ctrl-c quits. This is the quickest way to see what a config looks like, and to check that a block you are writing handles its clicks.

Configuration

Config lives in ~/.statusline.conf and is written on first run.

{
  "blocks": [
    {
      "name": "cpu",
      "color": "#1a1b26",
      "backgroundColor": "#ff9e64",
      "customOptions": {
        "absolute": true
      }
    }
  ],
  "middleware": [
    {
      "middleware": "powerlineSeparator",
      "options": {
        "trailing": false
      }
    }
  ]
}

| Key | Meaning | | --- | --- | | blocks[].name | block to render, built in or installed | | blocks[].color | text colour, #rgb or #rrggbb | | blocks[].backgroundColor | background colour | | blocks[].region | left, center or right, default left | | blocks[].interval | how often to re-render, in milliseconds | | blocks[].customOptions | options for that block, listed below | | middleware[].middleware | middleware to run over the rendered output | | middleware[].options | options for that middleware |

The config is validated on load. A typo is reported by name and position:

✖ Config at /home/you/.statusline.conf is not valid:
✖   blocks[0].colour is not a known option, expected one of: name, color, backgroundColor, customOptions
✖   blocks[1].color must be a hex colour like "#ffffff", got "red"
✖ Falling back to the default config.

A broken config never takes the bar down with it. The problem is reported and the default config is used, so there is still a status line to read the error on.

Refreshing

Blocks are not all polled on the same clock.

workspaces and window subscribe to the compositor and redraw the moment something happens: Hyprland's event socket, or an i3 IPC subscription under Sway and i3. A workspace switch shows up as it happens rather than up to a second later.

Every other block has its own interval, in milliseconds, and is served from its last value in between. This matters more than it sounds: asking pipewire for the volume and the GPU for its usage costs more than every other block put together, and neither changed because a window got focus. A redraw triggered by a workspace switch costs about 3ms rather than 35ms.

{"name": "gpu", "interval": 5000}

Watched blocks default to a 10 second interval as a safety net, everything else to one second. A block can also declare cacheable: false and never be served from the cache. Clicking a block throws its cached value away, since a click usually changes the very thing it reports.

The config file is watched too. Save it and the bar rebuilds itself; there is no need to restart the bar to try a change.

Regions

A block can name a region: left, center or right. Blocks that name none land on the left, so a config written before regions existed renders as it did.

{"name": "workspaces", "region": "left"},
{"name": "window",     "region": "center"},
{"name": "date",       "region": "right"}

How much a region means depends on where the output goes:

| Output | Regions | | --- | --- | | cli, tui | spread across the terminal width; the centre gives way when a side grows into it | | lemonbar | native %{l}, %{c}, %{r} alignment | | tmux, waybar | pick one region per invocation with --region | | i3status | not supported. Emitted in order, but not aligned |

The i3bar protocol has no notion of regions. Its status area is a single strip against the right of the bar, which is why i3 and sway draw workspaces themselves on the left. Blocks are still grouped by region so the order is predictable, but nothing is aligned.

powerlineSeparator decorates each region separately, so an arrow never bridges the gap between two sides of the bar.

Clicks, and what each bar can do with them

The i3bar protocol reports a click with the position inside the block, which is what lets workspaces tell which number was clicked. The terminal does the same. waybar does not: a custom module is one widget, and its on-click runs a command with no pointer position at all.

So under waybar:

  • give each clickable block its own module with --blocks <name>, and point its hooks at statusline click <name> <button>. A module wide hook cannot tell the clock from the volume, and clicking the clock would act on whichever block the hook named
  • for workspaces, use waybar's own hyprland/workspaces or sway/workspaces. It draws real buttons, so clicking, scrolling and its CSS states work
"custom/statusline-volume": {
    "exec": "statusline waybar --blocks volume",
    "return-type": "json",
    "escape": false,
    "on-click": "statusline click volume 1",
    "on-scroll-up": "statusline click volume 4",
    "on-scroll-down": "statusline click volume 5"
}

Each module is a process, so this trades memory for clickability. Under i3bar or swaybar one process does everything and every click works, with no hooks.

State

Some blocks remember something between renders: the clock remembers whether it is showing the time or the date. That cannot live in a variable, because a bar without a click protocol runs statusline click as a separate process which sets the value and exits, while the bar is a different process that has to notice. It goes in ~/.local/state/statusline/state.json, which is watched, so a toggle from anywhere redraws the bar immediately.

Blocks read and write it through src/services/state. Keep it to small things; it is state, not a cache and not a config.

Named configs

The default config is ~/.statusline.conf. Named ones live in ~/.config/statusline/<name>.conf:

statusline --config laptop i3status
statusline --config work tui
statusline configs                    # list them
STATUSLINE_CONFIG=laptop statusline cli

--config also takes a path, if it looks like one. A named config that does not exist yet is created with the defaults on first use, so statusline --config laptop cli is all it takes to start one.

Blocks

All of these are built in. Anything else is installed from npm.

| Block | Shows | Clickable | | --- | --- | --- | | workspaces | workspaces, focused one in colour | click one to go to it, scroll to cycle | | window | focused window title | | | media | what is playing, via playerctl | play/pause, next, previous | | volume | output volume | scroll to change, click to mute | | brightness | screen backlight | scroll to change | | network | wireless network and signal, or wired interface | | | ip | address of the interface reaching the network | | | temperature | CPU temperature, coloured by how hot it is | | | cpu | CPU usage | | | gpu | GPU usage, VRAM and temperature | | | memory | memory in use | | | load | load average | | | battery | charge, with an icon that tracks the level | | | clock | the time, or the date when clicked | click to swap time and date | | date | the date, spelled out | | | powerline | a single separator glyph | |

workspaces and window work under Hyprland, Sway and i3; the compositor is detected at runtime. volume works with pipewire, pulseaudio or bare alsa, whichever answers first. brightness reads sysfs, and writes through brightnessctl, light or xbacklight. gpu reads NVIDIA cards through nvidia-smi and AMD cards through sysfs; Intel cards expose no usage counter without elevated privileges and are not supported.

Blocks that have nothing to say render nothing at all. A desktop with no battery and no backlight simply does not draw those blocks, so the same config works on a desktop and a laptop.

Block options

Set these under customOptions.

| Block | Option | Meaning | | --- | --- | --- | | workspaces | color, background | colours of the focused workspace | | workspaces | padding | spaces each side of a workspace name, default 1 | | window | maxLength | trim the title, default 60 | | media | player | restrict to one player, e.g. spotify | | media | maxLength | trim the text, default 40 | | volume | control | alsa mixer control, default Master | | volume | step | percentage points per scroll, default 5 | | volume | headphone | use the headphone icon | | volume | onRightClick | command to run on right click, e.g. pavucontrol | | brightness | device | backlight device, autodetected otherwise | | brightness | step | percentage points per scroll, default 5 | | network | interface | pin to an interface instead of following the default route | | network | address | append the IPv4 address | | ip | interface | pin to an interface | | ip | all | list every physical interface | | temperature | path | hwmon directory, autodetected otherwise | | temperature | input | input file, default temp1_input | | temperature | critical | threshold for the hottest icon and colour, default 80 | | temperature | warning | threshold at which the colour changes, default 60 | | temperature | colors | {normal, warning, critical} hex colours, or false for none | | gpu | card | card index for nvidia-smi, or a name like card0 for AMD | | gpu | memory | append VRAM in use | | gpu | temperature | append the card temperature | | memory | absolute | show 12.4G / 62.0G instead of a percentage | | load | all | show all three averages | | load | perCore | divide by core count, so 1.00 means fully loaded | | battery | battery | battery name, autodetected otherwise | | battery | remaining | append estimated time to empty or full | | clock | format, dateFormat | Intl.DateTimeFormat options for each mode | | clock | calendar | set to false to leave the hover calendar off | | clock | icon | set to false to show the time on its own | | media | showPlayer | prefix the text with the player's name | | date | locale | BCP 47 locale tag | | date | format | Intl.DateTimeFormat options |

Icons

Icons are Private Use Area glyphs, so a patched font is needed to see them: any Nerd Font will do. They all live in src/utils/icons/index.js as code points, which is the one place to change to restyle the whole bar.

Middleware

Middleware receives the fully rendered output and returns a new one, so it can decorate, reorder or drop blocks after the fact.

powerlineSeparator is built in. It inserts an arrow between every pair of blocks, drawn in the background colour of the block on its left over the background colour of the block on its right, which is what makes the colours appear to flow into one another.

"middleware": [
  {
    "middleware": "powerlineSeparator",
    "options": {
      "trailing": false
    }
  }
]

Installing more blocks

statusline install block-time
statusline uninstall block-time

Leave off the statusline- prefix; it is added for you. Packages are installed into ~/.statusline_packages, not globally.

Example block: https://github.com/statusline/statusline-block-time

Writing a block

A block is a module exporting render, and optionally onClick.

module.exports = {
	render: function(block, status){
		return Promise.resolve({
			text: " hello "
		});
	},
	onClick: function(click, block, status){
		return Promise.resolve();
	}
};

render resolves with {text}, and may add markup: "pango" to say the text carries markup. Escape anything that came from outside before putting it in markup: one ampersand in a window title is enough to make a bar render the whole line as nothing. src/utils/markup has escape, strip and length; widths must be measured with length, never on the raw string. Resolve with an empty string to draw nothing at all. block is the entry from the config, so block.customOptions is where your options arrive.

A block can also export watch(block, status) to drive its own redraws instead of waiting to be polled. Call status.update(block) when something changes, and return a function that stops watching. Updates are coalesced, so a burst of events costs one render.

render may also resolve with a tooltip, as pango markup, which bars that draw tooltips will show on hover. The clock uses it for its calendar.

onClick receives the click event. click.button follows the i3bar numbering: 1 left, 2 middle, 3 right, 4 scroll up, 5 scroll down. The status line is re-rendered once the returned promise settles.

click.relative_x and click.width say where inside the block the click landed, which is how workspaces tells which of the numbers it drew was clicked. i3bar measures both in pixels and the terminal measures both in columns, so convert to a fraction before using them.

A block that throws is rendered as empty and the failure goes to ~/.statusline.log; one misbehaving block cannot take the bar down.

Writing middleware

module.exports = {
	apply: function(output, options, status){
		return output;
	}
};

output is the array of rendered blocks in i3bar protocol shape. Return a new array rather than mutating the one you were given.

Package naming

Published packages must be named statusline-[block/middleware/command]-[name], for example statusline-block-time. That is how the installer and the loader find them.

Development

yarn install
yarn lint
node index.js tui

Credits

nemanjan00

MIT