--- === 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 객체를 반환합니다.