Skip to content

feat: restore windows to original position & size after PaperWM stop#155

Open
dev24hrs wants to merge 2 commits into
mogenson:mainfrom
dev24hrs:windowRestore_afterStop
Open

feat: restore windows to original position & size after PaperWM stop#155
dev24hrs wants to merge 2 commits into
mogenson:mainfrom
dev24hrs:windowRestore_afterStop

Conversation

@dev24hrs
Copy link
Copy Markdown
Contributor

@dev24hrs dev24hrs commented Apr 4, 2026

Hi,
I had a new idea during use. When i stop paperwm or quit hammerspoon,windows released control, but not restored to original position & size after stop or quit.
So i submitted this request. I'm not sure if this request is necessary.

The following are the relevant code changes.

New Files window_restore.lua

Core module with two functions:

  • saveWindowFrames() — Iterates all windows managed by PaperWM
    and saves each window's {x, y, w, h}, window ID,
    and a stable bundleID|title key to both an in-memory snapshot (_saved)
    and hs.settings (key: PaperWM_saved_frames)
  • restoreWindowFrames() — Restores each window's frame by matching on window ID first (same session),
    falling back to bundleID|title (cross-session / after Hammerspoon restart).
    Clears the in-memory snapshot after restore

Modified Files
init.lua

  • load window_restore.lua and call init()
  • start() - call saveWindowFrames() before state.clear()
  • stop() - call restoreWindowFrames() after events.stop()

spec/mocks.lua
Fixed the setFrame method in mock_window. The original function(new_frame) signature incorrectly received self as the first argument when called as win:setFrame(f), causing the
actual frame to be discarded. Fixed to:

  setFrame = function(self_or_frame, maybe_frame)
      frame = maybe_frame ~= nil and maybe_frame or self_or_frame
  end,

New Test File spec/window_restore_spec.lua

13 tests across two suites:

saveWindowFrames (6 tests)

  • Saves an empty list when there are no managed windows
  • Saves one entry per managed window
  • Records correct x, y, w, h coordinates
  • Stores the window ID for same-session matching
  • Stores a stable bundleID|title key for cross-session matching
  • In-memory snapshot is independent of frame mutations after saving

restoreWindowFrames (7 tests)

  • Does not call setFrame when no frames have been saved
  • Restores frames correctly by window ID (same session)
  • Restores frames correctly by stable key (cross-session)
  • Does not move windows that have no saved entry
  • Consumes duplicate stable keys in order so each window gets a distinct frame
  • Clears the in-memory snapshot after restore

So users can change their init.lua with this to restore windows to original position & size after PaperWM stop

modal:bind({}, "escape", function()
    PaperWM:stop()
    modal:exit()
end)

Comment thread spec/mocks.lua
-- Support both win:setFrame(f) (passes self as first arg) and win.setFrame(f)
setFrame = function(self_or_frame, maybe_frame)
frame = maybe_frame ~= nil and maybe_frame or self_or_frame
end,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for catching this. lets change the signature to setFrame = function(self, new_frame). we should only support the win:setFrame(f) form. win.setFrame(f) is not valid for hammerspoon

Comment thread init.lua
PaperWM.state.init(PaperWM)
PaperWM.floating.init(PaperWM)
PaperWM.tiling.init(PaperWM)
PaperWM.tiling.init(PaperWM)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trailing whitespace

Comment thread README.md
modal:bind({}, "l", nil, actions.focus_right)
modal:bind({}, "escape", function() modal:exit() end)
modal:bind({}, "escape", function()
PaperWM:stop()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel like PaperWM:stop() is needed for this modal example. Otherwise, when using this example the user would have to restart PaperWM.spoon every time the exit the modal layer.

Comment thread window_restore.lua
end

-- persist to settings for cross-session restore
hs.settings.set(SavedFramesKey, saved)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the SavedFramesKey setting is overwritten on every PaperWM start, does it need to persist through Hammerspoon launches? It seems like this could just be a regular old Lua table.

Comment thread window_restore.lua
local paperwm = WindowRestore.PaperWM

-- prefer in-memory snapshot (same session); fall back to persisted data
local saved = WindowRestore._saved or hs.settings.get(SavedFramesKey)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the comment above, if the SavedFramesKey is set on every PaperWM.spoon start, under what condition would SavedFramesKey be set but not WindowRestore._saved?

Comment thread init.lua
-- windowRestore_afterStop: restore window frames to their pre-tiling positions
self.window_restore.restoreWindowFrames()

-- ensure any window without a saved frame stays within screen bounds
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this filter out windows with a saved frame? This looks like it will loop over windows within SavedFramesKey / WindowRestore._saved tables again and possibly changed their frames to be within the screen bounds.

@mogenson
Copy link
Copy Markdown
Owner

mogenson commented Apr 5, 2026

Hi, I'm glad that this change is helpful for you, but I'm not sure about whether to make this the default behavior for every PaperWM.spoon user. I think that the setFrameInScreenBounds method in PaperWM:stop() solves the most important use case of not losing windows off screen when PaperWM.spoon stops. This window restore feature also doesn't track new windows added while PaperWM.spoon is running. Also, if a user moves a window to a new space / screen while PaperWM.spoon is running, I don't know if they want to keep it there when PaperWM.spoon stops or move it back to the cached position in WindowRestore._saved.

I think this could be a good stand alone addition to PaperWM.spoon, since it doesn't need access to the tiling internals. It just needs a new hotkey that calls saveWindowFrames() or restoreWindowFrames() before calling PaperWM:start() or PaperWM:stop(). Perhaps you could create a new Spoon and add it to the Add-Ons list.

@dev24hrs
Copy link
Copy Markdown
Contributor Author

dev24hrs commented Apr 6, 2026

@mogenson Hi, it's currently just my personal need, i'm not sure if it's necessary for every PaperWM.spoon user.The code runs well on my local machine, and I will continue to update it. If needed, I will make this feature as a standalone addition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants