Giter VIP home page Giter VIP logo

paperwm.spoon's Introduction

PaperWM.spoon

Tiled scrollable window manager for MacOS. Inspired by PaperWM.

Spoon plugin for HammerSpoon MacOS automation app.

Demo

paperwm_spoon_demo.mp4

Installation

  1. Clone to Hammerspoon Spoons directory: git clone https://github.com/mogenson/PaperWM.spoon ~/.hammerspoon/Spoons/PaperWM.spoon.

  2. Open System Preferences -> Mission Control. Uncheck "Automatically rearrange Spaces based on most recent use" and check "Displays have separate Spaces".

Screen Shot 2022-01-07 at 14 10 11

Usage

Add the following to your ~/.hammerspoon/init.lua:

PaperWM = hs.loadSpoon("PaperWM")
PaperWM:bindHotkeys({
    -- switch to a new focused window in tiled grid
    focus_left  = {{"alt", "cmd"}, "left"},
    focus_right = {{"alt", "cmd"}, "right"},
    focus_up    = {{"alt", "cmd"}, "up"},
    focus_down  = {{"alt", "cmd"}, "down"},

    -- move windows around in tiled grid
    swap_left  = {{"alt", "cmd", "shift"}, "left"},
    swap_right = {{"alt", "cmd", "shift"}, "right"},
    swap_up    = {{"alt", "cmd", "shift"}, "up"},
    swap_down  = {{"alt", "cmd", "shift"}, "down"},

    -- position and resize focused window
    center_window        = {{"alt", "cmd"}, "c"},
    full_width           = {{"alt", "cmd"}, "f"},
    cycle_width          = {{"alt", "cmd"}, "r"},
    reverse_cycle_width  = {{"ctrl", "alt", "cmd"}, "r"},
    cycle_height         = {{"alt", "cmd", "shift"}, "r"},
    reverse_cycle_height = {{"ctrl", "alt", "cmd", "shift"}, "r"},

    -- move focused window into / out of a column
    slurp_in = {{"alt", "cmd"}, "i"},
    barf_out = {{"alt", "cmd"}, "o"},

    -- switch to a new Mission Control space
    switch_space_l = {{"alt", "cmd"}, ","},
    switch_space_r = {{"alt", "cmd"}, "."},
    switch_space_1 = {{"alt", "cmd"}, "1"},
    switch_space_2 = {{"alt", "cmd"}, "2"},
    switch_space_3 = {{"alt", "cmd"}, "3"},
    switch_space_4 = {{"alt", "cmd"}, "4"},
    switch_space_5 = {{"alt", "cmd"}, "5"},
    switch_space_6 = {{"alt", "cmd"}, "6"},
    switch_space_7 = {{"alt", "cmd"}, "7"},
    switch_space_8 = {{"alt", "cmd"}, "8"},
    switch_space_9 = {{"alt", "cmd"}, "9"},

    -- move focused window to a new space and tile
    move_window_1 = {{"alt", "cmd", "shift"}, "1"},
    move_window_2 = {{"alt", "cmd", "shift"}, "2"},
    move_window_3 = {{"alt", "cmd", "shift"}, "3"},
    move_window_4 = {{"alt", "cmd", "shift"}, "4"},
    move_window_5 = {{"alt", "cmd", "shift"}, "5"},
    move_window_6 = {{"alt", "cmd", "shift"}, "6"},
    move_window_7 = {{"alt", "cmd", "shift"}, "7"},
    move_window_8 = {{"alt", "cmd", "shift"}, "8"},
    move_window_9 = {{"alt", "cmd", "shift"}, "9"}
})
PaperWM:start()

Feel free to customize hotkeys or use PaperWM:bindHotkeys(PaperWM.default_hotkeys) for defaults. PaperWM actions are also available for manual keybinding via the PaperWM.actions table; for example, the following would enable navigation by either arrow keys or vim-style h/j/k/l directions:

PaperWM = hs.loadSpoon("PaperWM")
PaperWM:bindHotkeys(PaperWM.default_hotkeys)

hs.hotkey.bind({"ctrl", "alt", "cmd"}, "h", PaperWM.actions.focus_left)
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "j", PaperWM.actions.focus_down)
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "k", PaperWM.actions.focus_up)
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "l", PaperWM.actions.focus_right)

