In my earlier posts, I mentioned how personal projects helped me stay motivated while I was learning programming. In this post I will talk about the first personal project that I built back in 2017, covering all steps from idea to final version.
I covered the purpose of this project back in this post, but as a quick recap it is a macOS application intended for my previous day job as an IT support specialist. The app was meant to save us time by displaying relevant information about the user we were helping (their access rights, software licenses, etc).
Here’s what it looked like at the end:
Contribute to almaleh/LionSearchDemo development by creating an account on GitHub.
It’s called LionSearch (Lion was the logo of our company, and many internal services were named after it).
Some of its features:
- Launches instantly, no loading
- Instantly look up any user in the organization (over 60,000 users)
- Auto-complete as you are typing the username (no need to memorize full username)
- Call user via Skype by tapping directly on their phone number
- Filter group memberships to find specific info (like VPN access)
The idea for this app is based on a bash script written by one of my colleagues. The script polls the Active Directory and retrieves info about the user I’m helping. So as I was learning Swift, I started thinking how how cool it would be to have a lightweight Mac app that lets me look-up any user in the company and shows all of the relevant IT information in one centralized location.
To get started I had to research several topics:
- How do I build a Mac app? (at this point, I could barely make iOS apps)
- How could I possibly interact with the Active Directory via Swift?
- How do I filter the info that I get back from the Active Directory to get something useful? (it returns a massive text file for each user)
I started tackling these points one by one. Luckily, it turns out macOS is not that different from iOS. Many of the elements I was familiar with had Mac counterparts (UIView -> NSView, UITableView -> NSTableView, etc), although the implementation details were often different (e.g. NSTableView can have multiple columns)
After reading a few tutorials on macOS app development and getting a bit familiar with the platform, I turned my attention to the next problem. How in the world do I communicate with Microsoft’s Active Directory?
Shell scripts in Swift
It was difficult to find resources online on running arbitrary shell scripts with Swift; basically I wanted to execute this Terminal command:
dscl localhost -read Active Directory/LL/All Domains/Users/<User>
After some online research, and lots of trial and error, I finally got this code to do it:
The dscl localhost -read command returns a massive text file (thousands of lines!) for the requested user. So next up I had to search through this file to get the relevant information. I was looking for stuff like their email address, password expiration time, VPN access level, etc.
As an example, the last password change date would be listed next to the string PasswordLastSet: inside the file, access memberships would be listed after memberOf:, etc. So after some research, I decided that Regular Expressions would be my best option here.
What’s a Regex?
This was the first time I’d heard about regular expressions, and I had absolutely no idea how they worked! (let alone how to run them in Swift, which is kind of a pain in itself).
So I went back to study mode and started learning about Regex. After a few days of reading, and lots of practice with Regex 101, I felt comfortable enough with Regex to continue my work on the project. But then I hit another road block, running Regex in Swift.
After yet more research and trial & error, I settled on this code:
It’s not pretty, but it gets the job done! For instance, to find the last password change date, I can do this:
let lastPassRegex = "(?<=PasswordLastSet: )[^(\n)]+"
Next up I had to build the regex for all the different values I wanted to extract. At this point I knew what I had to do to finish this project, it just took some time to build it (lots of edge cases and small details to consider).
If I count all the research time, the initial version of the app took me about 2 weeks to complete. This version positively received, and my IT colleagues started requesting features, such as last logon time, software licenses, group memberships, etc.
The app started as a single screen. I gave the app window a fixed size to save myself the headaches of resizing, and I tried to keep the design as simple as possible. Here’s what it looked like:
After ‘shipping’ the app, the next major feature I added was displaying group memberships. Every user in our organization was a member of several Active Directory groups, such as the VPN access group (which gave them access to our company VPN), the Adobe Creative Suite group, etc.
While troubleshooting a user problem, it was very useful for me to know what kind of memberships they had. For instance, if someone complained that they can’t use the office Distribution List (which lets them email everyone in a certain office), I can easily confirm if they’re part of the group that can use the DL and inform them if they’re missing access.
Users could be members of 90+ groups, so I had to add a second screen to the app to fit this information. This second screen would be presented using the ‘Sheet’ style.
By default, Sheet controllers are resize-able on the Mac, even if the base App window isn’t! This caused all sorts of weird behavior, as you could make the sheet so tiny the Close button would disappear, or make it as big as your entire screen.
To prevent these issues, I added the following line in viewDidAppear to disable resizing:
The rest of the Groups controller was pretty straightforward. There’s an NSTextField to filter for specific groups. An NSTableView to display the results, and a clipboard button.
Getting the Groups data was just a matter of applying the correct Regex to the Active Directory text file.
This feature was much more difficult to implement than I had anticipated. First, the app needs to have an up-to-date list of all the employee usernames within our company (over 60,000 users). Then it has to display the closest matches in a tableview, and let the user seamlessly navigate between the results and the search field with their keyboard, then hit Enter to choose a result. (or use the mouse)
To get the users list, I tried a variety of Terminal commands, to no avail. The most I could get was 1000 (seemingly random) users out of 60,000. From what I’ve read online, it is not possible on the Mac to list more than 1000 users from the Active Directory.
Luckily I had access to a Windows Server instance. So I googled how to create PowerShell scripts, wrote a script that downloads the usernames of all Active Directory users, and scheduled a daily Windows task to run that script every morning to get an updated usernames list, then it would upload this list to a Box folder on the cloud under a specific URL.
I configured LionSearch (the Mac app) to automatically fetch this list of users from the above URL upon launch.
Now that I had the data I needed in the app, I started building the tableView to display the auto-complete results. I added some conditions to show the table, for example it would stay hidden unless 2+ characters are entered in the search field, and at least 1 match was found.
At the time I didn’t know how to make the table resize itself dynamically based on its content, so I hard coded the different size values based on the results count, then adjusted its origin to compensate for the change 😅
Finally, I needed to find a way to move between cells and select a result using the keyboard (in addition to the mouse.) The solution I settled on was to monitor keyboard events, then perform work based on the event code.
You can monitor events with this line:
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyDown, handler: myKeyDownEvent)
This gist should give you an idea of how the myKeyDownEvent function worked: https://gist.github.com/almaleh/8265e69e9d7a28199276b89b785f530f
I think the result looked pretty sweet 🙂
Some surprises along the way
- NSSearchField does not have a built-in shake animation that you can use for invalid entries. I had to use a custom animation (thanks HWS!) for this purpose:
2. It took me a while to figure out the proper syntax to call a phone number directly from the app (using FaceTime, Skype, or other software you might have installed). Here it is:
NSWorkspace.shared.open(URL(string: "tel:" + number)!)
3. The responder chain is a much bigger deal on the Mac than iOS
4. Working with Regex in Swift is not a pleasant experience! Although Raw Strings in Swift 5 will make it slightly less painful
Overall, I really enjoyed this project and I learned a ton of things along the way. As mentioned above, the initial version of the app took me about 2 weeks to complete, but the additional features and bug fixes were completed in the months following release.
The source code for the app is available here: