Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ adheres to [Semantic Versioning](https://semver.org) and the
multiple named notes, autosave (debounced while typing, flushed when the panel
closes and at quit, one atomically written file per note), a live
word/character/line count, copy-all, and an undoable clear.
- **Network Info**: a new menu bar tool showing the active interface and its
type, local IPv4, subnet mask, router, IPv6, and the configured DNS servers,
with every value click‑to‑copy. Local details are read from `getifaddrs` and
SystemConfiguration and never leave the Mac; the view refreshes itself when
the network changes rather than only at launch. The public‑IP lookup is a
separate, explicitly user‑initiated button that names the service it queries
(`api.ipify.org`) and can optionally be set to run automatically — it is
never fetched silently at launch, because that would disclose your address to
a third party every time the tool opens. No speed test: a throughput test
means sustained multi‑megabyte transfers from someone else's server, which is
its own decision about data usage and which server to trust, and does not
belong in a read‑only local‑info tool.

## [0.13.0] — 2026-07-12

Expand Down
11 changes: 11 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ let package = Package(
.executable(
name: "DMonteScratchpad",
targets: ["DMonteScratchpad"]
),
.executable(
name: "DMonteNetworkInfo",
targets: ["DMonteNetworkInfo"]
)
],
dependencies: [
Expand Down Expand Up @@ -297,6 +301,13 @@ let package = Package(
],
path: "Sources/DMonteScratchpadApp"
),
.executableTarget(
name: "DMonteNetworkInfo",
dependencies: [
"DMonteCore"
],
path: "Sources/DMonteNetworkInfoApp"
),
.testTarget(
name: "DMonteCoreTests",
dependencies: ["DMonteCore"],
Expand Down
32 changes: 32 additions & 0 deletions Packaging/NetworkInfoInfo.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>DMonteNetworkInfo</string>
<key>CFBundleIdentifier</key>
<string>com.havokentity.mactools.networkinfo</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDisplayName</key>
<string>DMonte Network Info</string>
<key>CFBundleName</key>
<string>DMonte Network Info</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.13.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSMultipleInstancesProhibited</key>
<true/>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Yahushad Monte</string>
</dict>
</plist>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The app updates itself automatically via [Sparkle](https://sparkle-project.org);
| **Dev Tools** | JSON, Base64, URL, hashing, UUID, timestamp, and case utilities |
| **Snippets** | A searchable library of reusable text — click one to paste it into the app you came from |
| **Scratchpad** | Always‑there plain‑text notepad with named notes, autosave, and a live word count |
| **Network Info** | Interface, local IPv4/IPv6, subnet, router, and DNS at a glance — click any value to copy; public IP only on request |

### Permissions

Expand Down
1 change: 1 addition & 0 deletions Scripts/package_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ HELPERS=(
"DMonteSnippets|DMonte Snippets.app|SnippetsInfo.plist"
"DMonteDriveEjector|DMonte Drive Ejector.app|DriveEjectorInfo.plist"
"DMonteScratchpad|DMonte Scratchpad.app|ScratchpadInfo.plist"
"DMonteNetworkInfo|DMonte Network Info.app|NetworkInfoInfo.plist"
)

stamp_version() {
Expand Down
3 changes: 2 additions & 1 deletion Sources/DMonteCore/AppPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ public enum AppDefaults {
DefaultsKey.focusTimerLongBreakMinutes: 15,
DefaultsKey.focusTimerLongBreakInterval: 4,
DefaultsKey.driveEjectorConfirmsEjectAll: true,
DefaultsKey.scratchpadFontSize: 13
DefaultsKey.scratchpadFontSize: 13,
DefaultsKey.networkInfoFetchesPublicIPAutomatically: false
])
}
}
24 changes: 23 additions & 1 deletion Sources/DMonteCore/HelperPanelHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,12 @@ public final class HelperPanelHost: NSObject {
switch configuration.positioning {
case .anchoredFrameOrCentered(let gap):
let frame: NSRect
if let anchor, let window = anchor.window, let screen = window.screen ?? NSScreen.main {
// `isAnchorReady` and not merely `anchor != nil`: a status button that exists but has
// not been laid out reports a zero-sized frame at the screen origin, and anchoring to
// that puts the panel in a corner of the wrong display. Centred is the honest answer
// until the real frame is available.
if let anchor, StatusItemAnchor.isPlaced(anchor), let window = anchor.window,
let screen = window.screen ?? NSScreen.main {
frame = HelperPanelPlacement.anchoredFrame(
for: size,
anchorFrame: Self.anchorFrameOnScreen(for: anchor, in: window),
Expand Down Expand Up @@ -530,4 +535,21 @@ public final class HelperPanelHost: NSObject {
let viewFrameInWindow = view.convert(view.bounds, to: nil)
return window.convertToScreen(viewFrameInWindow)
}

static func isAnchorReady(_ view: NSView?) -> Bool {
StatusItemAnchor.isPlaced(view)
}

/// `show()`, but only once the status item has stopped moving. See `StatusItemAnchor` for why
/// neither "it exists" nor "it is in the menu bar" is a sufficient signal on its own.
public func showWhenAnchored(timeout: TimeInterval = 2, pollInterval: TimeInterval = 0.05) {
StatusItemAnchor.whenSettled(
{ [weak self] in self?.anchorView() },
timeout: timeout,
pollInterval: pollInterval
) { [weak self] in
self?.show()
}
}

}
Loading
Loading