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
6 changes: 4 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

## Sloth version history

### 26/03/2026 - Version 3.6
### 21/04/2026 - Version 3.6

* Fixed issue with missing generic executable icons on macOS 26 Tahoe
* Minor performance improvements
* Minor bug fixes and edge case optimizations
* Minor bug fixes and improved edge case handling
* Minor interface refinements
* More code modernization

### 14/11/2025 - Version 3.5
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Sloth is free, open source software and has been continuously developed
and maintained for a very long time (since 2004).
**If you find this program useful, please [make a donation](https://sveinbjorn.org/donations).**

* **[⬇ Download Sloth 3.5](https://sveinbjorn.org/files/software/sloth.zip)**
* **[⬇ Download Sloth 3.6](https://sveinbjorn.org/files/software/sloth.zip)**
(~1.3 MB, Universal ARM/Intel 64-bit, macOS 11 or later, Developer ID signed and notarized)

Sloth can also be installed via [Homebrew](https://brew.sh) (may not be the latest version):
Expand All @@ -52,7 +52,6 @@ If you need to run it on macOS 10.13-10.15, version 3.4 works just fine.
For 10.9-10.12, version 3.2 does the trick.
All older versions can be downloaded [here](https://sveinbjorn.org/files/software/sloth/).


## Screenshots

#### View open files
Expand Down Expand Up @@ -86,8 +85,7 @@ Built products are created in `products/`.

## BSD License

Copyright (c) 2004-2025 Sveinbjorn Thordarson
&lt;<a href="mailto:sveinbjorn@sveinbjorn.org">sveinbjorn@sveinbjorn.org</a>&gt;
Copyright (c) 2004-2026 [Sveinbjorn Thordarson](mailto:sveinbjorn@sveinbjorn.org)

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Expand Down
8 changes: 4 additions & 4 deletions resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>Sloth</string>
<key>CFBundleGetInfoString</key>
<string>© 2004-2025 Sveinbjorn Thordarson. BSD License.</string>
<string>© 2004-2026 Sveinbjorn Thordarson. BSD License.</string>
<key>CFBundleIconFile</key>
<string>sloth</string>
<key>CFBundleIdentifier</key>
Expand All @@ -19,17 +19,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.5</string>
<string>3.6</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>383</string>
<string>391</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
<string>11.5</string>
<key>NSHumanReadableCopyright</key>
<string>© 2004-2025 Sveinbjorn Thordarson. BSD License.</string>
<string>© 2004-2026 Sveinbjorn Thordarson. BSD License.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
Expand Down
8 changes: 5 additions & 3 deletions resources/MainMenu.xib
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24765" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment version="110000" identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24765"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
Expand Down Expand Up @@ -652,7 +652,7 @@ CA
<window identifier="SlothWindow" title="Sloth" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" frameAutosaveName="SlothWindow" animationBehavior="default" tabbingMode="disallowed" titlebarAppearsTransparent="YES" id="21" userLabel="SlothWindow">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="125" y="249" width="892" height="611"/>
<rect key="screenRect" x="0.0" y="0.0" width="1536" height="935"/>
<rect key="screenRect" x="0.0" y="0.0" width="1496" height="937"/>
<value key="minSize" type="size" width="853" height="350"/>
<view key="contentView" wantsLayer="YES" id="2">
<rect key="frame" x="0.0" y="0.0" width="892" height="611"/>
Expand Down Expand Up @@ -1018,6 +1018,8 @@ DQ
<menu key="menu" id="H2g-v4-v5e">
<items>
<menuItem title="Manually" state="on" identifier="0" id="ME3-aB-chF"/>
<menuItem title="Every 1 sec" tag="1" identifier="1" id="7Yp-3Z-2vw"/>
<menuItem title="Every 2 sec" tag="2" identifier="2" id="2Qy-Xo-ldd"/>
<menuItem title="Every 3 sec" tag="3" identifier="3" id="mCs-ZR-eJn"/>
<menuItem title="Every 5 sec" tag="5" identifier="5" id="TNK-bH-SVj"/>
<menuItem title="Every 10 sec" tag="10" identifier="10" id="87h-gY-6aq"/>
Expand Down
2 changes: 1 addition & 1 deletion source/Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/

#define PROGRAM_NAME @"Sloth"
#define PROGRAM_VERSION @"3.5"
#define PROGRAM_VERSION @"3.6"
#define PROGRAM_WEBSITE @"https://sveinbjorn.org/sloth"
#define PROGRAM_DONATIONS @"https://sveinbjorn.org/donations"
#define PROGRAM_GITHUB_WEBSITE @"https://github.com/sveinbjornt/Sloth"
Expand Down
23 changes: 11 additions & 12 deletions source/InfoPanelController.m
Original file line number Diff line number Diff line change
Expand Up @@ -398,21 +398,20 @@ - (NSString *)accessModeDescriptionForItem:(NSDictionary *)itemDict {
}

// OK, we don't have any access mode
if (access == nil) {
if (accessModeName == nil) {
if (itemDict[@"fd"] == nil) {
return EMPTY_PLACEHOLDER;
}
if (hasFD) {
access = EMPTY_PLACEHOLDER;
} else if ([itemDict[@"fd"] isEqualToString:@"txt"]) {
access = @"N/A: Program binary, asset or shared lib";
} else if ([itemDict[@"fd"] isEqualToString:@"cwd"]) {
access = @"N/A: Current working directory";
} else if ([itemDict[@"fd"] isEqualToString:@"twd"]) {
access = @"N/A: Per-thread working directory";
} else {
if (hasFD) {
access = EMPTY_PLACEHOLDER;
} else if ([itemDict[@"fd"] isEqualToString:@"txt"]) {
access = @"N/A: Program binary, asset or shared lib";
} else if ([itemDict[@"fd"] isEqualToString:@"cwd"]) {
access = @"N/A: Current working directory";
} else if ([itemDict[@"fd"] isEqualToString:@"twd"]) {
access = @"N/A: Per-thread working directory";
} else {
access = [NSString stringWithFormat:@"No file descriptor. Type: %@", itemDict[@"fd"]];
}
access = [NSString stringWithFormat:@"No file descriptor. Type: %@", itemDict[@"fd"]];
}
}

Expand Down
5 changes: 1 addition & 4 deletions source/Item.m
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@ - (id)objectForKey:(id)aKey {
}

- (NSString * __nullable)bundleIdentifier {
if (self[@"path"]) {
return [ProcessUtils identifierForBundleAtPath:self[@"path"]];
}
return nil;
return [ProcessUtils identifierForBundleAtPath:self[@"path"]];
}

@end
17 changes: 9 additions & 8 deletions source/SlothController.m
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,8 @@ - (void)observeValueForKeyPath:(NSString *)keyPath
}

// Filter content according to active filters
- (NSMutableArray *)filterContent:(NSMutableArray *)unfilteredContent
numberOfMatchingFiles:(NSInteger *)matchingFilesCount {
- (NSMutableArray<Item *> *)filterContent:(NSMutableArray<Item *> *)unfilteredContent
numberOfMatchingFiles:(NSInteger *)matchingFilesCount {
BOOL showRegularFiles = [DEFAULTS boolForKey:@"showRegularFiles"];
BOOL showDirectories = [DEFAULTS boolForKey:@"showDirectories"];
BOOL showIPSockets = [DEFAULTS boolForKey:@"showIPSockets"];
Expand Down Expand Up @@ -489,14 +489,14 @@ - (NSMutableArray *)filterContent:(NSMutableArray *)unfilteredContent
return unfilteredContent;
}

NSMutableArray *filteredContent = [NSMutableArray array];
NSMutableArray<Item *> *filteredContent = [NSMutableArray array];

// Iterate over each process, filter the children
for (NSMutableDictionary *process in self.unfilteredContent) {
for (Item *process in unfilteredContent) {

NSMutableArray *matchingFiles = [NSMutableArray array];
NSMutableArray<Item*> *matchingFiles = [NSMutableArray array];

for (NSDictionary *file in process[@"children"]) {
for (Item *file in process[@"children"]) {

// Let's see if child gets filtered by type or path
if (showAllItemTypes == NO) {
Expand Down Expand Up @@ -597,7 +597,8 @@ - (NSMutableArray *)filterContent:(NSMutableArray *)unfilteredContent

// If we have matching files for the process, and it's not being excluded as a non-app
if ([matchingFiles count] && !(showApplicationsOnly && ![process[@"app"] boolValue])) {
NSMutableDictionary *p = [process mutableCopy];
Item *p = [[Item alloc] init];
[p addEntriesFromDictionary:process];
p[@"children"] = matchingFiles;
// Num files shown in brackets after name needs to be updated
[LsofTask updateProcessInfo:p];
Expand Down Expand Up @@ -999,7 +1000,7 @@ - (OSStatus)authenticate {
}

- (void)deauthenticate {
DLog(@"Deathenticating");
DLog(@"Deauthenticating");
if (authRef) {
AuthorizationFree(authRef, kAuthorizationFlagDestroyRights);
authRef = NULL;
Expand Down
15 changes: 8 additions & 7 deletions source/Util/IconUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@
#define ERR_ICON_PATH CORE_TYPES_RESOURCE(@"AlertStopIcon.icns")
#define QUESTIONMARK_ICON_PATH CORE_TYPES_RESOURCE(@"GenericQuestionMarkIcon.icns")

static NSMutableDictionary<NSString*, NSImage*> *iconStore;
static NSDictionary<NSString*, NSImage*> *iconStore;

@implementation IconUtils

+ (NSMutableDictionary<NSString*, NSImage*> *)_loadIcons {
+ (NSDictionary<NSString*, NSImage*> *)_loadIcons {
// We want to use the cool invertible Mojave template icons to represent
// files, directories, IP sockets, etc. but they might not available. We
// therefore define primary icon assets and fallbacks in case they're
Expand All @@ -57,7 +57,7 @@ @implementation IconUtils
// version of macOS, the new ones on Mojave onwards. Saves us from having
// to bloat the application bundle with custom icon assets.
NSMutableDictionary<NSString*, NSImage*> *icons = [NSMutableDictionary new];

NSDictionary<NSString*, NSArray<NSDictionary *>*> *iconSettings = @{
@"File": @[
@{ @"path": FILE_ICON_PATH, @"template": @YES },
Expand Down Expand Up @@ -101,11 +101,12 @@ @implementation IconUtils
@{ @"path": HOME_ICON_PATH, @"template": @YES },
@{ @"type": NSFileTypeForHFSTypeCode(kToolbarHomeIcon), @"template": @NO }
],
// @"GenericApplication": @[
// @{ @"name": @"NSDefaultApplicationIcon", @"template": @NO }
// ],
// @"GenericApplication": @[
// @{ @"name": @"NSDefaultApplicationIcon", @"template": @NO }
// ],
@"GenericExecutable": @[
@{ @"path": EXEC_ICON_PATH, @"template": @NO },
@{ @"type": @"public.unix-executable", @"template": @NO}
],
@"Locked": @[
@{ @"type": NSFileTypeForHFSTypeCode(kLockedIcon), @"template": @NO }
Expand Down Expand Up @@ -155,7 +156,7 @@ @implementation IconUtils
}
}

return icons;
return [icons copy];
}

+ (NSImage *)imageNamed:(NSString *)name {
Expand Down
14 changes: 7 additions & 7 deletions source/Util/ProcessUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ NS_ASSUME_NONNULL_BEGIN

+ (NSRunningApplication * __nullable)appForPID:(pid_t)pid;
+ (BOOL)isAppProcess:(NSString *)bundlePath;
+ (NSString * __nullable)identifierForBundleAtPath:(NSString *)path;
+ (NSString * __nullable)identifierForBundleAtPath:(NSString * __nullable)path;
+ (BOOL)isProcessOwnedByCurrentUser:(pid_t)pid;
+ (uid_t)UIDForPID:(pid_t)pid;
+ (NSString *)ownerUserNameForPID:(pid_t)pid;
+ (NSString *)macProcessNameForPID:(pid_t)pid;
+ (NSString *)carbonProcessSerialNumberForPID:(pid_t)pid;
+ (NSString *)procNameForPID:(pid_t)pid;
+ (NSString *)fullKernelProcessNameForPID:(pid_t)pid;
+ (NSString *)executablePathForPID:(pid_t)pid;
+ (NSString * __nullable)ownerUserNameForPID:(pid_t)pid;
+ (NSString * __nullable)macProcessNameForPID:(pid_t)pid;
+ (NSString * __nullable)carbonProcessSerialNumberForPID:(pid_t)pid;
+ (NSString * __nullable)procNameForPID:(pid_t)pid;
+ (NSString * __nullable)fullKernelProcessNameForPID:(pid_t)pid;
+ (NSString * __nullable)executablePathForPID:(pid_t)pid;
+ (BOOL)killProcess:(int)pid
asRoot:(BOOL)asRoot
usingSIGKILL:(BOOL)useSigkill;
Expand Down
31 changes: 16 additions & 15 deletions source/Util/ProcessUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ + (BOOL)isAppProcess:(NSString *)bundlePath {
return [[NSWorkspace sharedWorkspace] type:fileType conformsToType:@"com.apple.application"];
}

+ (NSString * __nullable)identifierForBundleAtPath:(NSString *)path {
return (NSString * __nullable)[[NSBundle bundleWithPath:path] bundleIdentifier];
+ (NSString * __nullable)identifierForBundleAtPath:(NSString * __nullable)path {
if (path == nil) {
return nil;
}
return [[NSBundle bundleWithPath:path] bundleIdentifier];
}

+ (BOOL)isProcessOwnedByCurrentUser:(pid_t)pid {
Expand Down Expand Up @@ -87,12 +90,12 @@ + (uid_t)UIDForPID:(pid_t)pid {
return uid;
}

+ (NSString *)ownerUserNameForPID:(pid_t)pid {
+ (NSString * __nullable)ownerUserNameForPID:(pid_t)pid {
uid_t uid = [ProcessUtils UIDForPID:pid];
if (uid == -1) {
return nil;
}
register struct passwd *pw = getpwuid(uid);
struct passwd *pw = getpwuid(uid);
if (pw == NULL) {
return nil;
}
Expand All @@ -101,10 +104,10 @@ + (NSString *)ownerUserNameForPID:(pid_t)pid {
return s;
}

#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

+ (NSString *)macProcessNameForPID:(pid_t)pid {
+ (NSString * __nullable)macProcessNameForPID:(pid_t)pid {
ProcessSerialNumber psn;
if (GetProcessForPID(pid, &psn) == noErr) {
CFStringRef procName = NULL;
Expand All @@ -118,7 +121,7 @@ + (NSString *)macProcessNameForPID:(pid_t)pid {

// Some processes on macOS have a Carbon Process Manager
// Serial Number (PSN) in addition to a PID
+ (NSString *)carbonProcessSerialNumberForPID:(pid_t)pid {
+ (NSString * __nullable)carbonProcessSerialNumberForPID:(pid_t)pid {
ProcessSerialNumber psn;
if (GetProcessForPID(pid, &psn) == noErr) {
return [NSString stringWithFormat:@"%d",
Expand All @@ -133,7 +136,7 @@ + (NSString *)carbonProcessSerialNumberForPID:(pid_t)pid {

// This function returns process name truncated to 32 characters
// This is a limitation with libproc on macOS
+ (NSString *)procNameForPID:(pid_t)pid {
+ (NSString * __nullable)procNameForPID:(pid_t)pid {
char name[1024];
if (proc_name(pid, name, sizeof(name)) > 0) {
NSString *nameStr = @(name);
Expand All @@ -148,7 +151,7 @@ + (NSString *)procNameForPID:(pid_t)pid {
// See: https://opensource.apple.com/tarballs/adv_cmds/
// NOTE: This doesn't work for processes not owned by
// the current user. /bin/ps gets around this with suid.
+ (NSString *)fullKernelProcessNameForPID:(pid_t)pid {
+ (NSString * __nullable)fullKernelProcessNameForPID:(pid_t)pid {
int mib[3], argmax;
size_t syssize;
char *procargs, *cp;
Expand Down Expand Up @@ -203,7 +206,7 @@ + (NSString *)fullKernelProcessNameForPID:(pid_t)pid {
return ([pname length] == 0) ? nil : [pname lastPathComponent];
}

+ (NSString *)executablePathForPID:(pid_t)pid {
+ (NSString * __nullable)executablePathForPID:(pid_t)pid {
NSString *path = nil;
char *pathbuf = calloc(PROC_PIDPATHINFO_MAXSIZE, 1);

Expand Down Expand Up @@ -244,16 +247,14 @@ + (BOOL)killProcess:(int)pid asRoot:(BOOL)asRoot usingSIGKILL:(BOOL)useSigkill {
}

// Create and launch authorized kill task
STPrivilegedTask *task = [[STPrivilegedTask alloc] init];
STPrivilegedTask *task = [STPrivilegedTask new];
[task setLaunchPath:@(toolPath)];
NSString *sigFlag = useSigkill ? @"-9" : @"-15";
NSString *pidString = [NSString stringWithFormat:@"%d", pid];
[task setArguments:@[sigFlag, pidString]];
[task launchWithAuthorization:authRef];

err = [task launchWithAuthorization:authRef];
AuthorizationFree(authRef, kAuthorizationFlagDestroyRights);

return YES;
return (err == errAuthorizationSuccess);

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In killProcess:asRoot:usingSIGKILL:, the return value for the privileged path only reflects whether AuthorizationExecuteWithPrivileges was invoked successfully (i.e., launchWithAuthorization returned errAuthorizationSuccess), not whether /bin/kill actually succeeded. This can report success even when kill exits non-zero (e.g., PID already gone, EPERM). Consider waiting for the task to exit and basing the return value on its terminationStatus (and/or capturing stderr/output for diagnostics).

Suggested change
return (err == errAuthorizationSuccess);
if (err != errAuthorizationSuccess) {
return NO;
}
[task waitUntilExit];
return ([task terminationStatus] == 0);

Copilot uses AI. Check for mistakes.
}

@end
Loading