Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Portly

A macOS menu bar app that shows the local servers you are running and lets you kill any of them with one click.

If you run a lot of development servers, it is easy to lose track of what is on :3000 versus :8080, and freeing a stuck port usually means running lsof -i :3000 and kill <pid> by hand. Portly puts that in your menu bar: a glanceable list of your local servers, each with a description and a kill button.

Portly listing local servers in the macOS menu bar

Highlights

  • Your servers, not the system's. By default Portly hides macOS background services (Control Center / AirPlay, rapportd, ARDAgent, and similar) and ephemeral internal sockets, so the list is only the things you started.
  • Readable descriptions. Each port is labelled from your own custom name, a built-in table of well-known ports (PostgreSQL, Redis, Vite, and more), or the owning process name, in that order.
  • One-click kill. Click to terminate gracefully (SIGTERM); hold Option to force-kill (SIGKILL). No confirmation dialog, no terminal.
  • Custom labels. Right-click a row to label a port (for example, name :3000 "Frontend"). Labels persist across launches.
  • Exposure indicator. Ports bound to all interfaces (reachable from your network, not just loopback) are flagged.
  • Lightweight and native. A single SwiftUI MenuBarExtra, no Dock icon, no runtime dependencies. The built app is well under one megabyte.

How it works

Portly enumerates listening TCP sockets with lsof and groups them by port:

lsof -nP -iTCP -sTCP:LISTEN +c0 -FpcnL

The -F field mode gives machine-readable output that is parsed into one entry per port (IPv4 and IPv6 listeners for the same port are merged).

Filtering "your servers" from system noise

On macOS every listener typically runs under your user account, including Apple's own daemons, so ownership alone cannot separate them. Portly uses two signals instead:

  1. Port range. Listeners in the IANA dynamic/ephemeral range (>= 49152) are hidden. These are short-lived internal IPC sockets, not servers you open in a browser.
  2. Process denylist. A curated list of Apple service processes (ControlCenter, rapportd, ARDAgent, sharingd, and others) is hidden.

A "Show system and background services" toggle in the options menu reveals the full list when you want it.

Killing a process

Kill is a direct kill(2) system call, not a shell-out. The result is mapped to a clear outcome, including EPERM (a root-owned port shows "Needs admin" rather than failing silently) and ESRCH (already gone).

Requirements

  • macOS 14 (Sonoma) or later
  • Swift 6 toolchain (Xcode 16+ or the Swift toolchain command line tools)

Portly is intentionally not sandboxed, because the App Sandbox blocks both spawning lsof and signalling other processes.

Build and run

git clone https://github.com/zabrodsk/portly.git
cd portly
./build.sh          # release build, assembles and ad-hoc signs Portly.app
open Portly.app

Use ./build.sh --debug for a faster debug build while iterating.

Tests

swift test

The suite covers the lsof parser (IPv4/IPv6 grouping, address splitting), the "local host" filter, description resolution priority, and the real kill(2) paths, including the permission-denied and no-such-process cases.

Project structure

Package.swift                  SwiftPM manifest (executable + test target)
build.sh                       Build, assemble, and ad-hoc sign Portly.app
Resources/Info.plist           LSUIElement (menu-bar-only) bundle metadata
Sources/Portly/
  PortlyApp.swift              App entry point, MenuBarExtra scene
  AppModel.swift               Observable state, refresh timer, kill actions
  PortScanner.swift            lsof invocation, parsing, and filtering
  ProcessKiller.swift          kill(2) wrapper with errno mapping
  PortDescriptions.swift       Well-known-port table and title resolution
  LabelStore.swift             Custom labels persisted in UserDefaults
  Models.swift                 PortEntry value type
  Views/
    PortsListView.swift        Popover: header, list, options, footer
    PortRowView.swift          A single port row and its kill button
Tests/PortlyTests/             XCTest suite

Distribution

build.sh ad-hoc signs the app so it runs on the machine that built it. To distribute a signed and notarized build, package Portly.app into a DMG and run it through Apple's notarization with your Developer ID.

License

MIT

About

macOS menu bar app that lists your local dev servers and kills them with one click

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages