最も時間のかかっていたフォルダを探す処理をswiftでコマンド化しました。ストレスなく使える速さになっています。
もう一つ、待避フォルダアイコン用のicnsファイルが必要ですが、無くても動きます。
gitとかやるまでもないんだよねー、という場合ありますよね、ないですか。
単に待避フォルダに移動・コピーはAppleScriptでずっとしていたんですが、ちょっとだけ豪華にしました。
適当に待避用フォルダを作成して、ファイルを入れる毎に日時フォルダに分別してコピーまたは移動し、移動の理由も付けておきたいな、という時にもテキストファイルで理由を保存しておけます。
また、複数の待避フォルダがあっても別々に使用したり、一つにマージしたりできます。
これで動作を想像してください。
こんな感じになります。
気軽に使うためにはKeyboard Maestroのexecute AppleScriptで実行させましょう。
・icnsファイルは、ホームフォルダのPictures(写真?画像?元からあるものです)に入れてください
・scptは↑のようにスクリプトメニューからでもいいですが、Keyboard Maestroでマクロ作った方が楽しいです
※ほぼChatGPT4oですが、結構な時間かかりました。
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
property targetBaseFolderName : "■待避"
property dateTimeFormat : "%Y-%m-%d_%H-%M-%S"
property reasonFileName : "★待避理由.txt"
property newFolderTagName : "重要"
--メイン
tell application "Finder"
set myFirstSelection to selection
--選択しているか
if myFirstSelection is {} then
display alert "エラー" message "項目が選択されていません。" as critical
return
end if
--Basenamefolder自体を選択していないか
set mySelection to my filterOutTaibiFolders(myFirstSelection, targetBaseFolderName)
if mySelection is {} then
display alert "「" & targetBaseFolderName & "~」フォルダ以外を選択してください" as warning
return
end if
--検索処理:inside/none/パスリスト
set myParentFolder to container of item 1 of mySelection as alias
set myFindReturn to my findTargetBaseFolder(myParentFolder)
if myFindReturn is "inside" then
--待避フォルダ内からなら実行しない
display alert "処理中止" message "選択された項目が「" & targetBaseFolderName & "」フォルダ自体、またはその中にあるようです。" as warning
return
else if myFindReturn is "none" then
--待避フォルダがパス上に見つからないので新規作成
set myDialogRet to display dialog (myParentFolder as text) & "に対応した待避フォルダが見つかりません。作成しますか?" & return & "「■待避」にテキストを追加できます。" default answer "" buttons {"キャンセル", "作成"} default button "作成"
if button returned of myDialogRet is "キャンセル" then
return
else
set myFindReturn to my createTargetBaseFolder(myParentFolder, (text returned of myDialogRet))
end if
end if
--複数あるなら指定するかまとめる処理をして、単一フォルダパスを返す
set myFindRetrunList to myFindReturn as list
if (count of myFindRetrunList) > 1 then
set myFindReturn to my promptFolderSelection(myFindRetrunList)
end if
--親フォルダがなければ作成
set myParentFolder to name of myParentFolder
set myFindParent to myFindReturn & "/" & myParentFolder
if not (exists (POSIX file myFindParent as string)) then
do shell script "mkdir " & quoted form of myFindParent
end if
set myFindReturn to (POSIX file myFindParent) as alias
set b to myFindReturn
--移動・コピー処理選択
set myLinefeed to (linefeed & linefeed & linefeed & linefeed)
set dialogReturn to display dialog "移動またはコピーを選び、必要なら理由を入力してください" & return & return & "┌移動/コピー先" & return & " " & myFindParent & "/(日時フォルダ)/" default answer myLinefeed buttons {"キャンセル", "コピー", "移動"} default button "移動"
set returnButton to button returned of dialogReturn
set returnText to text returned of dialogReturn
if returnButton is "キャンセル" then return
--親フォルダ作成
--移動・コピー処理メイン
--set myFindReturn to (POSIX path of myFindReturn) as alias
--サブフォルダ作成
set subFolderName to do shell script "date +" & quoted form of dateTimeFormat
set mySubFolder to make new folder at myFindReturn with properties {name:subFolderName}
--移動・コピー
if returnButton is "コピー" then
duplicate mySelection to mySubFolder
else if returnButton is "移動" then
move mySelection to mySubFolder
end if
--ノート処理
set spaceFinder to ""
set spaceFinder to do shell script "echo " & quoted form of returnText & " | tr -d '[:space:]' | LANG=ja_JP sed -e 's/[[:space:]]//g'"
if spaceFinder is not "" then
set myReasonFilePath to POSIX path of (mySubFolder as alias) & reasonFileName
do shell script "echo " & quoted form of returnText & " > " & quoted form of myReasonFilePath
my tagOverwrite((POSIX file myReasonFilePath) as alias, newFolderTagName)
end if
end tell
on filterOutTaibiFolders(originalList, baseName)
set filteredList to {}
tell application "Finder"
repeat with i in originalList
try
set currentItem to i
set myName to name of currentItem
set myClass to class of currentItem
-- 除外条件: (名前が baseName で「始まり」) かつ (クラスが folder)
if not ((myName starts with baseName) and (myClass is folder)) then
set end of filteredList to currentItem
end if
on error errMsg number errNum
log "エラー処理中: " & errMsg
end try
end repeat
end tell
return filteredList
end filterOutTaibiFolders
on findTargetBaseFolder(startingFolder)
set startPath to POSIX path of (startingFolder as alias)
set mySCFind to do shell script "/usr/local/bin/findfolder " & quoted form of targetBaseFolderName & " " & quoted form of startPath
if mySCFind is "inside" then
return "inside"
else if mySCFind is "none" then
return "none"
else if mySCFind contains ":::" then
return my splitText(mySCFind, ":::")
else
return mySCFind
end if
end findTargetBaseFolder
on createTargetBaseFolder(referenceFolder, mySuffix)
-- 待避ベースフォルダ新規作成
-- フォルダ選択 + 作成
tell application "Finder"
if mySuffix is not "" then set mySuffix to ":" & mySuffix
set creationLocationRef to choose folder with prompt "「" & targetBaseFolderName & mySuffix & "」フォルダを作成する場所を選択してください:" default location referenceFolder as alias
if creationLocationRef is false then return
set newBaseFolderRef to make new folder at creationLocationRef with properties {name:targetBaseFolderName & mySuffix}
end tell
-- カスタムアイコンの存在確認(エラーにしない)
set iconPath to POSIX path of (path to pictures folder) & "hfi.icns"
try
set iconExists to POSIX file iconPath as alias
on error
tell me to display alert "ホームのピクチャフォルダに、「hfi.icns」ファイルを置くと、カスタムアイコンを付けられます。"
end try
-- タグ付け
my tagOverwrite(newBaseFolderRef, newFolderTagName)
-- カスタムアイコンの設定
try
set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:iconPath)
(current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:(POSIX path of (newBaseFolderRef as alias)) options:2)
end try
return POSIX path of (newBaseFolderRef as alias)
end createTargetBaseFolder
on promptFolderSelection(folderNames)
set chosenFolder to choose from list folderNames with prompt "同一階層に複数の待避フォルダが見つかりました" & return & "使用する待避フォルダを選んでください:" without multiple selections allowed
if chosenFolder is false then error number -128
set selectedName to item 1 of chosenFolder
set confirmDialog to display dialog "選択した待避フォルダ" & return & selectedName & return & return & "このフォルダを使用しますか?それとも全ての待避フォルダをこのフォルダにマージしますか?" & return & "※ 同一の時刻のフォルダがある場合は上書きされます" buttons {"キャンセル", "このフォルダにマージ", "このフォルダを使用"} default button "このフォルダを使用"
set clickedButton to button returned of confirmDialog
if clickedButton is "キャンセル" then error number -128
if clickedButton is "このフォルダにマージ" then
--dittoでマージ処理
set targetPath to (POSIX path of selectedName)
tell application "Finder"
repeat with i in folderNames
set sourcePath to (POSIX path of i)
if sourcePath is not equal to targetPath then
do shell script "/usr/bin/ditto " & quoted form of sourcePath & " " & quoted form of targetPath
try
delete (POSIX file sourcePath as alias)
end try
end if
end repeat
end tell
end if
return selectedName
end promptFolderSelection
on tagOverwrite(targetRef, tagName)
if targetRef is missing value or tagName is "" then return
try
set itemAlias to targetRef as alias
set itemPathText to POSIX path of itemAlias
set itemURL to current application's NSURL's fileURLWithPath:itemPathText
set tagKey to current application's NSURLTagNamesKey
itemURL's setResourceValue:{tagName} forKey:tagKey |error|:(missing value)
on error errMsg
tell me to display alert "タグ設定エラー" message errMsg as warning
end try
end tagOverwrite
on splitText(t, delimiter)
--分離処理
set AppleScript's text item delimiters to delimiter
set theList to text items of t
set AppleScript's text item delimiters to ""
return theList
end splitText
上のコードで呼んでいるコマンド
swiftc -o findfolder script.swift
でコマンドにして、chmod +x
し/usr/local/bin/に入れてください。
#!/usr/bin/env swift
import Foundation
let args = CommandLine.arguments
guard args.count == 3 else {
print("Usage: find_taibi <prefix> <startPath>")
exit(1)
}
let prefix = args[1].precomposedStringWithCanonicalMapping
// expand ~ and normalize path
let startPath = (args[2] as NSString).expandingTildeInPath
// remove trailing slash safely
let cleanedPath = startPath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
// now build URL
var currentURL = URL(fileURLWithPath: "/" + cleanedPath).standardizedFileURL
while true {
// Check if this folder itself matches
let currentName = currentURL.lastPathComponent.precomposedStringWithCanonicalMapping
if currentName.hasPrefix(prefix) {
print("inside")
exit(0)
}
do {
let contents = try FileManager.default.contentsOfDirectory(
at: currentURL,
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
)
let matches = contents.filter {
let name = $0.lastPathComponent.precomposedStringWithCanonicalMapping
return name.hasPrefix(prefix) &&
(try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true
}
if !matches.isEmpty {
let joined = matches.map { $0.path }.joined(separator: ":::")
print(joined)
exit(0)
}
} catch {
// エラーは無視
}
let parent = currentURL.deletingLastPathComponent().standardized
if parent.path == currentURL.path || currentURL.path == "/" {
break
}
currentURL = parent
}
print("none")
exit(0)
旧版
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
property targetBaseFolderName : "■待避:"
property searchDepthLimit : 4
property dateTimeFormat : "%y%m%d-%H%M%S"
property reasonFileName : "★待避理由.txt"
property newFolderTagName : "重要"
tell application "Finder"
try
set selectedItems to my getSelectedItems()
set sourceParentFolder to container of item 1 of selectedItems as alias
set sourceParentName to name of sourceParentFolder
if my isInsideTargetFolder(selectedItems, targetBaseFolderName) then
display alert "処理中止" message "選択された項目が「" & targetBaseFolderName & "」フォルダ自体、またはその中にあるようです。" as warning
return
end if
set targetBaseFolder to my findTargetBaseFolder(sourceParentFolder)
if targetBaseFolder is not missing value then
set targetBaseFolderName to name of targetBaseFolder
end if
if targetBaseFolder is missing value then
set userSuffix to my promptForFolderSuffix()
set targetBaseFolderName to targetBaseFolderName & userSuffix
set targetBaseFolder to my findTargetBaseFolder(sourceParentFolder)
if targetBaseFolder is missing value then
set targetBaseFolder to my createTargetBaseFolder(sourceParentFolder)
end if
end if
set {chosenAction, reasonText} to my promptForAction(sourceParentName, targetBaseFolder)
if targetBaseFolder is missing value then
display alert "エラー" message "ターゲットフォルダが見つからず、処理を続行できません。" as critical
return
end if
my ensureFolderTagOverwrite(targetBaseFolder, newFolderTagName)
set subFolder1 to my getOrCreateFolder(targetBaseFolder, sourceParentName)
set dateTimeFolderName to do shell script "date +" & quoted form of dateTimeFormat
set destinationFolder to my createFolder(subFolder1, dateTimeFolderName)
try
if chosenAction is "移動" then
move selectedItems to destinationFolder
else if chosenAction is "コピー" then
duplicate selectedItems to destinationFolder
end if
if reasonText is not "" then my writeReasonFile(destinationFolder, reasonText)
on error errMsg
try
delete destinationFolder
end try
display alert "エラー" message chosenAction & "処理中にエラーが発生しました。" & return & errMsg as critical
end try
end try
end tell
on getSelectedItems()
tell application "Finder"
set selectedList to selection
if selectedList is {} then
display alert "エラー" message "項目が選択されていません。" as critical
error number -128
end if
return selectedList
end tell
end getSelectedItems
on isInsideTargetFolder(targetItems, baseName)
if targetItems is {} then error number -128
tell application "Finder"
set firstSelectedItem to item 1 of targetItems
if name of firstSelectedItem starts with baseName then return true
set checkFolderRef to container of firstSelectedItem
repeat until false
try
if name of checkFolderRef starts with baseName then return true
set checkFolderRef to container of checkFolderRef
on error
exit repeat
end try
end repeat
end tell
return false
end isInsideTargetFolder
on promptForAction(parentFolderName, targetFolder)
set targetFolderName to name of targetFolder
set initialAnswer to (linefeed & linefeed & linefeed & linefeed)
set dialogResult to display dialog "操作を選び、必要なら理由を入力してください:" & return & return & "移動/コピー先:" & return & "「" & targetFolderName & "」>「" & parentFolderName & "」>(日時フォルダ)" default answer initialAnswer buttons {"キャンセル", "コピー", "移動"} default button "移動"
if button returned of dialogResult is "キャンセル" then error number -128
set actionResult to button returned of dialogResult
set reasonResult to text returned of dialogResult
return {actionResult, reasonResult}
end promptForAction
on promptForFolderSuffix()
set suffixDialog to display dialog "待避フォルダがありません。作成しますか?" & return & "これから作成する待避フォルダ名に、テキストを追加できます。" default answer "" buttons {"キャンセル", "作成"} default button "作成"
if button returned of suffixDialog is "キャンセル" then
error number -128
else
return text returned of suffixDialog
end if
end promptForFolderSuffix
on findTargetBaseFolder(startingFolder)
tell application "Finder"
set currentFolderRef to startingFolder
repeat searchDepthLimit times
try
set folderList to folders of currentFolderRef
set foundFolders to {}
repeat with subFolderRef in folderList
if name of subFolderRef starts with targetBaseFolderName then set end of foundFolders to subFolderRef
end repeat
if (count of foundFolders) = 1 then
return item 1 of foundFolders
else if (count of foundFolders) > 1 then
set folderNames to {}
repeat with f in foundFolders
set end of folderNames to name of f
end repeat
set chosenName to my promptFolderSelection(folderNames)
if chosenName is false then return missing value
if class of chosenName is text then
repeat with f in foundFolders
if name of f is chosenName then return f
end repeat
else if class of chosenName is list then
set primaryFolderName to item 1 of chosenName
set primaryFolder to missing value
repeat with f in foundFolders
if name of f is primaryFolderName then
set primaryFolder to f
exit repeat
end if
end repeat
if primaryFolder is missing value then return missing value
repeat with f in foundFolders
set srcPath to POSIX path of (f as alias)
set dstPath to POSIX path of (primaryFolder as alias)
if srcPath is not equal to dstPath then
do shell script "/usr/bin/ditto " & quoted form of srcPath & " " & quoted form of dstPath
delete f
end if
end repeat
return primaryFolder
end if
end if
if class of container of currentFolderRef is desktop folder then exit repeat
set currentFolderRef to container of currentFolderRef
on error errMsg number errNum
if errNum is -128 then
error number -128
else
exit repeat
end if
end try
end repeat
end tell
return missing value
end findTargetBaseFolder
on promptFolderSelection(folderNames)
set chosenFolder to choose from list folderNames with prompt "同一階層に複数の待避フォルダが見つかりました" & return & "使用する待避フォルダを選んでください:" without multiple selections allowed
if chosenFolder is false then error number -128
set selectedName to item 1 of chosenFolder
set confirmDialog to display dialog "選択した待避フォルダ: " & selectedName & return & return & "このフォルダを使用しますか?それとも全ての待避フォルダをこのフォルダにマージしますか?" & return & "※ 同一の時刻のフォルダがある場合は上書きされます" buttons {"キャンセル", "全てマージ", "このフォルダを使用"} default button "このフォルダを使用"
set clickedButton to button returned of confirmDialog
if clickedButton is "キャンセル" then error number -128
if clickedButton is "このフォルダを使用" then return selectedName
if clickedButton is "全てマージ" then return chosenFolder
error number -128
end promptFolderSelection
on createTargetBaseFolder(referenceFolder)
try
set parentFolderRef to container of referenceFolder
on error
set parentFolderRef to referenceFolder
end try
set parentFolderAlias to parentFolderRef as alias
set creationLocationRef to choose folder with prompt "「" & targetBaseFolderName & "」フォルダを作成する場所を選択してください:" default location parentFolderAlias
if creationLocationRef is false then error number -128
tell application "Finder"
set newBaseFolderRef to make new folder at creationLocationRef with properties {name:targetBaseFolderName}
end tell
try
set iconPath to POSIX path of (path to pictures folder) & "hfi.icns"
set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:iconPath)
(current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:(POSIX path of (newBaseFolderRef as alias)) options:2)
on error
-- skip icon error
end try
return newBaseFolderRef
end createTargetBaseFolder
on createFolder(parentRef, folderLabel)
if parentRef is missing value or folderLabel is "" then
error number -128
end if
tell application "Finder"
return make new folder at parentRef with properties {name:folderLabel}
end tell
end createFolder
on writeReasonFile(destinationRef, userReasonText)
if destinationRef is missing value then error number -128
try
set trimmedReasonText to do shell script "echo " & quoted form of userReasonText & " | tr -d '[:space:]'"
if trimmedReasonText is "" then return
set reasonFileFullPath to POSIX path of (destinationRef as alias) & reasonFileName
do shell script "echo " & quoted form of userReasonText & " > " & quoted form of reasonFileFullPath
set urlClass to current application's NSURL
set tagKey to current application's NSURLTagNamesKey
set fileURL to urlClass's fileURLWithPath:reasonFileFullPath
fileURL's setResourceValue:{newFolderTagName} forKey:tagKey |error|:(missing value)
on error errMsg
display alert "警告" message "理由ファイルの作成またはタグ付けに失敗しました。" & return & errMsg as warning
end try
end writeReasonFile
on ensureFolderTagOverwrite(targetFolderRef, tagName)
if targetFolderRef is missing value or tagName is "" then error number -128
try
set folderAlias to targetFolderRef as alias
set folderPathText to POSIX path of folderAlias
set folderURL to current application's NSURL's fileURLWithPath:folderPathText
set tagKey to current application's NSURLTagNamesKey
folderURL's setResourceValue:{tagName} forKey:tagKey |error|:(missing value)
on error errMsg
display alert "タグ設定エラー" message errMsg as warning
end try
end ensureFolderTagOverwrite
on getOrCreateFolder(parentRef, childName)
if parentRef is missing value or childName is "" then error number -128
try
set parentAlias to parentRef as alias
tell application "Finder"
try
return folder childName of folder parentAlias
on error
return make new folder at folder parentAlias with properties {name:childName}
end try
end tell
on error errMsg
display alert "フォルダ作成エラー" message errMsg as warning
return missing value
end try
end getOrCreateFolder