Skip to content

Commit

Permalink
feat: initial working version
Browse files Browse the repository at this point in the history
  • Loading branch information
coolaj86 committed Aug 20, 2024
1 parent f1adc2f commit 36bc0c1
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 1 deletion.
1 change: 1 addition & 0 deletions .swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5.10
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,60 @@
# run-on-macos-screen-unlock
A tiny Swift program to run a command whenever the screen unlocks

A tiny Swift program to run a command whenever the screen unlocks \
(I use it for mounting remounting network shares after sleep)

```sh
run-on-macos-screen-unlock ./examples/mount-network-shares.sh
```

# Install

1. Download
```sh
curl --fail-with-body -L -O https://github.com/coolaj86/run-on-macos-screen-unlock/releases/download/v1.0.0/run-on-macos-screen-unlock-v1.0.0.tar.gz
```
2. Extract
```sh
tar xvf ./run-on-macos-screen-unlock-v1.0.0.tar.gz
```
3. Allow running even though it's unsigned
```sh
xattr -r -d com.apple.quarantine ./run-on-macos-screen-unlock
```
4. Move into your `PATH`
```sh
mv ./run-on-macos-screen-unlock ~/bin/
```
# Build from Source
1. Install XCode Tools \
(including `git` and `swift`)
```sh
xcode-select --install
```
2. Clone and enter the repo
```sh
git clone https://github.com/coolaj86/run-on-macos-screen-unlock.git
pushd ./run-on-macos-screen-unlock/
```
3. Build with `swiftc`
```sh
swiftc ./run-on-macos-screen-unlock.swift
```
# Release
1. Git tag and push
```sh
git tag v1.0.x
git push --tags
```
2. Create a release \
<https://github.com/coolaj86/run-on-macos-screen-unlock/releases/new>
3. Tar and upload
```sh
tar cvf ./run-on-macos-screen-unlock-v1.0.x.tar ./run-on-macos-screen-unlock
gzip ./run-on-macos-screen-unlock-v1.0.x.tar
open .
```
81 changes: 81 additions & 0 deletions examples/mount-network-shares.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/bin/sh
set -e
set -u

fn_mount_share() {
a_smb_share="${1:-}"
# ex: /usr/bin/osascript -e 'mount volume "smb://[email protected]/TimeMachineBackups"'
/usr/bin/osascript -e "mount volume \"${a_smb_share}\""
}

fn_version() {
echo "mount-macos-network-shares v1.0.0 (2024-08-19)"
echo "Copyright AJ ONeal (MPL-2.0)"
}

fn_help() {
echo ""
echo "USAGE"
echo " mount-macos-network-shares [path-to-config]"
echo ""
echo "OPTIONS"
echo " --help - print this message"
echo " -V,--version - print the version"
echo ""
echo "CONFIG"
echo " Default config file:"
echo " ~/.config/macos-network-shares/urls.conf"
echo ""
echo " Example config file contents:"
echo " smb://puter:[email protected]/TimeMachineBackups"
echo " smb://[email protected]/Family Photos"
echo ""
}

fn_mount_shares() {
b_urls_file="${1}"
while IFS= read -r b_share_url; do
fn_mount_share "${b_share_url}"
done < "${b_urls_file}"
}

main() {
if ! test -f ~/.config/macos-network-shares/urls.conf; then
mkdir -p ~/.config/macos-network-shares/ || true
chmod 0700 ~/.config/macos-network-shares || true
touch ~/.config/macos-network-shares/urls.conf || true
chmod 0600 ~/.config/macos-network-shares/urls.conf || true
echo "#smb://user:[email protected]/TimeMachineBackups" >> ~/.config/macos-network-shares/urls.conf || true
fi

b_urls_file="${1-$HOME/.config/macos-network-shares/urls.conf}"
case "${b_urls_file}" in
--version | -V | version)
fn_version
exit 0
;;
--help | help)
fn_help
exit 0
;;
*) ;;
esac

if ! test -e "${b_urls_file}" && grep -q -v -E '^\s*(#.*)?$' "${b_urls_file}"; then
{
echo ""
echo "ERROR"
echo " url list '${b_urls_file}' is empty or does not exist"
echo ""
fn_help
} >&2
fi

{
echo "Network URLs List: ${b_urls_file}"
} >&2

fn_mount_shares "${b_urls_file}"
}

main "$@"
175 changes: 175 additions & 0 deletions run-on-macos-screen-unlock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import Foundation

let name = (CommandLine.arguments[0] as NSString).lastPathComponent
let version = "1.0.0"
let build = "2024-08-19-001"

