diff --git a/CHANGES.md b/CHANGES.md index 2c88208..78488b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/README.md b/README.md index 3f07dd9..0e5d213 100644 --- a/README.md +++ b/README.md @@ -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): @@ -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 @@ -86,8 +85,7 @@ Built products are created in `products/`. ## BSD License -Copyright (c) 2004-2025 Sveinbjorn Thordarson -<sveinbjorn@sveinbjorn.org> +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: diff --git a/resources/Info.plist b/resources/Info.plist index 14de933..4a52367 100644 --- a/resources/Info.plist +++ b/resources/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable Sloth CFBundleGetInfoString - © 2004-2025 Sveinbjorn Thordarson. BSD License. + © 2004-2026 Sveinbjorn Thordarson. BSD License. CFBundleIconFile sloth CFBundleIdentifier @@ -19,17 +19,17 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.5 + 3.6 CFBundleSignature ???? CFBundleVersion - 383 + 391 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion 11.5 NSHumanReadableCopyright - © 2004-2025 Sveinbjorn Thordarson. BSD License. + © 2004-2026 Sveinbjorn Thordarson. BSD License. NSMainNibFile MainMenu NSPrincipalClass diff --git a/resources/MainMenu.xib b/resources/MainMenu.xib index 4bf07d5..390484f 100644 --- a/resources/MainMenu.xib +++ b/resources/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -652,7 +652,7 @@ CA - + @@ -1018,6 +1018,8 @@ DQ + + diff --git a/source/Common.h b/source/Common.h index c1b7d90..9142a5e 100644 --- a/source/Common.h +++ b/source/Common.h @@ -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" diff --git a/source/InfoPanelController.m b/source/InfoPanelController.m index 46ba97e..6dcffcf 100644 --- a/source/InfoPanelController.m +++ b/source/InfoPanelController.m @@ -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"]]; } } diff --git a/source/Item.m b/source/Item.m index 0fc0ff9..aedc221 100644 --- a/source/Item.m +++ b/source/Item.m @@ -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 diff --git a/source/SlothController.m b/source/SlothController.m index 9ae1ad0..4150f4d 100644 --- a/source/SlothController.m +++ b/source/SlothController.m @@ -382,8 +382,8 @@ - (void)observeValueForKeyPath:(NSString *)keyPath } // Filter content according to active filters -- (NSMutableArray *)filterContent:(NSMutableArray *)unfilteredContent - numberOfMatchingFiles:(NSInteger *)matchingFilesCount { +- (NSMutableArray *)filterContent:(NSMutableArray *)unfilteredContent + numberOfMatchingFiles:(NSInteger *)matchingFilesCount { BOOL showRegularFiles = [DEFAULTS boolForKey:@"showRegularFiles"]; BOOL showDirectories = [DEFAULTS boolForKey:@"showDirectories"]; BOOL showIPSockets = [DEFAULTS boolForKey:@"showIPSockets"]; @@ -489,14 +489,14 @@ - (NSMutableArray *)filterContent:(NSMutableArray *)unfilteredContent return unfilteredContent; } - NSMutableArray *filteredContent = [NSMutableArray array]; + NSMutableArray *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 *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) { @@ -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]; @@ -999,7 +1000,7 @@ - (OSStatus)authenticate { } - (void)deauthenticate { - DLog(@"Deathenticating"); + DLog(@"Deauthenticating"); if (authRef) { AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); authRef = NULL; diff --git a/source/Util/IconUtils.m b/source/Util/IconUtils.m index fb59059..8f76d1e 100644 --- a/source/Util/IconUtils.m +++ b/source/Util/IconUtils.m @@ -44,11 +44,11 @@ #define ERR_ICON_PATH CORE_TYPES_RESOURCE(@"AlertStopIcon.icns") #define QUESTIONMARK_ICON_PATH CORE_TYPES_RESOURCE(@"GenericQuestionMarkIcon.icns") -static NSMutableDictionary *iconStore; +static NSDictionary *iconStore; @implementation IconUtils -+ (NSMutableDictionary *)_loadIcons { ++ (NSDictionary *)_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 @@ -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 *icons = [NSMutableDictionary new]; - + NSDictionary*> *iconSettings = @{ @"File": @[ @{ @"path": FILE_ICON_PATH, @"template": @YES }, @@ -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 } @@ -155,7 +156,7 @@ @implementation IconUtils } } - return icons; + return [icons copy]; } + (NSImage *)imageNamed:(NSString *)name { diff --git a/source/Util/ProcessUtils.h b/source/Util/ProcessUtils.h index d23fc4e..dc9c1f6 100644 --- a/source/Util/ProcessUtils.h +++ b/source/Util/ProcessUtils.h @@ -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; diff --git a/source/Util/ProcessUtils.m b/source/Util/ProcessUtils.m index 99b5e60..5bd36bb 100644 --- a/source/Util/ProcessUtils.m +++ b/source/Util/ProcessUtils.m @@ -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 { @@ -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; } @@ -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; @@ -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", @@ -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); @@ -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; @@ -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); @@ -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); } @end diff --git a/source/Util/VolumesPopUpButton.m b/source/Util/VolumesPopUpButton.m index c2c4e3b..04fb7a8 100644 --- a/source/Util/VolumesPopUpButton.m +++ b/source/Util/VolumesPopUpButton.m @@ -128,26 +128,27 @@ - (void)populateMenu { // Add all filesystems (except /dev) for (NSNumber *fsid in [filesystems allKeys]) { NSDictionary *fs = filesystems[fsid]; - if ([fs[@"mountpoint"] isEqualToString:@"/dev"]) { + NSString *mountpoint = fs[@"mountpoint"]; + if ([mountpoint isEqualToString:@"/dev"]) { continue; } - NSString *menuItemName = fs[@"mountpoint"]; - + NSString *menuItemName = mountpoint; + // Get volume name, if possible NSString *volumeName; - NSURL *url = [NSURL fileURLWithPath:fs[@"mountpoint"]]; + NSURL *url = [NSURL fileURLWithPath:mountpoint]; NSError *err; [url getResourceValue:&volumeName forKey:NSURLVolumeNameKey error:&err]; if (volumeName != nil) { - menuItemName = [NSString stringWithFormat:@"%@ - %@", volumeName, fs[@"mountpoint"]]; + menuItemName = [NSString stringWithFormat:@"%@ - %@", volumeName, mountpoint]; } - + SEL action = @selector(notifyDelegateSelectionHasChanged:); NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:menuItemName action:action keyEquivalent:@""]; [item setTarget:self]; - [item setToolTip:fs[@"mountpoint"]]; + [item setToolTip:mountpoint]; [item setRepresentedObject:fs]; [volumesMenu addItem:item]; }