用Applescript 实现自定义的动态壁纸

1. 动态壁纸 Dynamic Wallpaper

讲到在苹果上整动态壁纸,就得先聊聊他自家的方案。最早苹果在macOS Mojave 里引入了这个概念,基于每天的不同时段去变化桌面的壁纸。其所用的技术是在设置壁纸时,用一个带有时间信息的Heic 格式的图片去设置成壁纸,里面包含了多张的tiff 图片及时间信息。到了时间就切换一下里面应用的图片。这个技术整挺好,又不占用资源又实用。有那么段时间你甚至可以自已做这样的图片去实现这样的功能,但现在,在Big Sur 上是不可能的啦。因为壁纸目录在 /System/Library/Desktop\ Pictures 这里。整个系统目录是只读的,所以你是不能自已加的。

讲到这里,能实现这个功能的 “官方渠道” 就已经 “此路不通” 了。得另想办法。

在查找资料前我就想着有没有可能用脚本去做这个事,毕竟Mac 是Unix 系的东西。有没有可能可以用Bash 脚本 加一些事件勾子来实现动态壁纸呢。

经查找,可以是可以的,但和想象中的有一点点不一样。

可以使用Applescript 来做,并用Launchctl 来实现定时调用。理论上来说应该也可以用Crontab+Bash 来实现调用,但在最新的系统上Cron 好像没啥作用。即使给了全盘文件的访问权限也不行。暂估计是因为苹果的安全设计导致的。另外用Lauchctl 有Cron 所没有的好处。下面再详细说。如果你是Linux 类的机器那估计是可以用Crontab+Bash 实现这个效果的。但我没试🤔

2. Applescript

那么Applescript 是什么?

上面是苹果开发官网的介绍。基本上可以理解成它就是苹果系统下的一个专门为苹果量身定制的脚本语言。

在有写其他语言的基础下,Applescript 的学习成本倒是不高。而且它非常类似英语的自然语言。所以总体上说还挺好学的。就是没有IDE 有点🌚 系统自带有个Applescript 编辑器(Script Editor),可以说基本没有Windows 下的记事本好用就是了。我学基本语法的时候就有过脚本的高亮卡Bug 了的情况,写着写着就不能写了。但是由于Applescript 不能用Vim 之类的编辑器打开所以也没啥选择。

在开始前我们需要准备好壁纸文件。我的想法是24小时每个小时变一次。一天由24张不一样的壁纸组成。你可以自已在一个地方定机定时拍摄一组图片或者在网上寻找别人的作品来做。

这里我是把文件放在了家目录下的图片里:

# Display X 就是指给第X个显示器用的。
# 图片名就是具体的小时数。
~/Pictures/Wallpapers/Display1/01.jpg

像这样:

我们要用到的脚本如下:

(*

Script By:
Puls Garney 08.11.2021

The Idea Came From:
https://github.com/pipwerks/OS-X-Wallpaper-Changer

Picture Locations:
~/Pictures/Wallpapers/Display1/01.jpg
~/Pictures/Wallpapers/Display2/03.jpg
~/Pictures/Wallpapers/Display3/24.jpg

*)

set useSameWallpaper to false

set wallpaperPath to "~/Pictures/Wallpapers/Display"

set currentPhoto to hours of (current date) as string

if ((the length of currentPhoto) < 2) then
    set currentPhoto to "0" & currentPhoto
end if

tell application "System Events"

    if (useSameWallpaper) then

        tell every desktop

            set wallpaper to wallpaperPath & "1/" & currentPhoto & ".jpg"

            set picture to POSIX file wallpaper

        end tell

    else

        set displays to a reference to every desktop

        repeat with counter from 1 to (count displays)

            try

                set wallpaper to (wallpaperPath & counter as string) & "/" & currentPhoto & ".jpg"

                set picture of item counter of displays to POSIX file wallpaper

            end try

        end repeat

    end if

end tell

下面我一段一段讲一下功能:

set useSameWallpaper to false

这句是个变量,如果你想在所有的显示器上都用同一组图片则可以改成true. 我想要每个都不一样所以我用了false.

set currentPhoto to hours of (current date) as string

if ((the length of currentPhoto) < 2) then
    set currentPhoto to "0" & currentPhoto
end if

然后我们获取到当前的系统时间的小时数,如果小时数的全长不到2位数就补全成2位,比如是两点钟就会是“02”,而不是“2”。