let versionMessage = """
\(name) \(version) (\(build))
"""

let copyrightMessage = """
Copyright 2024 AJ ONeal <[email protected]>
"""

let helpMessage = """
Runs a user-specified command whenever the screen is unlocked by
listening for the "com.apple.screenIsUnlocked" event, using /usr/bin/command -v
to find the program in the user's PATH (or the explicit path given), and then
runs it with /usr/bin/command, which can run aliases and shell functions also.
USAGE
\(name) [OPTIONS] <command> [--] [command-arguments]
OPTIONS
--version, -V, version
Display the version information and exit.
--help, help
Display this help and exit.
DESCRIPTION
\(name) is a simple command-line tool that demonstrates how to handle
version and help flags in a Swift program following POSIX conventions.
"""

signal(SIGINT) { _ in
printForHuman("received ctrl+c, exiting...\n")
exit(0)
}

enum ScriptError: Error {
case fileNotFound
}

func printForHuman(_ message: String) {
if let data = message.data(using: .utf8) {
FileHandle.standardError.write(data)
}
}

func getCommandPath(_ command: String) -> String? {
let commandv = Process()
commandv.launchPath = "/usr/bin/command"
commandv.arguments = ["-v", command]

let pipe = Pipe()
commandv.standardOutput = pipe
commandv.standardError = FileHandle.standardError

try! commandv.run()
commandv.waitUntilExit()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let scriptPath = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
else {
return nil
}

if commandv.terminationStatus != 0, scriptPath.isEmpty {
return nil
}

return scriptPath
}

class ScreenLockObserver {
var commandPath: String
var commandArgs: ArraySlice<String>

init(_ commandArgs: ArraySlice<String>) {
self.commandPath = commandArgs.first!
self.commandArgs = commandArgs

let dnc = DistributedNotificationCenter.default()

_ = dnc.addObserver(forName: NSNotification.Name("com.apple.screenIsLocked"), object: nil, queue: .main) { _ in
NSLog("notification: com.apple.screenIsLocked")
}

NSLog("Waiting for 'com.apple.screenIsUnlocked' to run \(self.commandArgs)")
_ = dnc.addObserver(forName: NSNotification.Name("com.apple.screenIsUnlocked"), object: nil, queue: .main) { _ in
NSLog("notification: com.apple.screenIsUnlocked")
self.runOnUnlock()
}
}

private func runOnUnlock() {
let task = Process()
task.launchPath = "/usr/bin/command"
task.arguments = Array(commandArgs)
task.standardOutput = FileHandle.standardOutput
task.standardError = FileHandle.standardError

do {
try task.run()
} catch {
printForHuman("Failed to run \(self.commandPath): \(error.localizedDescription)\n")
if let nsError = error as NSError? {
printForHuman("Error details: \(nsError)\n")
}
exit(1)
}

task.waitUntilExit()
}
}

@discardableResult
func removeItem(_ array: inout ArraySlice<String>, _ item: String) -> Bool {
if let index = array.firstIndex(of: item) {
array.remove(at: index)
return true
}
return false
}

func processArgs(_ args: inout ArraySlice<String>) -> ArraySlice<String> {
var childArgs: ArraySlice<String> = []
if let delimiterIndex = args.firstIndex(of: "--") {
let childArgsIndex = delimiterIndex + 1
childArgs = args[childArgsIndex...]
args.removeSubrange(delimiterIndex...)
}
if removeItem(&args, "--help") || removeItem(&args, "help") {
printForHuman(versionMessage)
printForHuman("\n")
printForHuman(helpMessage)
printForHuman("\n")
printForHuman(copyrightMessage)
exit(0)
}
if removeItem(&args, "--version") || removeItem(&args, "-V") || removeItem(&args, "version") {
printForHuman(versionMessage)
printForHuman(copyrightMessage)
exit(0)
}

childArgs = args + childArgs
guard childArgs.count > 0 else {
printForHuman(versionMessage)
printForHuman("\n")
printForHuman(helpMessage)
printForHuman("\n")
printForHuman(copyrightMessage)
exit(1)
}

let commandName = childArgs.first!
guard let commandPath = getCommandPath(commandName) else {
printForHuman("ERROR:\n \(commandName) not found in PATH\n")
exit(1)
}

childArgs[childArgs.startIndex] = commandPath
return childArgs
}

var args = CommandLine.arguments[1...]
let commandArgs = processArgs(&args)
_ = ScreenLockObserver(commandArgs)

RunLoop.main.run()

0 comments on commit 36bc0c1

Please sign in to comment.