Skip to content

Commit 36bc0c1

Browse files
committed
feat: initial working version
1 parent f1adc2f commit 36bc0c1

File tree

4 files changed

+316
-1
lines changed

4 files changed

+316
-1
lines changed

.swift-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5.10

README.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,60 @@
11
# run-on-macos-screen-unlock
2-
A tiny Swift program to run a command whenever the screen unlocks
2+
3+
A tiny Swift program to run a command whenever the screen unlocks \
4+
(I use it for mounting remounting network shares after sleep)
5+
6+
```sh
7+
run-on-macos-screen-unlock ./examples/mount-network-shares.sh
8+
```
9+
10+
# Install
11+
12+
1. Download
13+
```sh
14+
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
15+
```
16+
2. Extract
17+
```sh
18+
tar xvf ./run-on-macos-screen-unlock-v1.0.0.tar.gz
19+
```
20+
3. Allow running even though it's unsigned
21+
```sh
22+
xattr -r -d com.apple.quarantine ./run-on-macos-screen-unlock
23+
```
24+
4. Move into your `PATH`
25+
```sh
26+
mv ./run-on-macos-screen-unlock ~/bin/
27+
```
28+
29+
# Build from Source
30+
31+
1. Install XCode Tools \
32+
(including `git` and `swift`)
33+
```sh
34+
xcode-select --install
35+
```
36+
2. Clone and enter the repo
37+
```sh
38+
git clone https://github.com/coolaj86/run-on-macos-screen-unlock.git
39+
pushd ./run-on-macos-screen-unlock/
40+
```
41+
3. Build with `swiftc`
42+
```sh
43+
swiftc ./run-on-macos-screen-unlock.swift
44+
```
45+
46+
# Release
47+
48+
1. Git tag and push
49+
```sh
50+
git tag v1.0.x
51+
git push --tags
52+
```
53+
2. Create a release \
54+
<https://github.com/coolaj86/run-on-macos-screen-unlock/releases/new>
55+
3. Tar and upload
56+
```sh
57+
tar cvf ./run-on-macos-screen-unlock-v1.0.x.tar ./run-on-macos-screen-unlock
58+
gzip ./run-on-macos-screen-unlock-v1.0.x.tar
59+
open .
60+
```

examples/mount-network-shares.sh

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/bin/sh
2+
set -e
3+
set -u
4+
5+
fn_mount_share() {
6+
a_smb_share="${1:-}"
7+
# ex: /usr/bin/osascript -e 'mount volume "smb://jon@truenas.local/TimeMachineBackups"'
8+
/usr/bin/osascript -e "mount volume \"${a_smb_share}\""
9+
}
10+
11+
fn_version() {
12+
echo "mount-macos-network-shares v1.0.0 (2024-08-19)"
13+
echo "Copyright AJ ONeal (MPL-2.0)"
14+
}
15+
16+
fn_help() {
17+
echo ""
18+
echo "USAGE"
19+
echo " mount-macos-network-shares [path-to-config]"
20+
echo ""
21+
echo "OPTIONS"
22+
echo " --help - print this message"
23+
echo " -V,--version - print the version"
24+
echo ""
25+
echo "CONFIG"
26+
echo " Default config file:"
27+
echo " ~/.config/macos-network-shares/urls.conf"
28+
echo ""
29+
echo " Example config file contents:"
30+
echo " smb://puter:secret@truenas.local/TimeMachineBackups"
31+
echo " smb://wifu@192.168.1.101/Family Photos"
32+
echo ""
33+
}
34+
35+
fn_mount_shares() {
36+
b_urls_file="${1}"
37+
while IFS= read -r b_share_url; do
38+
fn_mount_share "${b_share_url}"
39+
done < "${b_urls_file}"
40+
}
41+
42+
main() {
43+
if ! test -f ~/.config/macos-network-shares/urls.conf; then
44+
mkdir -p ~/.config/macos-network-shares/ || true
45+
chmod 0700 ~/.config/macos-network-shares || true
46+
touch ~/.config/macos-network-shares/urls.conf || true
47+
chmod 0600 ~/.config/macos-network-shares/urls.conf || true
48+
echo "#smb://user:pass@truenas.local/TimeMachineBackups" >> ~/.config/macos-network-shares/urls.conf || true
49+
fi
50+
51+
b_urls_file="${1-$HOME/.config/macos-network-shares/urls.conf}"
52+
case "${b_urls_file}" in
53+
--version | -V | version)
54+
fn_version
55+
exit 0
56+
;;
57+
--help | help)
58+
fn_help
59+
exit 0
60+
;;
61+
*) ;;
62+
esac
63+
64+
if ! test -e "${b_urls_file}" && grep -q -v -E '^\s*(#.*)?$' "${b_urls_file}"; then
65+
{
66+
echo ""
67+
echo "ERROR"
68+
echo " url list '${b_urls_file}' is empty or does not exist"
69+
echo ""
70+
fn_help
71+
} >&2
72+
fi
73+
74+
{
75+
echo "Network URLs List: ${b_urls_file}"
76+
} >&2
77+
78+
fn_mount_shares "${b_urls_file}"
79+
}
80+
81+
main "$@"

run-on-macos-screen-unlock.swift

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import Foundation
2+
3+
let name = (CommandLine.arguments[0] as NSString).lastPathComponent
4+
let version = "1.0.0"
5+
let build = "2024-08-19-001"
6+
7+
let versionMessage = """
8+
\(name) \(version) (\(build))
9+
10+
"""
11+
12+
let copyrightMessage = """
13+
Copyright 2024 AJ ONeal <aj@therootcompany.com>
14+
15+
"""
16+
17+
let helpMessage = """
18+
Runs a user-specified command whenever the screen is unlocked by
19+
listening for the "com.apple.screenIsUnlocked" event, using /usr/bin/command -v
20+
to find the program in the user's PATH (or the explicit path given), and then
21+
runs it with /usr/bin/command, which can run aliases and shell functions also.
22+
23+
USAGE
24+
\(name) [OPTIONS] <command> [--] [command-arguments]
25+
26+
OPTIONS
27+
--version, -V, version
28+
Display the version information and exit.
29+
--help, help
30+
Display this help and exit.
31+
32+
DESCRIPTION
33+
\(name) is a simple command-line tool that demonstrates how to handle
34+
version and help flags in a Swift program following POSIX conventions.
35+
36+
"""
37+
38+
signal(SIGINT) { _ in
39+
printForHuman("received ctrl+c, exiting...\n")
40+
exit(0)
41+
}
42+
43+
enum ScriptError: Error {
44+
case fileNotFound
45+
}
46+
47+
func printForHuman(_ message: String) {
48+
if let data = message.data(using: .utf8) {
49+
FileHandle.standardError.write(data)
50+
}
51+
}
52+
53+
func getCommandPath(_ command: String) -> String? {
54+
let commandv = Process()
55+
commandv.launchPath = "/usr/bin/command"
56+
commandv.arguments = ["-v", command]
57+
58+
let pipe = Pipe()
59+
commandv.standardOutput = pipe
60+
commandv.standardError = FileHandle.standardError
61+
62+
try! commandv.run()
63+
commandv.waitUntilExit()
64+
65+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
66+
guard let scriptPath = String(data: data, encoding: .utf8)?
67+
.trimmingCharacters(in: .whitespacesAndNewlines)
68+
else {
69+
return nil
70+
}
71+
72+
if commandv.terminationStatus != 0, scriptPath.isEmpty {
73+
return nil
74+
}
75+
76+
return scriptPath
77+
}
78+
79+
class ScreenLockObserver {
80+
var commandPath: String
81+
var commandArgs: ArraySlice<String>
82+
83+
init(_ commandArgs: ArraySlice<String>) {
84+
self.commandPath = commandArgs.first!
85+
self.commandArgs = commandArgs
86+
87+
let dnc = DistributedNotificationCenter.default()
88+
89+
_ = dnc.addObserver(forName: NSNotification.Name("com.apple.screenIsLocked"), object: nil, queue: .main) { _ in
90+
NSLog("notification: com.apple.screenIsLocked")
91+
}
92+
93+
NSLog("Waiting for 'com.apple.screenIsUnlocked' to run \(self.commandArgs)")
94+
_ = dnc.addObserver(forName: NSNotification.Name("com.apple.screenIsUnlocked"), object: nil, queue: .main) { _ in
95+
NSLog("notification: com.apple.screenIsUnlocked")
96+
self.runOnUnlock()
97+
}
98+
}
99+
100+
private func runOnUnlock() {
101+
let task = Process()
102+
task.launchPath = "/usr/bin/command"
103+
task.arguments = Array(commandArgs)
104+
task.standardOutput = FileHandle.standardOutput
105+
task.standardError = FileHandle.standardError
106+
107+
do {
108+
try task.run()
109+
} catch {
110+
printForHuman("Failed to run \(self.commandPath): \(error.localizedDescription)\n")
111+
if let nsError = error as NSError? {
112+
printForHuman("Error details: \(nsError)\n")
113+
}
114+
exit(1)
115+
}
116+
117+
task.waitUntilExit()
118+
}
119+
}
120+
121+
@discardableResult
122+
func removeItem(_ array: inout ArraySlice<String>, _ item: String) -> Bool {
123+
if let index = array.firstIndex(of: item) {
124+
array.remove(at: index)
125+
return true
126+
}
127+
return false
128+
}
129+
130+
func processArgs(_ args: inout ArraySlice<String>) -> ArraySlice<String> {
131+
var childArgs: ArraySlice<String> = []
132+
if let delimiterIndex = args.firstIndex(of: "--") {
133+
let childArgsIndex = delimiterIndex + 1
134+
childArgs = args[childArgsIndex...]
135+
args.removeSubrange(delimiterIndex...)
136+
}
137+
if removeItem(&args, "--help") || removeItem(&args, "help") {
138+
printForHuman(versionMessage)
139+
printForHuman("\n")
140+
printForHuman(helpMessage)
141+
printForHuman("\n")
142+
printForHuman(copyrightMessage)
143+
exit(0)
144+
}
145+
if removeItem(&args, "--version") || removeItem(&args, "-V") || removeItem(&args, "version") {
146+
printForHuman(versionMessage)
147+
printForHuman(copyrightMessage)
148+
exit(0)
149+
}
150+
151+
childArgs = args + childArgs
152+
guard childArgs.count > 0 else {
153+
printForHuman(versionMessage)
154+
printForHuman("\n")
155+
printForHuman(helpMessage)
156+
printForHuman("\n")
157+
printForHuman(copyrightMessage)
158+
exit(1)
159+
}
160+
161+
let commandName = childArgs.first!
162+
guard let commandPath = getCommandPath(commandName) else {
163+
printForHuman("ERROR:\n \(commandName) not found in PATH\n")
164+
exit(1)
165+
}
166+
167+
childArgs[childArgs.startIndex] = commandPath
168+
return childArgs
169+
}
170+
171+
var args = CommandLine.arguments[1...]
172+
let commandArgs = processArgs(&args)
173+
_ = ScreenLockObserver(commandArgs)
174+
175+
RunLoop.main.run()

0 commit comments

Comments
 (0)