然后接下来我们用系统事件的方式,给广播不同的设置图片的事件。

tell every desktop

    set wallpaper to wallpaperPath & "1/" & currentPhoto & ".jpg"

    set picture to POSIX file wallpaper

end tell

如果是要全部显示器都显示同一组图片的。这里会全部采用第一组图片。

set displays to a reference to every desktop

repeat with counter from 1 to (count displays)

    try

        set wallpaper to (wallpaperPath & counter as string) & "/" & currentPhoto & ".jpg"

        set picture of item counter of displays to POSIX file wallpaper

    end try

end repeat

这里我们获取到所有的显示器实例。并按先前准备好的图片路径对每个不同的显示器设置不同的图片。

大致上,到这脚本就准备好了。如果有图片资源就可以试着手动跑一下了。在Applescript 编辑器里面直接运行就可以了。自带的编辑器叫Script Editor. 用Spotlight 就能找到。

或者你已经保存了脚本的话,也可以直接在命令行里用命令运行。

osascript ~/Pictures/Wallpapers/Wallpaper.scpt

如果运行有问题可以加个 log xxx 就可以打印日志调试。

现在我们有了图片和脚本。接下来就是要让他自已动起来。总不能我们人工地隔一段时间就开个编辑器运行一下吧🤣🤣

3. Launchctl

那么Lauchctl 是什么呢?

Launchctl 是Mac OS X 10.4 以后引进的一个统一的服务管理框架,可以启动、停止和管理守护进程、应用程序、进程和脚本等。他是通过plist 配置文件来指定执行周期和任务的。

主要的放置plist 的地方有:

~/Library/LaunchAgents           # 当前用户定义的任务
 /Library/LaunchAgents           # 系统管理员定义的任务
 /Library/LaunchDaemons          # 管理员定义的系统守护进程任务
 /System/Library/LaunchAgents    # 苹果定义的任务
 /System/Library/LaunchDaemons   # 苹果定义的系统守护进程任务

如果你熟悉Crontab, 那总体上它们就是类似的。plist 文件的可设置内容很多,用的是xml 语言。如果有兴趣可以自行了解更多,下面我只讲下我们在这个例子中需要用到的设置项。

我们到 ~/Library/LaunchAgents 下新建一个文件:

<!-- ~/Library/LaunchAgents/me.dynamic.wallpaper.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- Label唯一的标识 -->
    <key>Label</key>
    <string>me.dynamic.wallpaper.plist</string>

    <!-- 指定要运行的脚本 -->
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/osascript</string>
        <string>/Users/pulsgarney/Pictures/Wallpapers/Wallpaper.scpt</string>
    </array>

    <!-- 指定运行的时间 -->
    <key>StartCalendarInterval</key>
    <dict>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
</dict>
</plist>

上面我们这个文件定义了我们用osascript 去跑我们前面写好的脚本。每个小时的第1分钟开始时就跑一次,也就是一天24次。如果你的脚本有问题不能正常运行可以加上下面这两句打下日志以方便调试。

    <!-- 标准输出文件 -->
    <key>StandardOutPath</key>
    <string>/Users/pulsgarney/Pictures/Wallpapers/log/output.log</string>

    <!-- 标准错误输出文件,错误日志 -->
    <key>StandardErrorPath</key>
    <string>/Users/pulsgarney/Pictures/Wallpapers/log/error.log</string>

如果你想要它刷新的频率更高,可以加上这句让它定时刷新,单位为秒:

    <!-- 时间间隔 -->
    <key>StartInterval</key>
    <integer>300</integer>

接下来,我们就可以在命令行下加载这个任务了。

# 加载任务
launchctl load me.dynamic.wallpaper.plist
# 卸载任务
launchctl unload me.dynamic.wallpaper.plist

# 不管文件的时间设置,马上运行一次任务
launchctl start me.dynamic.wallpaper.plist

如果一切正常,那么在load 完后,每个小时都会自动更换壁纸。而且,用Lauchctl 比Crontab 有优势的一点就是:在你电脑没有在工作时所触发的任务它会在你重新运行电脑后整合成一个任务跑一次。

这个应用场景主要是比如我用完了电脑合上了屏幕,下次我打开它时,无论什么时间,只要我屏幕一翻开它就会更新我的壁纸,可以避免你半夜凌晨打开电脑结果你壁纸显示的是下午两三点大太阳的壁纸一亮屏就把人给送走了的烦恼。

今天就聊到这,掰~

03-05 15:09