From e0a3c5d0b9f2ccb87e841c0b25ac44e27a1241e2 Mon Sep 17 00:00:00 2001 From: azxyc <46279597+Azxyc@users.noreply.github.com> Date: Fri, 8 May 2026 20:34:07 +0100 Subject: [PATCH] rust rewrite --- .cargo/config.toml | 2 + .gitignore | 68 +--- Cargo.lock | 16 + Cargo.toml | 15 + Package.swift | 16 - README.md | 9 +- Sources/netcaps/netcaps.swift | 357 ------------------ changelog.md | 7 +- netcaps.xcodeproj/project.pbxproj | 300 --------------- .../contents.xcworkspacedata | 11 - .../UserInterfaceState.xcuserstate | Bin 31396 -> 0 bytes .../xcschemes/netcaps run-as-release.xcscheme | 87 ----- .../xcshareddata/xcschemes/netcaps.xcscheme | 85 ----- .../xcschemes/xcschememanagement.plist | 27 -- src/app.rs | 91 +++++ src/caps.rs | 60 +++ src/cli.rs | 173 +++++++++ src/ffi.rs | 139 +++++++ src/hid.rs | 350 +++++++++++++++++ src/main.rs | 26 ++ src/network.rs | 188 +++++++++ 21 files changed, 1081 insertions(+), 946 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 Package.swift delete mode 100644 Sources/netcaps/netcaps.swift delete mode 100644 netcaps.xcodeproj/project.pbxproj delete mode 100644 netcaps.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 netcaps.xcodeproj/project.xcworkspace/xcuserdata/tajc.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 netcaps.xcodeproj/xcshareddata/xcschemes/netcaps run-as-release.xcscheme delete mode 100644 netcaps.xcodeproj/xcshareddata/xcschemes/netcaps.xcscheme delete mode 100644 netcaps.xcodeproj/xcuserdata/tajc.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 src/app.rs create mode 100644 src/caps.rs create mode 100644 src/cli.rs create mode 100644 src/ffi.rs create mode 100644 src/hid.rs create mode 100644 src/main.rs create mode 100644 src/network.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..3baa594 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +MACOSX_DEPLOYMENT_TARGET = "10.15" diff --git a/.gitignore b/.gitignore index 52fe2f7..775e6c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,62 +1,10 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +target/ +debug/ -## User settings -xcuserdata/ +**/*.rs.bk +**/mutants.out*/ +rustc-ice-*.txt +*.pdb -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output +.DS_Store +.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..52f1a8e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "netcaps" +version = "1.6.1" +dependencies = [ + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5923049 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "netcaps" +version = "1.6.1" +edition = "2024" +description = "A network activity light on your Caps Lock key" +license = "MIT" +repository = "https://github.com/forcequitOS/netcaps" + +[dependencies] +libc = "0.2" + +[profile.release] +codegen-units = 1 +lto = true +strip = true diff --git a/Package.swift b/Package.swift deleted file mode 100644 index 9d6d60a..0000000 --- a/Package.swift +++ /dev/null @@ -1,16 +0,0 @@ -// swift-tools-version: 6.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "netcaps", - platforms: [ - .macOS(.v10_15) - ], - targets: [ - .executableTarget( - name: "netcaps" - ) - ] -) diff --git a/README.md b/README.md index 3ae9e52..a714c37 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ brew install forcequitOS/brew/netcaps It couldn't get much simpler than this. +### Build From Source +``` +cargo build --release --locked +``` + --- ### Run At Startup **Globally:** @@ -30,7 +35,7 @@ It couldn't get much simpler than this. And you're off to the races! >[!NOTE] -All functionality of your Caps Lock key is 100% preserved with netcaps. Also, netcaps is proudly written in Swift. Yay. +All functionality of your Caps Lock key is 100% preserved with netcaps. Also, netcaps is proudly written in Rust. Yay. >[!WARNING] I don't know if this will impact your battery life or if it'll kill your Caps Lock key LED over time. Your mileage may vary. I'm not responsible if this somehow blows up your computer, but it probably shouldn't. @@ -69,4 +74,4 @@ Want to monitor disk activity instead of network activity? Check out the sister --utun-only, -U - only listen on utunX -That really. Is about it. Have fun. \ No newline at end of file +That really. Is about it. Have fun. diff --git a/Sources/netcaps/netcaps.swift b/Sources/netcaps/netcaps.swift deleted file mode 100644 index 102eb9e..0000000 --- a/Sources/netcaps/netcaps.swift +++ /dev/null @@ -1,357 +0,0 @@ -// netcaps.swift -// Made by Taj C (forcequit) - -import Foundation -import Darwin -import IOKit -import CoreGraphics -import AppKit - -// MARK: - Caps Lock State -func isCapsLockOn() -> Bool { - return CGEventSource.keyState(.combinedSessionState, key: 57) -} - -@MainActor var legitInterval: TimeInterval = 0.00300 -@MainActor var lastCapsState: Bool = false -@MainActor var lastCapsCheckTime: TimeInterval = 0 -@MainActor func setInterval() { - let now = ProcessInfo.processInfo.systemUptime - if now - lastCapsCheckTime > 0.5 { - let currentCapsState = isCapsLockOn() - if currentCapsState != lastCapsState { - legitInterval = currentCapsState ? 0.01050 : 0.00300 - lastCapsState = currentCapsState - } - lastCapsCheckTime = now - } -} - -// MARK: - Blink Caps Lock LED -@MainActor -class CapsLockLEDManager { - private var manager: IOHIDManager? - private var cachedLEDElements: [(device: IOHIDDevice, element: IOHIDElement)] = [] - private var deviceCooldowns: [IOHIDDevice: TimeInterval] = [:] - init?() { - guard createManager() else { return nil } - } - private func createManager() -> Bool { - manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone)) - guard let manager = manager else { return false } - let match: [[String: Any]] = [[ - kIOHIDDeviceUsagePageKey as String: kHIDPage_GenericDesktop, - kIOHIDDeviceUsageKey as String: kHIDUsage_GD_Keyboard - ]] - IOHIDManagerSetDeviceMatchingMultiple(manager, match as CFArray) - IOHIDManagerRegisterDeviceMatchingCallback(manager, deviceAttachedCallback, Unmanaged.passUnretained(self).toOpaque()) - IOHIDManagerRegisterDeviceRemovalCallback(manager, deviceRemovedCallback, Unmanaged.passUnretained(self).toOpaque()) - IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) - let openRc = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone)) - if openRc == kIOReturnNotPermitted { - print("Input Monitoring permissions need to be granted, exiting...") - NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent")!) - exit(1) - } - return cacheDevices() - } - - private func cacheDevices() -> Bool { - guard let manager = manager, - let devicesCF = IOHIDManagerCopyDevices(manager) else { return false } - - cachedLEDElements.removeAll() - let devices = (devicesCF as NSSet) as! Set - - for device in devices { - var ledPage = kHIDPage_LEDs - let matchDict = NSMutableDictionary() - matchDict[kIOHIDElementUsagePageKey] = CFNumberCreate(kCFAllocatorDefault, .intType, &ledPage) - guard let elementsCF = IOHIDDeviceCopyMatchingElements(device, matchDict, 0) else { - continue - } - let elements = elementsCF as! [IOHIDElement] - for element in elements where IOHIDElementGetUsagePage(element) == UInt32(kHIDPage_LEDs) { - if IOHIDElementGetUsage(element) == UInt32(kHIDUsage_LED_CapsLock) { - cachedLEDElements.append((device, element)) - break - } - } - } - return !cachedLEDElements.isEmpty - } - - func reinitialize() { - deviceCooldowns.removeAll() - if let manager = manager { - IOHIDManagerUnscheduleFromRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) - IOHIDManagerClose(manager, IOOptionBits(kIOHIDOptionsTypeNone)) - } - manager = nil - let _ = createManager() - } - - func toggle(_ on: Bool) { - let now = ProcessInfo.processInfo.systemUptime - for (device, element) in cachedLEDElements { - if let cooldownUntil = deviceCooldowns[device], now < cooldownUntil { - continue - } - let value = IOHIDValueCreateWithIntegerValue( - kCFAllocatorDefault, - element, - mach_absolute_time(), - on ? 1 : 0 - ) - let result = IOHIDDeviceSetValue(device, element, value) - if result == -536870165 { - return - } - if result == -536870203 { - deviceCooldowns[device] = now + 1.0 - continue - } - if result == 268435459 || result == -536870195 { - if !silent { - print("Re-initializing IOHIDManager...") - } - reinitialize() - } - } - } - - func blink(times: Int = 1, interval: TimeInterval) { - let capsOn = isCapsLockOn() - for _ in 1...times { - if capsOn { - toggle(false) - Thread.sleep(forTimeInterval: interval) - toggle(true) - } else { - toggle(true) - Thread.sleep(forTimeInterval: interval) - toggle(false) - } - Thread.sleep(forTimeInterval: interval) - } - } - - // This stuff should just recache everything upon attaching/detaching a device - private let deviceAttachedCallback: IOHIDDeviceCallback = { context, result, sender, device in - guard let context = context else { return } - let this = Unmanaged.fromOpaque(context).takeUnretainedValue() - Task { @MainActor in - try? await Task.sleep(nanoseconds: 500_000_000) - this.reinitialize() - } - } - - private let deviceRemovedCallback: IOHIDDeviceCallback = { context, result, sender, device in - guard let context = context else { return } - let this = Unmanaged.fromOpaque(context).takeUnretainedValue() - Task { @MainActor in - try? await Task.sleep(nanoseconds: 500_000_000) - this.reinitialize() - } - } -} -@MainActor let ledManager = CapsLockLEDManager() -@MainActor -func blinkCapsLock(times: Int = 1, interval: TimeInterval = legitInterval) { - ledManager?.blink(times: times, interval: interval) -} - -// MARK: - Network Monitoring -@MainActor var allowedPrefixes: Set = ["en", "pdp_ip", "awdl", "ap", "llw"] -@MainActor var cachedInterfaceNames: [String] = [] -@MainActor var lastInterfaceCacheTime: TimeInterval = 0 - -@MainActor func refreshInterfaceCache() { - var ifaddr: UnsafeMutablePointer? - guard getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr else { return } - defer { freeifaddrs(ifaddr) } - - cachedInterfaceNames.removeAll(keepingCapacity: true) - var ptr = firstAddr - while true { - let name = String(cString: ptr.pointee.ifa_name) - if isAllowedInterface(name) && !cachedInterfaceNames.contains(name) { - cachedInterfaceNames.append(name) - } - guard let next = ptr.pointee.ifa_next else { break } - ptr = next - } -} - -@MainActor func isAllowedInterface(_ name: String) -> Bool { - if name.hasPrefix("en") { return allowedPrefixes.contains("en") } - if name.hasPrefix("lo") { return allowedPrefixes.contains("lo") } - if name.hasPrefix("utun") { return allowedPrefixes.contains("utun") } - if name.hasPrefix("awdl") { return allowedPrefixes.contains("awdl") } - if name.hasPrefix("llw") { return allowedPrefixes.contains("llw") } - if name.hasPrefix("ap") { return allowedPrefixes.contains("ap") } - if name.hasPrefix("pdp_ip") { return allowedPrefixes.contains("pdp_ip") } - return false -} - -@MainActor func getNetworkBytes() -> (rx: UInt64, tx: UInt64) { - let currentTime = ProcessInfo.processInfo.systemUptime - if currentTime - lastInterfaceCacheTime > 30.0 || cachedInterfaceNames.isEmpty { - refreshInterfaceCache() - lastInterfaceCacheTime = currentTime - } - var ifaddr: UnsafeMutablePointer? - guard getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr else { return (0, 0) } - defer { freeifaddrs(ifaddr) } - var rx: UInt64 = 0 - var tx: UInt64 = 0 - var ptr = firstAddr - while true { - let name = String(cString: ptr.pointee.ifa_name) - if isAllowedInterface(name) { - if let data = ptr.pointee.ifa_data?.assumingMemoryBound(to: if_data.self) { - rx &+= UInt64(data.pointee.ifi_ibytes) - tx &+= UInt64(data.pointee.ifi_obytes) - } - } - - if ptr.pointee.ifa_next == nil { break } - ptr = ptr.pointee.ifa_next! - } - return (rx, tx) -} - -// This is really goofy. -let args = CommandLine.arguments -let silent = args.contains("-s") || args.contains("--silent") - -@main -struct main { - static func main() { - // MARK: - Argument Handling - // I probably should've just used ArgumentParser - let hasHelpOrVersion = args.contains("-h") || args.contains("--help") || - args.contains("-v") || args.contains("--version") - if hasHelpOrVersion && args.count > 2 { - print("") - print("Error: --help and --version cannot be combined with other flags.") - print("") - exit(1) - } - - if args.contains("-v") || args.contains("--version") { - print("netcaps version 1.6.1") - print(" Made by Taj C (forcequit)") - print(" Check this out on GitHub, at https://github.com/forcequitOS/netcaps") - exit(0) - } - - let onlyFlags = [ - args.contains("-L") || args.contains("--local-only"), - args.contains("-E") || args.contains("--en-only"), - args.contains("-P") || args.contains("--peers-only"), - args.contains("-U") || args.contains("--utun-only") - ] - let onlyFlagCount = onlyFlags.filter { $0 }.count - - let hasIncludedFlags = args.contains("-l") || args.contains("--local-included") || - args.contains("-u") || args.contains("--utun-included") - let hasOnlyFlags = onlyFlagCount > 0 - - if (onlyFlagCount > 1) || (hasIncludedFlags && hasOnlyFlags) || args.contains("-h") || args.contains("--help") { - print("") - print("Usage:") - print(" netcaps [arguments]") - print("") - print("Arguments:") - print(" --silent, -s - silences command-line output") - print(" --local-only, -L - only listen on loX") - print(" --en-only, -E - only listen on enX") - print(" --peers-only, -P - only listen on awdlX and llwX") - print(" --utun-only, -U - only listen on utunX") - print(" --local-included, -l - listen on loX in addition to others") - print(" --utun-included, -u - listen on utunX in addition to others") - print(" --version, -v - displays current version of netcaps") - print(" --help, -h - shows this help menu") - print("") - if onlyFlagCount > 1 { - print("Error: Cannot use multiple 'only' flags together.") - print("") - } else if hasIncludedFlags && hasOnlyFlags { - print("Error: Cannot combine 'included' flags with 'only' flags.") - print("") - } - exit(0) - } - - if args.contains("-L") || args.contains("--local-only") { - allowedPrefixes = ["lo"] - } else if args.contains("-E") || args.contains("--en-only") { - allowedPrefixes = ["en"] - } else if args.contains("-P") || args.contains("--peers-only") { - allowedPrefixes = ["awdl", "llw"] - } else if args.contains("-U") || args.contains("--utun-only") { - allowedPrefixes = ["utun"] - } else if args.contains("-l") || args.contains("--local-included") { - allowedPrefixes.insert("lo") - } else if args.contains("-u") || args.contains("--utun-included") { - allowedPrefixes.insert("utun") - } - - if silent { - setpriority(PRIO_PROCESS, 0, 15) - } - var previousBytes = getNetworkBytes() - var checksWithoutActivity = 0 - let maxChecksBeforeSlowdown = 7500 - - // MARK: - Main Loop - while true { - autoreleasepool { - setInterval() - Thread.sleep(forTimeInterval: legitInterval) - let currentBytes = getNetworkBytes() - if currentBytes.rx > previousBytes.rx || currentBytes.tx > previousBytes.tx { - if !silent { - print("RX: \(currentBytes.rx), TX: \(currentBytes.tx)") - } - blinkCapsLock() - checksWithoutActivity = 0 - } else { - checksWithoutActivity += 1 - if checksWithoutActivity >= maxChecksBeforeSlowdown { - Thread.sleep(forTimeInterval: 0.05) - } - } - previousBytes = currentBytes - } - } - } -} - -class SleepObserver { - init() { - let center = NSWorkspace.shared.notificationCenter - center.addObserver( - self, - selector: #selector(willSleep(_:)), - name: NSWorkspace.willSleepNotification, - object: nil - ) - center.addObserver( - self, - selector: #selector(didWake(_:)), - name: NSWorkspace.didWakeNotification, - object: nil - ) - } - @MainActor @objc func willSleep(_ notification: Notification) { - ledManager?.toggle(false) - } - @MainActor @objc func didWake(_ notification: Notification) { - Task { - try? await Task.sleep(nanoseconds: 500_000_000) - ledManager?.reinitialize() - } - } -} diff --git a/changelog.md b/changelog.md index 461f4cf..3e98eee 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +### Rust Rewrite + +- Replaced the SwiftPM/Xcode executable with a Cargo-based Rust implementation. +- Kept the same Caps Lock LED, network-interface monitoring, and command-line behavior. + ### Efficiency & Leniency - The default interval's been changed again to be LESS efficient, because I've done some magic tricks to decrease CPU usage further, and CPU usage was already really low. @@ -7,4 +12,4 @@ - Only the first LED will be enumerated for a given device - CPU usage should be somewhat reduced now if you use Karabiner-Elements. Your CPU usage for netcaps WILL ALWAYS be better, though, if you disable Karabiner's virtual HID device. The peak CPU usage I've gotten WITH Karabiner on this release is ~10%. Without, it's around 6%. -Future: Don't re-enumerate duplicate devices maybe or maybe not depending on if I can be bothered. \ No newline at end of file +Future: Don't re-enumerate duplicate devices maybe or maybe not depending on if I can be bothered. diff --git a/netcaps.xcodeproj/project.pbxproj b/netcaps.xcodeproj/project.pbxproj deleted file mode 100644 index acd41e8..0000000 --- a/netcaps.xcodeproj/project.pbxproj +++ /dev/null @@ -1,300 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXCopyFilesBuildPhase section */ - 36DEB8B72E8A141B00BED146 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = /usr/share/man/man1/; - dstSubfolderSpec = 0; - files = ( - ); - runOnlyForDeploymentPostprocessing = 1; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 36D756482E8A1D7B003065E0 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; - 36DEB8B92E8A141B00BED146 /* netcaps */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = netcaps; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 36D756502E8A20A2003065E0 /* Sources */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = Sources; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - 36DEB8B62E8A141B00BED146 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 36DEB8B02E8A141B00BED146 = { - isa = PBXGroup; - children = ( - 36D756502E8A20A2003065E0 /* Sources */, - 36D756482E8A1D7B003065E0 /* Package.swift */, - 36DEB8BA2E8A141B00BED146 /* Products */, - ); - sourceTree = ""; - }; - 36DEB8BA2E8A141B00BED146 /* Products */ = { - isa = PBXGroup; - children = ( - 36DEB8B92E8A141B00BED146 /* netcaps */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 36DEB8B82E8A141B00BED146 /* netcaps */ = { - isa = PBXNativeTarget; - buildConfigurationList = 36DEB8C02E8A141B00BED146 /* Build configuration list for PBXNativeTarget "netcaps" */; - buildPhases = ( - 36DEB8B52E8A141B00BED146 /* Sources */, - 36DEB8B62E8A141B00BED146 /* Frameworks */, - 36DEB8B72E8A141B00BED146 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 36D756502E8A20A2003065E0 /* Sources */, - ); - name = netcaps; - packageProductDependencies = ( - ); - productName = netcaps; - productReference = 36DEB8B92E8A141B00BED146 /* netcaps */; - productType = "com.apple.product-type.tool"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 36DEB8B12E8A141B00BED146 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 2600; - LastUpgradeCheck = 2600; - TargetAttributes = { - 36DEB8B82E8A141B00BED146 = { - CreatedOnToolsVersion = 26.0.1; - }; - }; - }; - buildConfigurationList = 36DEB8B42E8A141B00BED146 /* Build configuration list for PBXProject "netcaps" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 36DEB8B02E8A141B00BED146; - minimizedProjectReferenceProxies = 1; - preferredProjectObjectVersion = 77; - productRefGroup = 36DEB8BA2E8A141B00BED146 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 36DEB8B82E8A141B00BED146 /* netcaps */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXSourcesBuildPhase section */ - 36DEB8B52E8A141B00BED146 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 36DEB8BE2E8A141B00BED146 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 26.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 36DEB8BF2E8A141B00BED146 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 26.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - }; - name = Release; - }; - 36DEB8C12E8A141B00BED146 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = cc.forcequit.netcaps; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 6.0; - }; - name = Debug; - }; - 36DEB8C22E8A141B00BED146 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = cc.forcequit.netcaps; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 6.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 36DEB8B42E8A141B00BED146 /* Build configuration list for PBXProject "netcaps" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 36DEB8BE2E8A141B00BED146 /* Debug */, - 36DEB8BF2E8A141B00BED146 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 36DEB8C02E8A141B00BED146 /* Build configuration list for PBXNativeTarget "netcaps" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 36DEB8C12E8A141B00BED146 /* Debug */, - 36DEB8C22E8A141B00BED146 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 36DEB8B12E8A141B00BED146 /* Project object */; -} diff --git a/netcaps.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/netcaps.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 35c0f7c..0000000 --- a/netcaps.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/netcaps.xcodeproj/project.xcworkspace/xcuserdata/tajc.xcuserdatad/UserInterfaceState.xcuserstate b/netcaps.xcodeproj/project.xcworkspace/xcuserdata/tajc.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 202c1f45cf0f629d064079fa4b4ffe28a2afcf40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31396 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/netcaps.xcodeproj/xcshareddata/xcschemes/netcaps.xcscheme b/netcaps.xcodeproj/xcshareddata/xcschemes/netcaps.xcscheme deleted file mode 100644 index a11d347..0000000 --- a/netcaps.xcodeproj/xcshareddata/xcschemes/netcaps.xcscheme +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/netcaps.xcodeproj/xcuserdata/tajc.xcuserdatad/xcschemes/xcschememanagement.plist b/netcaps.xcodeproj/xcuserdata/tajc.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index d78c0e5..0000000 --- a/netcaps.xcodeproj/xcuserdata/tajc.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - SchemeUserState - - netcaps run-as-release.xcscheme_^#shared#^_ - - orderHint - 1 - - netcaps.xcscheme_^#shared#^_ - - orderHint - 0 - - - SuppressBuildableAutocreation - - 36DEB8B82E8A141B00BED146 - - primary - - - - - diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..c0564e5 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,91 @@ +use std::thread; +use std::time::Duration; + +use crate::caps::BlinkInterval; +use crate::cli::{CliCommand, CliConfig}; +use crate::hid::CapsLockLedManager; +use crate::network::NetworkMonitor; + +const MAX_CHECKS_BEFORE_SLOWDOWN: u32 = 7_500; + +pub fn run() -> Result<(), String> { + match crate::cli::parse(std::env::args()) { + CliCommand::Run(config) => run_monitor(config), + CliCommand::Version => { + print_version(); + Ok(()) + } + CliCommand::Help { message, exit_code } => { + print_help(message.as_deref()); + std::process::exit(exit_code); + } + } +} + +fn run_monitor(config: CliConfig) -> Result<(), String> { + if config.silent { + lower_process_priority(); + } + + let mut led_manager = CapsLockLedManager::new(config.silent)?; + let mut network = NetworkMonitor::new(config.interface_filter); + let mut blink_interval = BlinkInterval::default(); + let mut previous_bytes = network.byte_counts(); + let mut checks_without_activity = 0; + + loop { + blink_interval.refresh(); + thread::sleep(blink_interval.current()); + + let current_bytes = network.byte_counts(); + if current_bytes.has_activity_since(previous_bytes) { + if !config.silent { + println!("RX: {}, TX: {}", current_bytes.rx, current_bytes.tx); + } + led_manager.blink(1, blink_interval.current()); + checks_without_activity = 0; + } else { + checks_without_activity += 1; + if checks_without_activity >= MAX_CHECKS_BEFORE_SLOWDOWN { + thread::sleep(Duration::from_millis(50)); + } + } + + previous_bytes = current_bytes; + } +} + +fn lower_process_priority() { + unsafe { + let _ = libc::setpriority(libc::PRIO_PROCESS, 0, 15); + } +} + +fn print_version() { + println!("netcaps version {}", env!("CARGO_PKG_VERSION")); + println!(" Made by Taj C (forcequit)"); + println!(" Check this out on GitHub, at https://github.com/forcequitOS/netcaps"); +} + +fn print_help(message: Option<&str>) { + println!(); + println!("Usage:"); + println!(" netcaps [arguments]"); + println!(); + println!("Arguments:"); + println!(" --silent, -s - silences command-line output"); + println!(" --local-only, -L - only listen on loX"); + println!(" --en-only, -E - only listen on enX"); + println!(" --peers-only, -P - only listen on awdlX and llwX"); + println!(" --utun-only, -U - only listen on utunX"); + println!(" --local-included, -l - listen on loX in addition to others"); + println!(" --utun-included, -u - listen on utunX in addition to others"); + println!(" --version, -v - displays current version of netcaps"); + println!(" --help, -h - shows this help menu"); + println!(); + + if let Some(message) = message { + println!("Error: {message}"); + println!(); + } +} diff --git a/src/caps.rs b/src/caps.rs new file mode 100644 index 0000000..d0740b6 --- /dev/null +++ b/src/caps.rs @@ -0,0 +1,60 @@ +use std::time::{Duration, Instant}; + +use crate::ffi; + +const CAPS_LOCK_KEY_CODE: u16 = 57; +const CAPS_ON_INTERVAL: Duration = Duration::from_micros(10_500); +const CAPS_OFF_INTERVAL: Duration = Duration::from_micros(3_000); +const CAPS_STATE_REFRESH_INTERVAL: Duration = Duration::from_millis(500); + +pub fn is_caps_lock_on() -> bool { + unsafe { + ffi::CGEventSourceKeyState( + ffi::K_CG_EVENT_SOURCE_STATE_COMBINED_SESSION_STATE, + CAPS_LOCK_KEY_CODE, + ) + } +} + +pub struct BlinkInterval { + current: Duration, + last_caps_state: bool, + last_check: Option, +} + +impl Default for BlinkInterval { + fn default() -> Self { + Self { + current: CAPS_OFF_INTERVAL, + last_caps_state: false, + last_check: None, + } + } +} + +impl BlinkInterval { + pub fn current(&self) -> Duration { + self.current + } + + pub fn refresh(&mut self) { + if self + .last_check + .is_some_and(|last_check| last_check.elapsed() <= CAPS_STATE_REFRESH_INTERVAL) + { + return; + } + + let current_caps_state = is_caps_lock_on(); + if current_caps_state != self.last_caps_state { + self.current = if current_caps_state { + CAPS_ON_INTERVAL + } else { + CAPS_OFF_INTERVAL + }; + self.last_caps_state = current_caps_state; + } + + self.last_check = Some(Instant::now()); + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..d930dbe --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,173 @@ +use crate::network::{InterfaceFilter, InterfaceKind}; + +pub enum CliCommand { + Run(CliConfig), + Version, + Help { + message: Option, + exit_code: i32, + }, +} + +pub struct CliConfig { + pub silent: bool, + pub interface_filter: InterfaceFilter, +} + +#[derive(Default)] +struct Flags { + silent: bool, + help: bool, + version: bool, + local_only: bool, + en_only: bool, + peers_only: bool, + utun_only: bool, + local_included: bool, + utun_included: bool, +} + +pub fn parse(args: impl IntoIterator) -> CliCommand { + let args = args.into_iter().collect::>(); + let mut flags = Flags::default(); + + for arg in args.iter().skip(1) { + match arg.as_str() { + "--silent" | "-s" => flags.silent = true, + "--help" | "-h" => flags.help = true, + "--version" | "-v" => flags.version = true, + "--local-only" | "-L" => flags.local_only = true, + "--en-only" | "-E" => flags.en_only = true, + "--peers-only" | "-P" => flags.peers_only = true, + "--utun-only" | "-U" => flags.utun_only = true, + "--local-included" | "-l" => flags.local_included = true, + "--utun-included" | "-u" => flags.utun_included = true, + _ => {} + } + } + + if (flags.help || flags.version) && args.len() > 2 { + return CliCommand::Help { + message: Some("--help and --version cannot be combined with other flags.".to_owned()), + exit_code: 1, + }; + } + + if flags.version { + return CliCommand::Version; + } + + let only_flag_count = [ + flags.local_only, + flags.en_only, + flags.peers_only, + flags.utun_only, + ] + .into_iter() + .filter(|flag| *flag) + .count(); + + if only_flag_count > 1 { + return CliCommand::Help { + message: Some("Cannot use multiple 'only' flags together.".to_owned()), + exit_code: 0, + }; + } + + if only_flag_count > 0 && (flags.local_included || flags.utun_included) { + return CliCommand::Help { + message: Some("Cannot combine 'included' flags with 'only' flags.".to_owned()), + exit_code: 0, + }; + } + + if flags.help { + return CliCommand::Help { + message: None, + exit_code: 0, + }; + } + + CliCommand::Run(CliConfig { + silent: flags.silent, + interface_filter: interface_filter(flags), + }) +} + +fn interface_filter(flags: Flags) -> InterfaceFilter { + if flags.local_only { + return InterfaceFilter::only([InterfaceKind::Loopback]); + } + + if flags.en_only { + return InterfaceFilter::only([InterfaceKind::Ethernet]); + } + + if flags.peers_only { + return InterfaceFilter::only([InterfaceKind::Awdl, InterfaceKind::Llw]); + } + + if flags.utun_only { + return InterfaceFilter::only([InterfaceKind::Utun]); + } + + let mut filter = InterfaceFilter::default(); + if flags.local_included { + filter.insert(InterfaceKind::Loopback); + } + if flags.utun_included { + filter.insert(InterfaceKind::Utun); + } + filter +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_args(args: &[&str]) -> CliCommand { + parse(args.iter().map(|arg| (*arg).to_owned())) + } + + #[test] + fn parses_version() { + assert!(matches!( + parse_args(&["netcaps", "--version"]), + CliCommand::Version + )); + } + + #[test] + fn rejects_help_combined_with_other_flags() { + match parse_args(&["netcaps", "--help", "--silent"]) { + CliCommand::Help { + message: Some(message), + exit_code: 1, + } => assert!(message.contains("cannot be combined")), + _ => panic!("expected a failing help command"), + } + } + + #[test] + fn rejects_multiple_only_flags() { + match parse_args(&["netcaps", "--local-only", "--en-only"]) { + CliCommand::Help { + message: Some(message), + exit_code: 0, + } => assert!(message.contains("multiple 'only' flags")), + _ => panic!("expected a help command for conflicting only flags"), + } + } + + #[test] + fn allows_both_included_flags() { + match parse_args(&["netcaps", "--local-included", "--utun-included"]) { + CliCommand::Run(config) => { + assert!(config.interface_filter.allows_name("lo0")); + assert!(config.interface_filter.allows_name("utun4")); + assert!(config.interface_filter.allows_name("en0")); + } + _ => panic!("expected a run command"), + } + } +} diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000..499e5bb --- /dev/null +++ b/src/ffi.rs @@ -0,0 +1,139 @@ +#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] + +use libc::{c_char, c_int, c_long, c_uint, c_void}; + +pub type CFAllocatorRef = *const c_void; +pub type CFArrayRef = *const c_void; +pub type CFDictionaryRef = *const c_void; +pub type CFIndex = c_long; +pub type CFNumberRef = *const c_void; +pub type CFNumberType = CFIndex; +pub type CFSetRef = *const c_void; +pub type CFStringRef = *const c_void; +pub type CFStringEncoding = u32; +pub type CFTypeRef = *const c_void; +pub type CGEventSourceStateID = c_uint; +pub type CGKeyCode = u16; +pub type IOHIDDeviceRef = *const c_void; +pub type IOHIDElementRef = *const c_void; +pub type IOHIDManagerRef = *const c_void; +pub type IOHIDValueRef = *const c_void; +pub type IOOptionBits = u32; +pub type IOReturn = c_int; + +pub const K_CF_NUMBER_SINT32_TYPE: CFNumberType = 3; +pub const K_CF_STRING_ENCODING_UTF8: CFStringEncoding = 0x0800_0100; +pub const K_CG_EVENT_SOURCE_STATE_COMBINED_SESSION_STATE: CGEventSourceStateID = 0; +pub const K_HID_PAGE_GENERIC_DESKTOP: i32 = 0x01; +pub const K_HID_USAGE_GD_KEYBOARD: i32 = 0x06; +pub const K_HID_PAGE_LEDS: i32 = 0x08; +pub const K_HID_USAGE_LED_CAPS_LOCK: u32 = 0x02; +pub const K_IO_OPTIONS_TYPE_NONE: IOOptionBits = 0; +pub const K_IORETURN_SUCCESS: IOReturn = 0; +pub const K_IORETURN_EXCLUSIVE_ACCESS: IOReturn = -536_870_203; +pub const K_IORETURN_NOT_OPEN: IOReturn = -536_870_195; +pub const K_IORETURN_NOT_PERMITTED: IOReturn = -536_870_174; +pub const K_IORETURN_ABORTED: IOReturn = -536_870_165; +pub const K_KARABINER_SUCCESS_SKIP: IOReturn = 268_435_459; + +#[repr(C)] +pub struct CFArrayCallBacks { + pub version: CFIndex, + pub retain: *const c_void, + pub release: *const c_void, + pub copy_description: *const c_void, + pub equal: *const c_void, +} + +#[repr(C)] +pub struct CFDictionaryKeyCallBacks { + pub version: CFIndex, + pub retain: *const c_void, + pub release: *const c_void, + pub copy_description: *const c_void, + pub equal: *const c_void, + pub hash: *const c_void, +} + +#[repr(C)] +pub struct CFDictionaryValueCallBacks { + pub version: CFIndex, + pub retain: *const c_void, + pub release: *const c_void, + pub copy_description: *const c_void, + pub equal: *const c_void, +} + +#[link(name = "CoreFoundation", kind = "framework")] +unsafe extern "C" { + pub static kCFTypeArrayCallBacks: CFArrayCallBacks; + pub static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks; + pub static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks; + + pub fn CFArrayCreate( + allocator: CFAllocatorRef, + values: *const *const c_void, + num_values: CFIndex, + callbacks: *const CFArrayCallBacks, + ) -> CFArrayRef; + pub fn CFArrayGetCount(array: CFArrayRef) -> CFIndex; + pub fn CFArrayGetValueAtIndex(array: CFArrayRef, index: CFIndex) -> *const c_void; + pub fn CFDictionaryCreate( + allocator: CFAllocatorRef, + keys: *const *const c_void, + values: *const *const c_void, + num_values: CFIndex, + key_callbacks: *const CFDictionaryKeyCallBacks, + value_callbacks: *const CFDictionaryValueCallBacks, + ) -> CFDictionaryRef; + pub fn CFNumberCreate( + allocator: CFAllocatorRef, + the_type: CFNumberType, + value_ptr: *const c_void, + ) -> CFNumberRef; + pub fn CFRelease(cf: CFTypeRef); + pub fn CFRetain(cf: CFTypeRef) -> CFTypeRef; + pub fn CFSetGetCount(set: CFSetRef) -> CFIndex; + pub fn CFSetGetValues(set: CFSetRef, values: *mut *const c_void); + pub fn CFStringCreateWithCString( + allocator: CFAllocatorRef, + c_str: *const c_char, + encoding: CFStringEncoding, + ) -> CFStringRef; +} + +#[link(name = "CoreGraphics", kind = "framework")] +unsafe extern "C" { + pub fn CGEventSourceKeyState(state_id: CGEventSourceStateID, key: CGKeyCode) -> bool; +} + +#[link(name = "IOKit", kind = "framework")] +unsafe extern "C" { + pub fn IOHIDDeviceCopyMatchingElements( + device: IOHIDDeviceRef, + matching: CFDictionaryRef, + options: IOOptionBits, + ) -> CFArrayRef; + pub fn IOHIDDeviceSetValue( + device: IOHIDDeviceRef, + element: IOHIDElementRef, + value: IOHIDValueRef, + ) -> IOReturn; + pub fn IOHIDElementGetUsage(element: IOHIDElementRef) -> u32; + pub fn IOHIDElementGetUsagePage(element: IOHIDElementRef) -> u32; + pub fn IOHIDManagerClose(manager: IOHIDManagerRef, options: IOOptionBits) -> IOReturn; + pub fn IOHIDManagerCopyDevices(manager: IOHIDManagerRef) -> CFSetRef; + pub fn IOHIDManagerCreate(allocator: CFAllocatorRef, options: IOOptionBits) -> IOHIDManagerRef; + pub fn IOHIDManagerOpen(manager: IOHIDManagerRef, options: IOOptionBits) -> IOReturn; + pub fn IOHIDManagerSetDeviceMatchingMultiple(manager: IOHIDManagerRef, multiple: CFArrayRef); + pub fn IOHIDValueCreateWithIntegerValue( + allocator: CFAllocatorRef, + element: IOHIDElementRef, + time_stamp: u64, + value: CFIndex, + ) -> IOHIDValueRef; +} + +unsafe extern "C" { + pub fn mach_absolute_time() -> u64; +} diff --git a/src/hid.rs b/src/hid.rs new file mode 100644 index 0000000..e525817 --- /dev/null +++ b/src/hid.rs @@ -0,0 +1,350 @@ +use std::collections::HashMap; +use std::ffi::CStr; +use std::process::Command; +use std::ptr; +use std::thread; +use std::time::{Duration, Instant}; + +use libc::c_void; + +use crate::caps::is_caps_lock_on; +use crate::ffi; + +#[derive(Clone, Copy)] +struct LedElement { + device: ffi::IOHIDDeviceRef, + element: ffi::IOHIDElementRef, +} + +pub struct CapsLockLedManager { + manager: Option, + led_elements: Vec, + device_cooldowns: HashMap, + silent: bool, +} + +impl CapsLockLedManager { + pub fn new(silent: bool) -> Result { + let mut manager = Self { + manager: None, + led_elements: Vec::new(), + device_cooldowns: HashMap::new(), + silent, + }; + manager.create_manager()?; + Ok(manager) + } + + pub fn blink(&mut self, times: usize, interval: Duration) { + let caps_on = is_caps_lock_on(); + for _ in 0..times { + if caps_on { + self.toggle(false); + thread::sleep(interval); + self.toggle(true); + } else { + self.toggle(true); + thread::sleep(interval); + self.toggle(false); + } + thread::sleep(interval); + } + } + + fn toggle(&mut self, on: bool) { + let now = Instant::now(); + let mut should_reinitialize = false; + + for led in self.led_elements.iter().copied() { + let device_key = led.device as usize; + if self + .device_cooldowns + .get(&device_key) + .is_some_and(|cooldown_until| now < *cooldown_until) + { + continue; + } + + let result = unsafe { set_led_value(led.device, led.element, on) }; + match result { + ffi::K_IORETURN_SUCCESS => {} + ffi::K_IORETURN_ABORTED => return, + ffi::K_IORETURN_EXCLUSIVE_ACCESS => { + self.device_cooldowns + .insert(device_key, now + Duration::from_secs(1)); + } + ffi::K_KARABINER_SUCCESS_SKIP | ffi::K_IORETURN_NOT_OPEN => { + should_reinitialize = true; + break; + } + _ => {} + } + } + + if should_reinitialize { + if !self.silent { + println!("Re-initializing IOHIDManager..."); + } + let _ = self.reinitialize(); + } + } + + fn reinitialize(&mut self) -> Result<(), String> { + self.device_cooldowns.clear(); + self.close_manager(); + self.create_manager() + } + + fn create_manager(&mut self) -> Result<(), String> { + let manager = unsafe { ffi::IOHIDManagerCreate(ptr::null(), ffi::K_IO_OPTIONS_TYPE_NONE) }; + if manager.is_null() { + return Ok(()); + } + + if let Some(matching_array) = unsafe { create_keyboard_matching_array() } { + unsafe { + ffi::IOHIDManagerSetDeviceMatchingMultiple(manager, matching_array); + ffi::CFRelease(matching_array); + } + } + + let open_result = unsafe { ffi::IOHIDManagerOpen(manager, ffi::K_IO_OPTIONS_TYPE_NONE) }; + if open_result == ffi::K_IORETURN_NOT_PERMITTED { + unsafe { + ffi::CFRelease(manager); + } + open_input_monitoring_preferences(); + return Err("Input Monitoring permissions need to be granted, exiting...".to_owned()); + } + + if open_result != ffi::K_IORETURN_SUCCESS { + unsafe { + ffi::CFRelease(manager); + } + if !self.silent { + eprintln!("Unable to open IOHIDManager: {open_result}"); + } + return Ok(()); + } + + self.manager = Some(manager); + self.cache_devices(); + Ok(()) + } + + fn cache_devices(&mut self) { + self.clear_led_cache(); + + let Some(manager) = self.manager else { + return; + }; + + let devices = unsafe { ffi::IOHIDManagerCopyDevices(manager) }; + if devices.is_null() { + return; + } + + let count = unsafe { ffi::CFSetGetCount(devices) }; + if count <= 0 { + unsafe { + ffi::CFRelease(devices); + } + return; + } + + let mut values = vec![ptr::null(); count as usize]; + unsafe { + ffi::CFSetGetValues(devices, values.as_mut_ptr()); + } + + for device_value in values { + let device = device_value as ffi::IOHIDDeviceRef; + if device.is_null() { + continue; + } + + if let Some(element) = unsafe { find_caps_lock_led(device) } { + unsafe { + ffi::CFRetain(device); + } + self.led_elements.push(LedElement { device, element }); + } + } + + unsafe { + ffi::CFRelease(devices); + } + } + + fn close_manager(&mut self) { + self.clear_led_cache(); + if let Some(manager) = self.manager.take() { + unsafe { + let _ = ffi::IOHIDManagerClose(manager, ffi::K_IO_OPTIONS_TYPE_NONE); + ffi::CFRelease(manager); + } + } + } + + fn clear_led_cache(&mut self) { + for led in self.led_elements.drain(..) { + unsafe { + ffi::CFRelease(led.element); + ffi::CFRelease(led.device); + } + } + } +} + +impl Drop for CapsLockLedManager { + fn drop(&mut self) { + self.close_manager(); + } +} + +unsafe fn set_led_value( + device: ffi::IOHIDDeviceRef, + element: ffi::IOHIDElementRef, + on: bool, +) -> ffi::IOReturn { + let value = unsafe { + ffi::IOHIDValueCreateWithIntegerValue( + ptr::null(), + element, + ffi::mach_absolute_time(), + if on { 1 } else { 0 }, + ) + }; + if value.is_null() { + return ffi::K_IORETURN_NOT_OPEN; + } + + let result = unsafe { ffi::IOHIDDeviceSetValue(device, element, value) }; + unsafe { + ffi::CFRelease(value); + } + result +} + +unsafe fn find_caps_lock_led(device: ffi::IOHIDDeviceRef) -> Option { + let usage_page_key = unsafe { create_cf_string(c"UsagePage") }?; + let usage_page = unsafe { create_cf_number(ffi::K_HID_PAGE_LEDS) }?; + let keys = [usage_page_key]; + let values = [usage_page]; + let matching = unsafe { create_cf_dictionary(&keys, &values)? }; + let elements = unsafe { + ffi::IOHIDDeviceCopyMatchingElements(device, matching, ffi::K_IO_OPTIONS_TYPE_NONE) + }; + + unsafe { + ffi::CFRelease(matching); + ffi::CFRelease(usage_page_key); + ffi::CFRelease(usage_page); + } + + if elements.is_null() { + return None; + } + + let count = unsafe { ffi::CFArrayGetCount(elements) }; + let mut caps_lock_element = None; + for index in 0..count { + let element = + unsafe { ffi::CFArrayGetValueAtIndex(elements, index) as ffi::IOHIDElementRef }; + if element.is_null() { + continue; + } + + let usage_page = unsafe { ffi::IOHIDElementGetUsagePage(element) }; + let usage = unsafe { ffi::IOHIDElementGetUsage(element) }; + if usage_page == ffi::K_HID_PAGE_LEDS as u32 && usage == ffi::K_HID_USAGE_LED_CAPS_LOCK { + caps_lock_element = Some(unsafe { ffi::CFRetain(element) as ffi::IOHIDElementRef }); + break; + } + } + + unsafe { + ffi::CFRelease(elements); + } + caps_lock_element +} + +unsafe fn create_keyboard_matching_array() -> Option { + let usage_page_key = unsafe { create_cf_string(c"DeviceUsagePage") }?; + let usage_key = unsafe { create_cf_string(c"DeviceUsage") }?; + let usage_page = unsafe { create_cf_number(ffi::K_HID_PAGE_GENERIC_DESKTOP) }?; + let usage = unsafe { create_cf_number(ffi::K_HID_USAGE_GD_KEYBOARD) }?; + let keys = [usage_page_key, usage_key]; + let values = [usage_page, usage]; + let dictionary = unsafe { create_cf_dictionary(&keys, &values) }; + + unsafe { + ffi::CFRelease(usage_page_key); + ffi::CFRelease(usage_key); + ffi::CFRelease(usage_page); + ffi::CFRelease(usage); + } + + let dictionary = dictionary?; + let array_values = [dictionary]; + let array = unsafe { + ffi::CFArrayCreate( + ptr::null(), + array_values.as_ptr(), + array_values.len() as ffi::CFIndex, + &ffi::kCFTypeArrayCallBacks, + ) + }; + + unsafe { + ffi::CFRelease(dictionary); + } + + if array.is_null() { None } else { Some(array) } +} + +unsafe fn create_cf_number(value: i32) -> Option { + let number = unsafe { + ffi::CFNumberCreate( + ptr::null(), + ffi::K_CF_NUMBER_SINT32_TYPE, + &value as *const i32 as *const c_void, + ) + }; + if number.is_null() { None } else { Some(number) } +} + +unsafe fn create_cf_string(value: &CStr) -> Option { + let string = unsafe { + ffi::CFStringCreateWithCString(ptr::null(), value.as_ptr(), ffi::K_CF_STRING_ENCODING_UTF8) + }; + if string.is_null() { None } else { Some(string) } +} + +unsafe fn create_cf_dictionary( + keys: &[ffi::CFTypeRef], + values: &[ffi::CFTypeRef], +) -> Option { + debug_assert_eq!(keys.len(), values.len()); + let dictionary = unsafe { + ffi::CFDictionaryCreate( + ptr::null(), + keys.as_ptr(), + values.as_ptr(), + keys.len() as ffi::CFIndex, + &ffi::kCFTypeDictionaryKeyCallBacks, + &ffi::kCFTypeDictionaryValueCallBacks, + ) + }; + if dictionary.is_null() { + None + } else { + Some(dictionary) + } +} + +fn open_input_monitoring_preferences() { + let _ = Command::new("open") + .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent") + .status(); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..448414e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +#[cfg(target_os = "macos")] +mod app; +#[cfg(target_os = "macos")] +mod caps; +#[cfg(target_os = "macos")] +mod cli; +#[cfg(target_os = "macos")] +mod ffi; +#[cfg(target_os = "macos")] +mod hid; +#[cfg(target_os = "macos")] +mod network; + +#[cfg(target_os = "macos")] +fn main() { + if let Err(error) = app::run() { + eprintln!("{error}"); + std::process::exit(1); + } +} + +#[cfg(not(target_os = "macos"))] +fn main() { + eprintln!("netcaps is only supported on macOS."); + std::process::exit(1); +} diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..566b2fb --- /dev/null +++ b/src/network.rs @@ -0,0 +1,188 @@ +use std::collections::HashSet; +use std::ffi::CStr; +use std::time::{Duration, Instant}; + +const INTERFACE_CACHE_INTERVAL: Duration = Duration::from_secs(30); + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum InterfaceKind { + Ap, + Awdl, + Ethernet, + Llw, + Loopback, + PdpIp, + Utun, +} + +#[derive(Clone)] +pub struct InterfaceFilter { + allowed: HashSet, +} + +impl Default for InterfaceFilter { + fn default() -> Self { + Self::only([ + InterfaceKind::Ethernet, + InterfaceKind::PdpIp, + InterfaceKind::Awdl, + InterfaceKind::Ap, + InterfaceKind::Llw, + ]) + } +} + +impl InterfaceFilter { + pub fn only(kinds: impl IntoIterator) -> Self { + Self { + allowed: kinds.into_iter().collect(), + } + } + + pub fn insert(&mut self, kind: InterfaceKind) { + self.allowed.insert(kind); + } + + pub(crate) fn allows_name(&self, name: &str) -> bool { + InterfaceKind::from_name(name).is_some_and(|kind| self.allowed.contains(&kind)) + } +} + +impl InterfaceKind { + fn from_name(name: &str) -> Option { + if name.starts_with("pdp_ip") { + return Some(Self::PdpIp); + } + if name.starts_with("utun") { + return Some(Self::Utun); + } + if name.starts_with("awdl") { + return Some(Self::Awdl); + } + if name.starts_with("llw") { + return Some(Self::Llw); + } + if name.starts_with("lo") { + return Some(Self::Loopback); + } + if name.starts_with("en") { + return Some(Self::Ethernet); + } + if name.starts_with("ap") { + return Some(Self::Ap); + } + None + } +} + +#[derive(Clone, Copy)] +pub struct ByteCounts { + pub rx: u64, + pub tx: u64, +} + +impl ByteCounts { + pub fn has_activity_since(self, previous: Self) -> bool { + self.rx > previous.rx || self.tx > previous.tx + } +} + +pub struct NetworkMonitor { + filter: InterfaceFilter, + cached_interface_names: HashSet, + last_cache_refresh: Option, +} + +impl NetworkMonitor { + pub fn new(filter: InterfaceFilter) -> Self { + Self { + filter, + cached_interface_names: HashSet::new(), + last_cache_refresh: None, + } + } + + pub fn byte_counts(&mut self) -> ByteCounts { + if self.cache_expired() { + self.refresh_interface_cache(); + } + + unsafe { read_network_bytes(&self.cached_interface_names) } + } + + fn cache_expired(&self) -> bool { + self.cached_interface_names.is_empty() + || self + .last_cache_refresh + .is_none_or(|last_refresh| last_refresh.elapsed() > INTERFACE_CACHE_INTERVAL) + } + + fn refresh_interface_cache(&mut self) { + self.cached_interface_names = unsafe { read_interface_names(&self.filter) }; + self.last_cache_refresh = Some(Instant::now()); + } +} + +unsafe fn read_interface_names(filter: &InterfaceFilter) -> HashSet { + let mut ifaddr = std::ptr::null_mut(); + if unsafe { libc::getifaddrs(&mut ifaddr) } != 0 || ifaddr.is_null() { + return HashSet::new(); + } + + let mut names = HashSet::new(); + let mut current = ifaddr; + while !current.is_null() { + if let Some(name) = unsafe { interface_name(current) } + && filter.allows_name(&name) + { + names.insert(name); + } + current = unsafe { (*current).ifa_next }; + } + + unsafe { + libc::freeifaddrs(ifaddr); + } + names +} + +unsafe fn read_network_bytes(interface_names: &HashSet) -> ByteCounts { + let mut ifaddr = std::ptr::null_mut(); + if unsafe { libc::getifaddrs(&mut ifaddr) } != 0 || ifaddr.is_null() { + return ByteCounts { rx: 0, tx: 0 }; + } + + let mut rx = 0_u64; + let mut tx = 0_u64; + let mut current = ifaddr; + + while !current.is_null() { + if let Some(name) = unsafe { interface_name(current) } + && interface_names.contains(&name) + { + let data = unsafe { (*current).ifa_data as *const libc::if_data }; + if !data.is_null() { + rx = rx.wrapping_add(unsafe { (*data).ifi_ibytes as u64 }); + tx = tx.wrapping_add(unsafe { (*data).ifi_obytes as u64 }); + } + } + current = unsafe { (*current).ifa_next }; + } + + unsafe { + libc::freeifaddrs(ifaddr); + } + ByteCounts { rx, tx } +} + +unsafe fn interface_name(ifaddr: *mut libc::ifaddrs) -> Option { + let name = unsafe { (*ifaddr).ifa_name }; + if name.is_null() { + return None; + } + + unsafe { CStr::from_ptr(name) } + .to_str() + .ok() + .map(ToOwned::to_owned) +}