Save and load session | v0.01

save the list of all opened sprites and load them all later with single click.

session-001
v0.01

don’t forget to create file named session in your scripts folder!
also: if you’re running linux or macos, you’ll need to set correct local path

-- SESSION 0.01 
-- b236 
-- 
-- save the list of all opened sprites and load them later all at once.
--  
-- to erase the session file, just hit save when no sprites are opened. 
-- ATTENTION: the file "session" has to be present in scripts folder! 


local path = os.getenv('APPDATA') .. "/Aseprite/scripts/" 
local sessionFile = "session" 

local s = {}

function getSprites() 
	for i,sprite in ipairs(app.sprites) do
	  table.insert(s,sprite.filename) 
	end 
end 

function saveSession() 
	local file = io.open(path .. sessionFile, "w+") 
	
	for i in pairs(s) do 
		file:write(s[i] .. "\n") 
	end 
	
	file:close() 
end 

function loadSession()	
	local file = io.open(path .. sessionFile, "r");
	local data = file:lines("*l") 
	
	for str in data do
		table.insert(s, str)
	end

	--for i in pairs(s) do 
	for i = #s, 1, -1 do
        app.open(s[i]) 
    end  
	
	file:close() 
end 

function clearList() 
	for i in pairs(s) do 
        s[i] = nil 
    end  
end 

local dlgWin = Dialog("*** Session 0.01 ***  ") 
	:button{ id="a", text="Save", onclick=function() 
		clearList() 
		getSprites() 
		saveSession() 
		clearList() 
	end} 
	:button{ id="b", text="Load", onclick=function() 
		clearList() 
		loadSession() 
		clearList() 
		end} 
	dlgWin:button{ id="e", text="Close", onclick=function() 
		dlgWin:close() 
	end} 
	
dlgWin:show{wait=false}

6 Likes

TODO:

  • work with multiple session files

Thanks for this! I never have less than a dozen tabs open at a time and having to deal with each one individually in the middle of a project just to restart my computer is anxiety inducing.

Does “session” file need a file type? and do I need to name the extension file anything in particular and is it *.lua? or *.aseprite-extension

-as a non programmer I am curious to why many people distribute their scripts in a copy/paste format instead of just posting the extension file itself?

hi! :]
session file doesn’t have extension and it’s meant to be in the same directory as other aseprite scripts.
you can name your script anything you want, but it has to be .lua file.
as long as it is in the aseprite directory it will show in the scripts menu under that file name. you can also attach shortcuts to scripts.

there are couple of reasons i prefer scripts. at least for me extensions are way too many extra steps for just a little bit of integration in gui. and more importantly, they are supposed to be polished, final products.
i’m not a programmer either and my scripts aren’t that - they are guaranteed to break if you don’t use them as you’re supposed to. :]] for instance: if the script needs a sprite to run, i’m almost certainly not checking whether any sprites are open. like here: List of active sprites
meanwhile proper developer would do all they can to save users from themselves.

but since it’s just a code, anyone can look into it and make any changes they wish. which might be a third reason: scripts are more inviting to start playing with code than extensions. there is no magic, no hidden tricks - it’s just a plain text and you can change it.

1 Like

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 :sweat_smile: 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 :slightly_smiling_face:

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()
3 Likes

that’s great, i’m happy to hear that! and thanks for sharing :]
i gave it a quick test and so far it seems to work fine. :+1:

i decided to not go after multiple sessions, because i realized that it will only lead to me having a mess of session files and that instead i should try to not have dozens of unfinished artworks. ;]]

1 Like