Hey, I saw this and thought I’d give it a shot at improving it with the multiple sessions feature - I got a bit carried away though It should work pretty well, I’ve been testing it quite a bit during development.
Thanks for inspiring me to learn lua and Asprite scripting
Here’s the script, the details of the feature list are in the comments at the top:
-- Save/Load sessions v1-b
--
-- Save the list of all opened sprites and load them later all at once.
--
-- Save New Session: Saves the opened sprites to a new session list in "sessions"
--
-- Load: Opens the session from the selected session list
-- * Only sprites that are not already open from the session
--
-- Save: Overwrites the selected session list with the opened sprites
--
-- Delete: Removes the selected session list entry from "sessions"
--
-- Close: Closes the opened sprites that are in the selected session
-- * If any are unsaved, you are given the following options:
-- * Yes: Saves any unsaved sprites from the session list
-- before closing
-- * Only Close Saved: Closes only the unmodified sprites
-- the session list
-- * No: Closes all sprites from the session list, modified or not
-- * Cancel: Leaves all the current session sprites open
local SCRIPTS_PATH = os.getenv('APPDATA') .. "/Aseprite/scripts/"
local SESSION_FNAME = "sessions"
local SESSION_FPATH = SCRIPTS_PATH .. SESSION_FNAME
local DEFAULT_COMBO_VAL = "Select Session"
local function getFileNameFromPath(path)
return path:match("[\\/]([^\\/]+)$")
end
local function contains (tbl, value)
for i, v in ipairs(tbl) do
if v == value then
return true
end
end
return false
end
local function getOpenFilePaths()
local fPaths = {}
-- app.sprites is ordered from last to first opened, So collect
-- them in reverse order
-- note that once files are opened, changing the order of the tabs
-- will not affect the order in app.sprites
for i = #app.sprites, 1, -1 do
local sprite = app.sprites[i]
table.insert(fPaths, sprite.filename)
end
return fPaths
end
local function getOpenSprites()
local sprites = {}
for i, sprite in ipairs(app.sprites) do
table.insert(sprites, sprite)
end
return sprites
end
local function loadSessions(filePath)
local sessions = {}
local currentSessionName = nil
local file = io.open(filePath, "r")
if not file then
-- create if not exists
file = io.open(filePath, "w+")
end
for line in file:lines() do
if line:find("^session end") then
currentSessionName = nil
elseif line:find("^session") then
currentSessionName = line:match("^session%s+(.*)")
sessions[currentSessionName] = {}
elseif currentSessionName then
table.insert(sessions[currentSessionName], line)
end
end
file:close()
return sessions
end
local function appendSession(filePath, sessionName, sessionPaths)
local file = io.open(filePath, "a")
file:write("session " .. sessionName .. "\n")
for i, path in ipairs(sessionPaths) do
file:write(path .. "\n")
end
file:write("session end\n\n")
file:close()
end
local function saveSessions(filePath, sessions)
if not next(sessions) then
app.alert("Something went wrong; session(s) have not been saved.")
return
end
-- clear current sessions file
local file = io.open(filePath, "w")
file:close()
for name, paths in pairs(sessions) do
appendSession(filePath, name, paths)
end
end
local function getSessionNames(sessions)
if not sessions then
sessions = loadSessions(SESSION_FPATH)
end
local sessionNames = {}
for name, _ in pairs(sessions) do
table.insert(sessionNames, name)
end
table.sort(sessionNames)
return sessionNames
end
local function loadSession(sessionFPaths)
local anyOpened = false
local notFound = {}
local alreadyOpen = getOpenFilePaths()
for i, path in ipairs(sessionFPaths) do
if not contains(alreadyOpen, path) then
anyOpened = true
local file = io.open(path, "r")
if not file then
table.insert(notFound, getFileNameFromPath(path))
else
app.open(path)
end
end
end
if next(notFound) then
-- empty labels are used to force alignment
local failedToOpenDlg = Dialog("Failed to Open One or More Files")
:label{ label="" }
:label{ text="The following files could not be found" }
:label{ label="" }
:label{ text="and have likely been moved or deleted:" }
:label{ label="" }
:newrow()
for i, fileName in ipairs(notFound) do
failedToOpenDlg
:label{ text=" * " .. fileName }
:newrow()
end
failedToOpenDlg:newrow()
failedToOpenDlg
:label{ label="" }
:newrow()
:label{ label="" }
:label{ text="Save the session again to remove" }
:label{ label="" }
:label{ text="these files from the session." }
:label{ label="" }
failedToOpenDlg:button{ text="Ok" }
failedToOpenDlg:show()
elseif not anyOpened then
local desc = "All files in the session are already open."
if not next(sessionFPaths) then
desc = "No files to load; Empty session."
end
Dialog("Nothing to Open")
:label{ text=desc }
:button{ text="Ok" }
:show()
end
end
local function saveNewSessionDlg(sessionsPath)
local function saveSession(saveDlg)
local sessionName = saveDlg.data.name
if sessionName == "" then
Dialog("No Name Entered")
:label{ text="Enter a name and press create, or press cancel to exit." }
:button{ text="Ok" }
:show()
return
end
local existingNames = getSessionNames()
if (contains(existingNames, sessionName)) then
Dialog("Session Already Exists")
:label{ text="A session already exists with the name \"" .. sessionName .. "\"." }
:button{ text="Ok" }
:show()
return
end
local openPaths = getOpenFilePaths()
appendSession(sessionsPath, sessionName, openPaths)
saveDlg:close()
refreshMainDialog(sessionName)
end
local dlg = Dialog("Save Session")
:label{ label="", text="New session name:" }
:entry{ id="name" }
dlg:button{ id="create", text="Create", onclick=function() saveSession(dlg) end }
:button{ text="Cancel" }
:show()
end
local function displaySessions(dlg, sessions, selected)
local defaultVal = DEFAULT_COMBO_VAL
selected = selected or defaultVal
local sessionNames = getSessionNames(sessions)
table.insert(sessionNames, 1, defaultVal)
dlg:combobox{
id="sessionCombo",
option = selected,
options = sessionNames,
onchange=function()
refreshMainDialog(dlg.data.sessionCombo)
end
}
end
local dlgMain = nil
function refreshMainDialog(selected)
if selected == nil then
selected = DEFAULT_COMBO_VAL
end
local prevBounds = nil
if dlgMain then
prevBounds = dlgMain.bounds
dlgMain:close()
end
local sessionsPath = SESSION_FPATH
local loadedSessions = loadSessions(sessionsPath)
local isSessionSelected = selected ~= DEFAULT_COMBO_VAL
local function setAndSaveSelectedSession(filePath, loaded)
local currentlyOpen = getOpenFilePaths()
if not next(currentlyOpen) then
local desc = "Nothing open to save to the current session; The session has not been saved."
if not next(loaded[selected]) then
desc = "Nothing open to save to the empty session."
end
Dialog("Nothing to Save")
:label{ text=desc }
:button{ text="Ok" }
:show()
else
loaded[selected] = getOpenFilePaths()
saveSessions(SESSION_FPATH, loaded)
end
end
local function deleteSelectedSession(filePath, loaded)
local data = Dialog("Confirm Delete")
:label{ text="Are you sure you want to delete the session \"" .. selected .. "\"?" }
:button{ id="confirm", text="Confirm" }
:button{ text="Cancel" }
:show().data
if (data.confirm) then
loaded[selected] = nil
saveSessions(SESSION_FPATH, loaded)
refreshMainDialog()
end
end
local function closeSelectedSession(selectedPaths)
local openSprites = getOpenSprites()
local anySessionFilesOpen = false
for i, sprite in ipairs(openSprites) do
if (contains(selectedPaths, sprite.filename)) then
anySessionFilesOpen = true
break
end
end
if not anySessionFilesOpen then
Dialog("No Open Session Files")
:label{ text="There are no open files from the selected session to close." }
:button{ text="Ok" }
:show()
end
local anyUnsaved = false
for i, sprite in ipairs(openSprites) do
if (contains(selectedPaths, sprite.filename)) then
if (sprite.isModified) then
anyUnsaved = true
break
end
end
end
if not anyUnsaved then
for i, sprite in ipairs(openSprites) do
if (contains(selectedPaths, sprite.filename)) then
sprite:close()
end
end
else
local data = Dialog("Unsaved changes")
:label{ text="Some of the files are currently unsaved - Save them before closing?" }
:button{ id="yes", text="Yes" }
:button{ id="closeSaved", text="Only Close Saved" }
:button{ id="no", text="No" }
:button{ text="Cancel" }
:show().data
if (data.yes) then
for i, sprite in ipairs(openSprites) do
if (contains(selectedPaths, sprite.filename)) then
if (sprite.isModified) then
sprite:saveAs(sprite.filename)
else
sprite:close()
end
end
end
elseif (data.closeSaved) then
for i, sprite in ipairs(openSprites) do
if (contains(selectedPaths, sprite.filename)) then
if (not sprite.isModified) then
sprite:close()
end
end
end
elseif (data.no) then
for i, sprite in ipairs(openSprites) do
if (contains(selectedPaths, sprite.filename)) then
sprite:close()
end
end
end
end
end
dlgMain = Dialog("Session Save/Load")
:newrow{ always=true }
:button{
text="Save New Session",
onclick=function()
saveNewSessionDlg(sessionsPath)
end
}
:separator()
displaySessions(dlgMain, loadedSessions, selected)
dlgMain
:button{
text="Load",
enabled=isSessionSelected,
onclick=function()
loadSession(loadedSessions[selected])
end
}
:button{
text="Save",
enabled=isSessionSelected,
onclick=function()
setAndSaveSelectedSession(SESSION_FPATH, loadedSessions)
end
}
:button{
text="Delete",
enabled=isSessionSelected,
onclick=function()
deleteSelectedSession(SESSION_FPATH, loadedSessions)
end
}
:button{
text="Close",
enabled=isSessionSelected,
onclick=function()
closeSelectedSession(loadedSessions[selected])
end
}
if prevBounds then
dlgMain.bounds = prevBounds
end
dlgMain:show{wait=false}
end
refreshMainDialog()