fd17d81cb9
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.
114 lines
10 KiB
Lua
114 lines
10 KiB
Lua
--- === 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 객체를 반환합니다.
|