hs.hotkey.bind({"ctrl", "alt", "cmd", "shift"}, "h", PaperWM.actions.swap_left)
hs.hotkey.bind({"ctrl", "alt", "cmd", "shift"}, "j", PaperWM.actions.swap_down)
hs.hotkey.bind({"ctrl", "alt", "cmd", "shift"}, "k", PaperWM.actions.swap_up)
hs.hotkey.bind({"ctrl", "alt", "cmd", "shift"}, "l", PaperWM.actions.swap_right)

PaperWM:start() will begin automatically tiling new and existing windows. PaperWM:stop() will release control over windows.

Set PaperWM.window_gap to the number of pixels to space between windows and the top and bottom screen edges.

Configure one or many PaperWM.window_filter:rejectApp("appName") to ignore specific applications. For example:

PaperWM.window_filter:rejectApp("iStat Menus Status")
PaperWM.window_filter:rejectApp("Finder")
PaperWM:start() -- restart for new window filter to take effect

Set PaperWM.window_ratios to the ratios to cycle window widths and heights through. For example:

PaperWM.window_ratios = { 0.23607, 0.38195, 0.61804 }

Limitations

MacOS does not allow a window to be moved fully off-screen. Windows that would be tiled off-screen are placed in a margin on the left and right edge of the screen. They are still visible and clickable.

It's difficult to detect when a window is dragged from one space or screen to another. Use the move_window_N commands to move windows between spaces and screens.

Arrange screens vertically to prevent windows from bleeding into other screens.

Screen Shot 2022-01-07 at 14 18 27

Add-ons

The following Spoons compliment PaperWM.spoon nicely.

  • ActiveSpace.spoon Show active and layout of Mission Control spaces in the menu bar.
  • Swipe.spoon Perform actions when trackpad swipe gestures are recognized. Here's an example config to change PaperWM.spoon focused window:
-- focus adjacent window with 3 finger swipe
local current_id, threshold
Swipe = hs.loadSpoon("Swipe")
Swipe:start(3, function(direction, distance, id)
    if id == current_id then
        if distance > threshold then
            threshold = math.huge -- trigger once per swipe

            -- use "natural" scrolling
            if direction == "left" then
                PaperWM.actions.focus_right()
            elseif direction == "right" then
                PaperWM.actions.focus_left()
            elseif direction == "up" then
                PaperWM.actions.focus_down()
            elseif direction == "down" then
                PaperWM.actions.focus_up()
            end
        end
    else
        current_id = id
        threshold = 0.2 -- swipe distance > 20% of trackpad size
    end
end)

Contributing

Contributions are welcome! Here are a few preferences:

  • Global variables are CamelCase (eg. PaperWM)
  • Local variables are snake_case (eg. local focused_window)
  • Function names are lowerCamelCase (eg. function windowEventHandler())
  • Use <const> where possible
  • Create a local copy when deeply nested members are used often (eg. local Watcher <const> = hs.uielement.watcher)

Code format checking and linting is provided by lua-language-server for commits and pull requests. Run lua-language-server --check=init.lua locally before commiting.

paperwm.spoon's People

Contributors

ambirdsall avatar bttnns avatar du-song avatar jvns avatar mogenson avatar slarwise avatar vitalygashkov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

paperwm.spoon's Issues

Feature request: floating layer

Thanks for the excellent window manager!

Are there any plans to add a floating layer in a similar way to PaperWM for Gnome?

Respect window gap when computing window widths

Hi!

When using a window gap, windows that seem like they should fit the whole screen perfectly if they are next to each other result in some scrolling when switching between them. Could the window gap be taken into consideration so that there is no scrolling motion?

For example, assume we have

  • Two windows
  • The first has the width ratio 0.38195
  • The second has the width ratio 0.61804
  • The window gap is 30px

Then I would like the windows to fill the screen nicely since 0.38195 + 0.61804 = 0.999... But instead the unfocused window bleed into the outer window gap resulting in some scrolling when switching between the windows. See demo below.

Screen.Recording.2023-09-15.at.21.09.08.mov

Happy to make a PR!

switching to or from firefox is slow

As the title says. When switching from or to firefox the whole experience is quite janky. It takes almost a second before the focus shifts to or from firefox.
During this second firefox' cpu spikes to 100%.

Reposition last active window to right corner

When you close the last window/column, e.g. last window to the right, the next active window is not repositioned, leaving unused space to the right side. Would be better if last active window moves to the right side of the screen. So workspace will be filled entirely.

Also, would be great to be able to change window sizes not only by predefined sizes, but with separate key bindings +/- both horizontally and vertically.

In addition, it would be helpful when you open only one window or close all windows except one, then the last opened window will fill the entire screen automatically.

Thank you all for last changes! Behaves a lot better now!

Exclude floating windows

Thank you! Looks great!
There must be a standard option to exclude floating windows from managing.

Terminal and Finder tabs are detected as unique windows and tiled with gaps

Open a new tab in a Terminal or Finder window (may also occur in other applications). That window will be moved right the width of the window. This is because we are detecting the new tab as a unique window with a width of the original window.

Figure out how to detect when a window is a tab and ignore it. Maybe compare window ID with the window list?

Moving to a Chrome window changes focus to other Chrome window on another monitor

This may be a MacOS problem that can't be solved.

How to reproduce

  1. Open up a chrome window on both monitors.
  2. While having a Chrome window open on one monitor, navigate away from the Chrome window using PaperWM.spoon.
  3. When navigating back to the Chrome window, MacOS changes focus to the Chrome window on the other monitor.

Obviously it would be nice if it didn't change focus to the other monitor, but again, I think this is core MacOS problem.

can't switch desktop on other display.

when I press switch_space_2 which is on my other display, mission control opens but doesn't actually give focus to the space. I can then only press escape, and focus returns to the previous space.

Space switches multiple times

2024-02-22 11:51:51: 11:51:51 ERROR:   LuaSkin: hs.timer callback error: ...merspoon.app/Contents/Resources/extensions/hs/spaces.lua:801: attempt to index a nil value (local 'child')
stack traceback:
	...merspoon.app/Contents/Resources/extensions/hs/spaces.lua:801: in function 'hs.spaces.gotoSpace'
	/Users/mogenson/.hammerspoon/Spoons/PaperWM.spoon/init.lua:249: in function </Users/mogenson/.hammerspoon/Spoons/PaperWM.spoon/init.lua:247>

from line local status, errMsg2 = child:performAction("AXPress") in

module.gotoSpace = function(...)
    local args = { ... }
    assert(#args == 1, "expected 1 argument")
    local spaceID = args[1]
    assert(math.type(spaceID) == "integer", "space id must be an integer")

    local screenUUID, screenID = module.spaceDisplay(spaceID), nil
    if not screenUUID then
        return nil, "space not found in managed displays"
    end
    for _, vScreen in ipairs(screen.allScreens()) do
        if screenUUID == vScreen:getUUID() then
            screenID = vScreen:id()
            break
        end
    end

    local count
    for i, vSpace in ipairs(module.spacesForScreen(screenUUID)) do
        if spaceID == vSpace then
            count = i
            break
        end
    end

    openMissionControl()
    local mcSpacesList, errMsg = findSpacesSubgroup("mc.spaces.list", screenID)
    if not mcSpacesList then
        closeMissionControl()
        return nil, errMsg
    end

    -- delay to make sure Mission Control has stabilized
    waitForMissionControl()

    local child = mcSpacesList[count]

    local status, errMsg2 = child:performAction("AXPress")
    if status then
        return true
    else
        closeMissionControl()
        return nil, errMsg2
    end
end

from line Spaces.gotoSpace(space) in

    elseif Spaces.spaceType(space) == "user" then
        leftClick(point) -- if there are no windows and the space is a user space then click
        -- MacOS will sometimes switch to a another space with a focused window
        -- Setup a timer to check that the requested space stays active
        local function spaceCheck()
            if space ~= Spaces.focusedSpace() then
                Spaces.gotoSpace(space)
                leftClick(point)
            end
        end
        for i = 1, 3 do Timer.doAfter(i * Window.animationDuration, spaceCheck) end
    end

Configurable window widths

Thanks for an awesome window manager! Is it possible to make the window widths configurable? I use my 13 inch macbook pro and for me, the old window widths are more usable compared to the golden ratio ones introduces in #17.

Is it possible to make these configurable? Or perhaps even dynamic, depending on the screen size?

By old window widths, I mean these ratios:

{ 0.38195, 0.5, 0.61804 }.

`attempt to index a nil value` in function `xpcall`

I just installed PaperVM.spoon. I am getting the following error:

2022-10-15 21:13:40: -- Lazy extension loading enabled
2022-10-15 21:13:40: -- Loading ~/.hammerspoon/init.lua
2022-10-15 21:13:40: -- Loading Spoon: PaperWM
2022-10-15 21:13:40: -- Loading extension: window
2022-10-15 21:13:40: -- Loading extensions: window.filter
2022-10-15 21:13:40: -- Loading extension: logger
2022-10-15 21:13:40: -- Loading extension: spaces
2022-10-15 21:13:40: *** ERROR: /Users/dteiml/.hammerspoon/Spoons/PaperWM.spoon/init.lua:437: attempt to index a nil value
stack traceback:
	/Users/dteiml/.hammerspoon/Spoons/PaperWM.spoon/init.lua:437: in function 'PaperWM.addWindow'
	/Users/dteiml/.hammerspoon/Spoons/PaperWM.spoon/init.lua:389: in function 'PaperWM.refreshWindows'
	/Users/dteiml/.hammerspoon/Spoons/PaperWM.spoon/init.lua:182: in function 'PaperWM.start'
	/Users/dteiml/.hammerspoon/init.lua:248: in main chunk
	[C]: in function 'xpcall'
	...poon.app/Contents/Resources/extensions/hs/_coresetup.lua:723: in function 'hs._coresetup.setup'
	(...tail calls...)

I get this for both the default keybindings, as well as just for:

-- ~/.hammerspoon/init.lua
PaperWM = hs.loadSpoon("PaperWM")
PaperWM:start()

Issue getting it to work

Hi,

I'm facing issues getting it to work.

2022-02-28 17:30:52: *** ERROR: ...n.app/Contents/Resources/extensions/hs/window_filter.lua:1530: attempt to index a nil value (field 'watcher')
stack traceback:
	...n.app/Contents/Resources/extensions/hs/window_filter.lua:1530: in main chunk
	[C]: in function 'rawrequire'
	...poon.app/Contents/Resources/extensions/hs/_coresetup.lua:662: in function 'require'
	(...tail calls...)
	[C]: in function 'rawrequire'
	...poon.app/Contents/Resources/extensions/hs/_coresetup.lua:662: in function 'require'
	...merspoon.app/Contents/Resources/extensions/hs/window.lua:1027: in function <...merspoon.app/Contents/Resources/extensions/hs/window.lua:1025>
	(...tail calls...)
	/Users/mgaber/.hammerspoon/Spoons/PaperWM.spoon/init.lua:58: in main chunk
	[C]: in function 'rawrequire'
	...poon.app/Contents/Resources/extensions/hs/_coresetup.lua:662: in function 'require'
	...poon.app/Contents/Resources/extensions/hs/_coresetup.lua:337: in field 'loadSpoon'
	/Users/mgaber/.hammerspoon/init.lua:107: in main chunk
	[C]: in function 'xpcall'
	...poon.app/Contents/Resources/extensions/hs/_coresetup.lua:723: in function 'hs._coresetup.setup'
	(...tail calls...)

Running 12.2.1 on M1Pro

I'm very new to Darwin, not able to trace it back

Configurable screen margin

Hi :) What do you think about making the screen margin, a.k.a. the margin where "off-screen" windows are placed, configurable? I think it looks pretty nice with a margin that is smaller than the window gap. Here is an example with window gap being 30 and the screen margin 10.

Screenshot 2023-09-23 at 13 56 03

Expose user-accessible action references to enable creating redundant keybindings

Most of the time, I like to use vim-style home row directions for navigating the windows; but I also appreciate how the GNOME PaperWM lets me have this and sideswipe gestures (I'm not aware of a simple hammerspoon API for scripting gestures, but I could easily see one getting added in the future) and use the arrow keys (for example, when carrying my laptop around and using the keyboard one-handed).

Right now, I'm able to create these duplicate bindings manually, by using code like

-- in fact, I use fennel for my config, so there's a small chance this code snippet is incorrect
hs.hotkey.bind({{"cmd", "alt"}, "left"}, function () PaperWM:focusWindow(-1) end)

But this approach is undocumented, hard to read, and it generally feels brittle and hacky. Ideally, this would be unnecessary (see Hammerspoon/hammerspoon#3569), but for the time being, it would be very nice if the definitions of preconfigured user actions (i.e. the function bindings for partially-applied methods created inside PaperWM:bindHotkeys) were moved to somewhere user accessible. I was thinking a property of the module table akin to PaperWM.default_hotkeys; does this sound reasonable?

I'm happy to create a PR for this—in fact, I intend to write the patch regardless, for my own benefit, but it would be nice to not have to rebase upstream changes and deal with potential conflicts in perpetuity.

Automatically move windows when 1 window is minimised

How to reproduce

  1. Open at least 3 windows.
  2. Move to the far left window.
  3. Minimise the 2nd window (directly to the right of the currently focused far left window).
  4. Observe that the 3rd window doesn't automatically move to be directly next to the first window unless you navigate.

Also seems you can't navigate between tiles with the keyboard when you un-minimise a window, since it gets focused any for some reason ignores any PaperWM.spoon key bindings.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.