feat: add PicsumRandom wallpaper Spoon with hotkey

Implement a Hammerspoon Spoon that sets random Lorem Picsum wallpapers on a 3-hour automatic refresh interval. Add Ctrl+Alt+Cmd+W hotkey for manual immediate refresh with system conflict detection. Update main init.lua to load the Spoon, handle missing installation errors, and bind the hotkey. Set up cache directory management for downloaded wallpaper images and replace the prior Unsplash-based wallpaper setup.
This commit is contained in:
2026-06-26 10:07:16 +09:00
parent de5f09da95
commit fd17d81cb9
8 changed files with 136 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
--- === PicsumRandom === -- Lorem Picsum 기반으로 랜덤 배경화면을 적용하는 Spoon 이름을 선언합니다.
--- -- Spoon 설명 헤더 구분선입니다.
--- Automatically sets a random Lorem Picsum image as your wallpaper periodically. -- Spoon 동작 설명을 문서 헤더에 남깁니다.
--- -- Spoon 설명 헤더 구분선입니다.
--- Local custom Spoon for Hammerspoon. -- 로컬 사용자 정의 Spoon임을 문서 헤더에 남깁니다.
local obj = {} -- Spoon 객체 테이블을 생성합니다.
obj.__index = obj -- 메서드 조회를 위해 자기 자신을 메타테이블 인덱스로 지정합니다.
obj.name = "PicsumRandom" -- Spoon 이름을 정의합니다.
obj.version = "1.0" -- Spoon 버전을 정의합니다.
obj.author = "OpenAI" -- 현재 로컬 Spoon 작성자를 기록합니다.
obj.homepage = "https://picsum.photos/" -- 사용 중인 오픈소스 이미지 서비스 홈페이지를 기록합니다.
obj.license = "MIT" -- 로컬 Spoon 라이선스 표기를 남깁니다.
obj.maxPixelDimension = 4096 -- Picsum이 안정적으로 처리할 수 있도록 요청 최대 한 변 길이를 제한합니다.
obj.refreshIntervalSeconds = 3 * 60 * 60 -- 배경화면 자동 갱신 주기를 3시간으로 설정합니다.
obj.firstRefreshDelaySeconds = 5 -- 설정 로드 후 첫 갱신까지의 지연 시간을 5초로 설정합니다.
obj.cacheRootDirectory = hs.configdir .. "/cache" -- 다운로드 캐시의 상위 디렉터리 경로를 정의합니다.
obj.cacheDirectory = hs.configdir .. "/cache/PicsumRandom" -- 다운로드 이미지를 저장할 캐시 디렉터리 경로를 정의합니다.
obj.currentDownloadPath = nil -- 현재 다운로드 대상 파일 경로를 저장할 변수를 초기화합니다.
obj.task = nil -- 현재 실행 중인 다운로드 태스크를 저장할 변수를 초기화합니다.
obj.timer = nil -- 주기 실행 타이머 객체를 저장할 변수를 초기화합니다.
local function ensureCacheDirectory() -- 캐시 디렉터리가 없으면 생성하는 보조 함수를 정의합니다.
if hs.fs.attributes(obj.cacheRootDirectory) == nil then -- 상위 cache 디렉터리 존재 여부를 먼저 확인합니다.
hs.fs.mkdir(obj.cacheRootDirectory) -- 상위 cache 디렉터리가 없을 때 먼저 생성합니다.
end -- 상위 cache 디렉터리 생성 분기를 마칩니다.
if hs.fs.attributes(obj.cacheDirectory) == nil then -- 캐시 디렉터리 존재 여부를 먼저 확인합니다.
hs.fs.mkdir(obj.cacheDirectory) -- 캐시 디렉터리가 없을 때 생성합니다.
end -- 캐시 디렉터리 생성 분기를 마칩니다.
end -- 캐시 디렉터리 보장 함수 정의를 마칩니다.
local function buildImageRequest() -- 현재 메인 화면 크기에 맞는 Picsum 요청 정보를 생성하는 함수를 정의합니다.
local screenObject = hs.screen.mainScreen() -- 현재 메인 화면 객체를 가져옵니다.
local screenMode = screenObject:currentMode() -- 메인 화면의 현재 해상도와 배율 정보를 읽습니다.
local rawPixelWidth = math.floor(screenMode.w * screenMode.scale) -- 실제 픽셀 기준 원본 가로 길이를 계산합니다.
local rawPixelHeight = math.floor(screenMode.h * screenMode.scale) -- 실제 픽셀 기준 원본 세로 길이를 계산합니다.
local maxDimension = math.max(rawPixelWidth, rawPixelHeight) -- 원본 해상도에서 가장 긴 한 변 길이를 계산합니다.
local resizeRatio = 1 -- 기본적으로는 축소 없이 원본 비율을 유지하도록 배율을 초기화합니다.
if maxDimension > obj.maxPixelDimension then -- 요청 크기가 허용 상한을 넘는 경우를 처리합니다.
resizeRatio = obj.maxPixelDimension / maxDimension -- 가장 긴 변이 상한에 맞도록 축소 배율을 계산합니다.
end -- 요청 크기 상한 초과 여부 분기를 마칩니다.
local pixelWidth = math.max(1, math.floor(rawPixelWidth * resizeRatio)) -- 축소 배율을 반영한 최종 가로 길이를 계산합니다.
local pixelHeight = math.max(1, math.floor(rawPixelHeight * resizeRatio)) -- 축소 배율을 반영한 최종 세로 길이를 계산합니다.
local seedValue = tostring(hs.timer.absoluteTime()) -- 매번 다른 이미지를 받기 위해 현재 시간을 문자열 시드로 변환합니다.
local imageUrl = "https://picsum.photos/seed/" .. seedValue .. "/" .. tostring(pixelWidth) .. "/" .. tostring(pixelHeight) .. ".jpg" -- 화면 크기와 시드를 반영한 Picsum 이미지 URL을 생성합니다.
local filePath = obj.cacheDirectory .. "/wallpaper-" .. seedValue .. ".jpg" -- macOS 캐시를 피하기 위해 매 요청마다 고유한 저장 경로를 생성합니다.
return screenObject, imageUrl, filePath -- 화면 객체와 이미지 URL과 저장 경로를 함께 반환합니다.
end -- 이미지 요청 정보 생성 함수 정의를 마칩니다.
local function applyDownloadedWallpaper() -- 다운로드된 이미지를 배경화면으로 적용하는 보조 함수를 정의합니다.
if obj.currentDownloadPath == nil then -- 현재 다운로드 파일 경로가 없는 비정상 상태를 먼저 처리합니다.
hs.printf("PicsumRandom: download path is missing") -- 디버깅을 위해 경로 누락 상태를 콘솔에 기록합니다.
return -- 적용할 파일이 없으므로 함수를 종료합니다.
end -- 다운로드 경로 존재 여부 검사 분기를 마칩니다.
if hs.fs.attributes(obj.currentDownloadPath) == nil then -- 파일이 실제로 존재하는지 다시 확인합니다.
hs.printf("PicsumRandom: downloaded file not found at %s", obj.currentDownloadPath) -- 누락된 파일 경로를 콘솔에 기록합니다.
return -- 파일이 없으면 배경화면 적용을 중단합니다.
end -- 파일 존재 여부 검사 분기를 마칩니다.
hs.screen.mainScreen():desktopImageURL("file://" .. obj.currentDownloadPath) -- 다운로드된 이미지를 현재 메인 화면의 배경화면으로 적용합니다.
end -- 배경화면 적용 함수 정의를 마칩니다.
local function downloadCallback(exitCode, standardOutput, standardError) -- 다운로드 태스크 완료 후 호출될 콜백 함수를 정의합니다.
obj.task = nil -- 다운로드 완료 시 실행 중 태스크 참조를 먼저 정리합니다.
if exitCode == 0 then -- curl 실행이 성공한 경우를 처리합니다.
applyDownloadedWallpaper() -- 성공적으로 저장된 이미지를 배경화면으로 적용합니다.
else -- curl 실행이 실패한 경우를 처리합니다.
hs.printf("PicsumRandom download failed: %s %s", tostring(standardOutput), tostring(standardError)) -- 실패 원인을 콘솔에 기록합니다.
end -- 다운로드 성공 여부 분기를 마칩니다.
end -- 다운로드 완료 콜백 함수 정의를 마칩니다.
function obj:refresh() -- 새 랜덤 이미지를 즉시 받아 적용하는 공개 메서드를 정의합니다.
ensureCacheDirectory() -- 다운로드 전에 캐시 디렉터리가 준비되도록 보장합니다.
if self.task ~= nil then -- 기존 다운로드 태스크가 아직 실행 중인 경우를 처리합니다.
self.task:terminate() -- 중복 다운로드를 막기 위해 이전 태스크를 종료합니다.
self.task = nil -- 종료한 이전 태스크 참조를 정리합니다.
end -- 중복 다운로드 방지 분기를 마칩니다.
local _, imageUrl, filePath = buildImageRequest() -- 현재 화면에 맞는 이미지 URL과 저장 경로를 계산합니다.
self.currentDownloadPath = filePath -- 이번 다운로드의 저장 경로를 객체 상태에 보관합니다.
self.task = hs.task.new("/usr/bin/curl", downloadCallback, { "-L", "-f", "-sS", imageUrl, "-o", filePath }) -- 실패를 감지할 수 있도록 curl 다운로드 태스크를 생성합니다.
if self.task == nil then -- 태스크 생성 자체가 실패한 경우를 처리합니다.
hs.printf("PicsumRandom: failed to create curl task") -- 태스크 생성 실패를 콘솔에 기록합니다.
return -- 더 진행할 수 없으므로 함수를 종료합니다.
end -- 태스크 생성 성공 여부 분기를 마칩니다.
self.task:start() -- 생성된 다운로드 태스크를 즉시 시작합니다.
end -- 수동 새로고침 메서드 정의를 마칩니다.
function obj:start() -- 주기적 랜덤 배경화면 갱신을 시작하는 메서드를 정의합니다.
ensureCacheDirectory() -- 시작 전에 캐시 디렉터리가 존재하도록 보장합니다.
if self.timer == nil then -- 아직 타이머가 만들어지지 않은 첫 시작 상황을 처리합니다.
self.timer = hs.timer.doEvery(self.refreshIntervalSeconds, function() self:refresh() end) -- 설정한 주기마다 새 배경화면을 받는 타이머를 생성합니다.
else -- 이미 타이머가 존재하는 경우를 처리합니다.
self.timer:start() -- 기존 타이머를 다시 시작합니다.
end -- 타이머 생성 여부 분기를 마칩니다.
self.timer:setNextTrigger(self.firstRefreshDelaySeconds) -- 첫 배경화면 갱신 시점을 짧게 앞당겨 설정합니다.
end -- 자동 갱신 시작 메서드 정의를 마칩니다.
function obj:stop() -- 자동 갱신을 중단하는 메서드를 정의합니다.
if self.timer ~= nil then -- 생성된 타이머가 있는 경우만 처리합니다.
self.timer:stop() -- 주기 실행 타이머를 중지합니다.
end -- 타이머 중지 분기를 마칩니다.
if self.task ~= nil then -- 실행 중인 다운로드 태스크가 있는 경우를 처리합니다.
self.task:terminate() -- 진행 중 다운로드를 안전하게 중단합니다.
self.task = nil -- 종료한 태스크 참조를 정리합니다.
end -- 다운로드 태스크 정리 분기를 마칩니다.
end -- 자동 갱신 중지 메서드 정의를 마칩니다.
function obj:init() -- Spoon 로드 직후 초기화 메서드를 정의합니다.
self:start() -- Spoon이 로드되면 자동 갱신을 바로 시작합니다.
end -- Spoon 초기화 메서드 정의를 마칩니다.
return obj -- 완성된 Spoon 객체를 반환합니다.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

+23
View File
@@ -326,3 +326,26 @@ else -- Caffeine Spoon 로드에 실패한 경우 사용자에게 원인을 알
hs.alert.show("Caffeine.spoon 설치가 필요합니다") -- 누락된 Spoon 설치 필요 안내를 표시합니다. hs.alert.show("Caffeine.spoon 설치가 필요합니다") -- 누락된 Spoon 설치 필요 안내를 표시합니다.
hs.printf("Failed to load Caffeine spoon: %s", tostring(caffeineError)) -- 콘솔에 로드 실패 원인을 기록합니다. hs.printf("Failed to load Caffeine spoon: %s", tostring(caffeineError)) -- 콘솔에 로드 실패 원인을 기록합니다.
end -- Caffeine Spoon 로드 예외 처리 분기를 마칩니다. end -- Caffeine Spoon 로드 예외 처리 분기를 마칩니다.
-- 공식 UnsplashRandom 대신 PicsumRandom Spoon을 로드해 다른 오픈소스 이미지 소스로 배경화면을 갱신합니다.
local picsumRandomRefreshHotkeyModifiers = { "ctrl", "alt", "cmd" } -- 즉시 새 배경화면을 받는 단축키의 수정 키 조합을 정의합니다.
local picsumRandomRefreshHotkeyKey = "w" -- 즉시 새 배경화면을 받는 단축키의 기본 키를 정의합니다.
local picsumRandomRefreshHotkey = nil -- 실제로 등록된 즉시 새로고침 단축키 객체를 저장할 변수를 초기화합니다.
local picsumRandomLoaded, picsumRandomError = pcall(hs.loadSpoon, "PicsumRandom") -- PicsumRandom Spoon 로드와 초기화를 예외 안전하게 시도합니다.
if picsumRandomLoaded then -- PicsumRandom Spoon 로드에 성공한 경우를 처리합니다.
local picsumRandomRefreshSystemConflict = hs.hotkey.systemAssigned(picsumRandomRefreshHotkeyModifiers, picsumRandomRefreshHotkeyKey) -- 시스템이 같은 단축키를 이미 점유하고 있는지 먼저 확인합니다.
if picsumRandomRefreshSystemConflict then -- 시스템 단축키 충돌이 있는 경우를 처리합니다.
hs.alert.show("ctrl-alt-cmd-w 단축키가 시스템에 이미 할당되어 등록하지 않았습니다") -- 단축키 충돌 사실을 사용자에게 화면으로 알립니다.
hs.printf("Skipped binding PicsumRandom refresh hotkey due to system conflict: %s", hs.inspect(picsumRandomRefreshSystemConflict)) -- 충돌 세부 정보를 콘솔에 남겨 원인 파악을 돕습니다.
else -- 시스템 단축키 충돌이 없는 경우를 처리합니다.
picsumRandomRefreshHotkey = hs.hotkey.bind(picsumRandomRefreshHotkeyModifiers, picsumRandomRefreshHotkeyKey, function() -- 전역 단축키를 눌렀을 때 즉시 새 배경화면 갱신을 실행합니다.
hs.alert.show("새 배경화면을 가져오는 중입니다") -- 단축키가 정상 인식됐음을 사용자에게 즉시 알려 줍니다.
spoon.PicsumRandom:refresh() -- 현재 화면 크기에 맞는 새 랜덤 배경화면 다운로드를 바로 시작합니다.
end) -- 즉시 새 배경화면 갱신 단축키 바인딩 정의를 마칩니다.
end -- 시스템 단축키 충돌 여부에 따른 등록 분기를 마칩니다.
hs.printf("PicsumRandom spoon loaded successfully") -- 콘솔에 성공 로그를 남겨 정상 로드를 확인할 수 있게 합니다.
else -- PicsumRandom Spoon 로드에 실패한 경우 사용자에게 원인을 알립니다.
hs.alert.show("PicsumRandom.spoon 설치를 확인해 주세요") -- 누락되었거나 손상된 Spoon 설치 상태를 사용자에게 안내합니다.
hs.printf("Failed to load PicsumRandom spoon: %s", tostring(picsumRandomError)) -- 콘솔에 로드 실패 원인을 기록합니다.
end -- PicsumRandom Spoon 로드 예외 처리 분기를 마칩니다.