The World’s Most Comprehensive Guide to Implementing Programmatic Creation of Tabs in a Single NSWindowController That You Shouldn’t Adhere To

This is a follow-up to “Programmatically Add Tabs to NSWindows without NSDocument” from January this year. There, I was creating NSWindow instances from storyboards and re-used a single window controller to manage them. The point of that post was to solve the problem that newly created tabs will themselves not respond to the “+” button – or any menu action, for that matter, because they fell out of the responder chain at first.

This is a continuation of that idea. Please note that these are all experimental observations.

I do not recommend switching out a NSWindowController’s main window reference when switching tabs for the following reasons:

  1. As Dave DeLong pointed out, it feels wrong: you wouldn’t switch out a NSViewController.view but instead create a new view/controller combo. Same should go for windows.
  2. Here, the windows are empty; fully functional window controls will likely break in practice.
  3. You run into all kinds of weird behavor, as documented below.

Even though I do not recomment this approach, I understand people are looking for this and I can recall why I tried to do it this way myself. After all, the default tab creation selector is newWindowForTab(_:). It can be implemented anywhere in the responder chain between the window (which displays the tab bar) and the AppDelegate (which is probably the last responder you have control over). I presume that most apps don’t have many NSResponder objects between AppDelegate and NSWindowControllers, so implementing the callback in your window controllers is an idea that comes natural.

Keep in mind that implementing the callback in the NSWindowController does not mean you need to implement everything related to tab creation there! You can inform a delegate that manages multiple controllers about the event. I’ll write about a better approach in an upcoming post.

Experimental observations

Still interested in the experiment?

For the scope of this exploration into tabs, we will work with this implementation of newWindowForTab(_:), which is called when you hit the “+” button in a window’s tab bar. I put it into a window controller.

  • It shares a single window controller among all tabs.
  • It treats NSWindowController.window as the “root window” and adds tabs to it. We’ll talk about that label in a minute.
  • It activates the newly added tab immediately.
override func newWindowForTab(_ sender: Any?) {

    guard let rootWindow = self.window else {
        preconditionFailure("Expected window to be loaded")
    }

    let newWindowController = self.storyboard!.instantiateInitialController() as! WindowController
    let newWindow = newWindowController.window!

    // Add as a new tab to the right of the original window
    mainKeyWindow.addTabbedWindow(newWindow, ordered: .above)
    newWindow.delegate = self // Set delegate first to notify about key window change
    newWindow.makeKeyAndOrderFront(self)

    // `newWindowController` is not referenced and will be deallocated at the end of
    // this method, so use 1 shared controller to keep the window in the responder chain.
    newWindow.windowController = self
}

Windows without a tab bar will not always have a tab group

To maybe nobody’s surprise, this is the effect of NSWindow.TabbingMode on a sole NSWindow:

  • .disallowed: both the window’s tabbedWindows array and the tabGroup are nil.
  • .preferred: the tabbedWindows array contains 1 element, the window itself, and the tabGroup is non-nil.
  • .automatic: the window’s tabbedWindows array is nil if the tab bar is hidden, but tabGroup is non-nil. If you show the tab bar, it’s the same as starting with .preferred.

When a window is tab-able, it has a NSWindowTabGroup. When it shows tabs, its tabbedWindows array will show the visible tabs.

But even if tabbedWindows is nil, the tabGroup.windows will contain 1 element, the window itself!

So we can think of tabbedWindows as the visible tabs, or a representation of the view state, while tabGroup contains the “tab model”.

The concrete type of AppKit’s default tabGroup instance if NSWindowStackController, by the way, which appears to be a private type.

When your root window shows with the tabbing mode set to .automatic, tabbedWindows will still be nil during windowDidLoad. Only later is the array not-nil.

This is how I find out which window is contained in the collection, by the way: by setting a breakpoint and inspecting the memory addresses in the debugger output.

(lldb) po self.window!
<NSWindow: 0x600003e0c400>

(lldb) po self.window!.tabbedWindows
▿ Optional<Array<NSWindow>>
  ▿ some : 1 elements
    - 0 : <NSWindow: 0x600003e0c400>

The selected tab’s window will be key and main window when you set it programmatically

Let’s call the window controllers window property the “root window”, because we start with that one and insert tabs into it.

When you open tabs next to the root window, i.e. to the right of it, you will notice that the root window will remain key and main window.

Root window <NSWindow: 0x600003e04300> Window #1 has tabs:
-  <NSWindow: 0x600003e04300> Window #1 isKey = true , isMain = true
-  <NSWindow: 0x600003e05800> Window #5 isKey = false , isMain = false
-  <NSWindow: 0x600003e05000> Window #4 isKey = false , isMain = false
-  <NSWindow: 0x600003e08600> Window #3 isKey = false , isMain = false
-  <NSWindow: 0x600003e20200> Window #2 isKey = false , isMain = false

When you send makeKeyAndOrderFront(nil) to the new tab so it becomes active after showing, see what happens if you log the window hierarchy afterwards:

Root window <NSWindow: 0x600003e0c100> Window #1 has tabs:
-  <NSWindow: 0x600003e0c100> Window #1 isKey = false , isMain = false
-  <NSWindow: 0x600003e04700> Window #5 isKey = true , isMain = true
-  <NSWindow: 0x600003e02e00> Window #4 isKey = false , isMain = false
-  <NSWindow: 0x600003e06c00> Window #3 isKey = false , isMain = false
-  <NSWindow: 0x600003e02d00> Window #2 isKey = false , isMain = false

Note that only sending the new tab’s window the makeKey() message will not activate the tab. You need makeKeyAndOrderFront!

Manually selecting a tab will make it key, but not main window – at first

Also note that manually selecting a tab will make it key window, but it will not make it the main window of the app. Here’s the log output from windowDidBecomeKey(_:) after I selected a tab manually:

Root window <NSWindow: 0x600003e08500> Window #1 has tabs:
-  <NSWindow: 0x600003e08500> Window #1 isKey = false , isMain = true
-  <NSWindow: 0x600003e0eb00> Window #5 isKey = false , isMain = false
-  <NSWindow: 0x600003e0ea00> Window #4 isKey = true , isMain = false
-  <NSWindow: 0x600003e00b00> Window #3 isKey = false , isMain = false
-  <NSWindow: 0x600003e0e900> Window #2 isKey = false , isMain = false

The main window state will change, too, eventually, but only after the windowDidBecomeKey(_:) callback has exited. If you’re curious, add a key-value subscription and see for yourself:

NSApp!.observe(\.mainWindow, options: [.new, .old]) { [weak self] (app, change) in
    print("main window \(change.oldValue) -> \(change.newValue)")
}

This will show the change after windowDidBecomeKey(_:) exited.

When you Cmd-Tab in and out of the app again after a new tab was seleted, the windowDidBecomeKey(_:) callback is invoked again. Then, when tabbing back into the app, you’ll the that the selected tab is both main and key window. So don’t be fooled by the initial output like I was for quite some time.

Tabbed windows also have a list of all tabbed windows!

This surprised me, but is somewhat consistent with the active tab becoming key and main window (eventually).

When you add a tabbed window, all windows, including the root window, will have the same tabbedWindows content:

Root window <NSWindow: 0x600003e20000> Window #2 has tabs:
-  <NSWindow: 0x600003e08600> Window #1 isKey = false , isMain = true , and has tabs:
    -  <NSWindow: 0x600003e08600> Window #1 isKey = false , isMain = true
    -  <NSWindow: 0x600003e20000> Window #2 isKey = true , isMain = false
-  <NSWindow: 0x600003e20000> Window #2 isKey = true , isMain = false , and has tabs:
    -  <NSWindow: 0x600003e08600> Window #1 isKey = false , isMain = true
    -  <NSWindow: 0x600003e20000> Window #2 isKey = true , isMain = false

Because I sometimes get asked how I figure this stuff out, I put the following print statements into newWindowForTab(_:) after adding the tab:

func inspectWindowHierarchy() {
    let rootWindow = windowController.window!

    guard let tabbedWindows = rootWindow.tabbedWindows else {
        print("Root window", rootWindow, "does not have tabs!")
        return
    }

    print("Root window", rootWindow, "has tabs:")
    rootWindow.tabbedWindows?.forEach { window in
        print("- ", window, "isKey =", window.isKeyWindow, ", isMain =", window.isMainWindow, ", and has tabs:")
        window.tabbedWindows?.forEach { subWindow In
            print("    - ", subWindow, "isKey =", subWindow.isKeyWindow, ", isMain =", subWindow.isMainWindow)
        }
    }
}

The original window will not have a special status

When you make another tab active via makeKeyAndOrderFront, it becomes key and main window. That’s just how a non-tabbed window you activated and brought to front would behave.

But I found this confusing at first, especially before I found out all the rest above, and I still am not sure what this really means. Who is responsible for drawing the frame and tab bar?

Are the windows switched out from underneath each other to make it appear that the frame on screen stays the same? (This would explain the odd behavior of the frame resetting, see below.)

Originally I expected the root window to give up key status, but not main status. I expected it to remain “special” in some way, but it turns out I couldn’t find any specialty, as you will see.

Closing the root window will break the naive addition of tabs

Say you have 10 tabs opened and close the tab for the root window. Then tabs 1...9 will be visible. But once you try to add a new tab by calling rootWindow.addTabbedWindow(...) again, you’ll find out that a new window frame will open that includes the rootWindow’s tab and the new one. So closing the root window apparently affects how addTabbedWindow behaves.

What happens when you open new tabs and close the root window’s tab

Before jumping to conclusions, let’s have a look at what happens to the tabbesWindows arrays.

To inspect this, make the window controller the new windows’ delegate if you haven’t already:

newWindow.delegate = self

Note: Setting newWindow.windowController = self does not, in fact, change the delegate. You have to do this in an extra step, just like your Xib or Storyboard comes with a delegate wiring from the get-go.

Then I print the window hierarchy again once the key window changes, which happens automatically after I close the root window’s tab and another tab gets focused:

func windowDidBecomeKey(_ notification: Notification) {
    guard let window = notification.object as? NSWindow else { return }
    guard window != self.window else { return }
    inspectWindowHierarchy()
}

The output:

Root window <NSWindow: 0x600003e0c000> does not have tabs!

Bummer. So closing the root window effectively removes our access to all of the remaining tabs on screen. There still is a window frame drawn with the remaining tabs, but the window controller has no means to get there. Note that the root window’s tabbedWindows is nil, and its tabGroup.windows contains only a reference to itself.

Before you ask: I also had a look at the root window’s childWindows. The visible window isn’t listed there, either. The windows have no connection I could find between each other. As far as AppKit is concerned, the closed window is gone, detached from the remaining visible tabs, and their controller cannot get to them.

Switching tabs actually changes the visible window frame on screen

Next, start fresh and add 2 tabs to the application. I select the 3rd tab, stop execution in Xcode, open the View Debugger, and inspect the NSWindow that is part of the window controller scene. Its object address is not the address of the root window, it’s the address of the window corresponding to the current tab.

You may have noticed I said “visible window” or “window frame” a lot in this post. It’s because naming things gets confusing.

From a user’s perspective, switching tabs changes the visible contents of the window. It does not exchange the window, as far as a user perceives any change at all. The window, with its titlebar and its close, maximize, and minimize buttons, stays the same. But in the world of AppKit, that’s not the case. The window on screen is replaced with the window of the selected tab, and the tab bar just shows a different active selection.

The thing visible on screen I call the “window frame”, to reference the drawn artifact. It seemingly stays constant. But “the window”, as in NSWindow, is different under the hood.

How can you figure out which window is the one used to draw the frame onto your screen?

  • Find the active tab. As long as the rootWindow is visible, you can use rootWindow.tabbedWindows.first(where: { $0.isKeyWindow }) to get the key window, i.e. the active tab.
  • Find the active app’s window. Once the rootWindow is closed, and its tabbedWindows reference is nilled-out, your only chance is to use the application’s global window list: NSApp.windows.first(where: { $0.isKeyWindow }). (In applications with e.g. a preferences window or when you show the About panel, you’ll need to use a different filter predicate to exclude all the windows that are not the tab-based ones.)

Note than when you open the standard About panel, NSApp.windows.first(where: { $0.isKeyWindow }) will produce the About panel because it has key window status. But the About panel will not be the main window; it’s an auxiliary window only. Use NSApp.windows.first(where: { $0.isMainWindow }) to get to the window with the tabs in it.

Fix the window controller’s notion of “its window” to always know the active tab

I do not like that the root window is still referenced by the window controller although it’s been closed.

The notion of a “root window” was introduced by myself to distinguish between “the window visible from the start”, and “window for new tabs”. This made me believe the root window retained a special status, but nothing indicates this is the case.

It doesn’t make sense to cling to it. What I thought to be the root window really is nothing special at all. Its similar to all other windows in the tab group:

  • all windows contain a list of tabbedWindows, including the one we started with, and
  • closing any window removes it from tabbedWindows and also nils out its own list of tabs.

If the root window does not have any special status as far as AppKit is concerned, then the window controller shouldn’t treat it in a special way and stop holding on to it once it’s closed.

Let’s reflect this matter by updating the strong NSWindowController.window reference to point to the currently active tab.

Make sure the window controller still is the NSWindowDelegate of all windows for this to work:

class WindowController: NSWindowController, NSWindowDelegate {
    // ...

    func windowDidBecomeKey(_ notification: Notification) {
        guard let window = notification.object as? NSWindow else { return }
        // Replace the reference to the main key window
        guard window != self.window else { return }
        self.window = window
    }
}

Now you can close the original window’s tab and add tabs to the root window of your window controller. It will add tabs next to the currently active tab in all cases, and another frame with the previously closed window will not pop up again!

If you close most tabs that had key status, the window frame will reset when you add a new one

This is hard to pin down accurately, but it’s related to some status that tabs have when you activate them most of the time, but not always. One way to reproduce, moving the window frame before you add a new tab to see if the window frame moves at all:

  • Close cither the first root window’s tab, or
  • whichever tab gets key and main window status after the root tab closes, or
  • whichever tab gets key and main window status after the previous initial tab closes.

There’s a quirk if you call makeKeyAndOrderFront(nil) when showing a new tab. If you do, closing the tab will often result in the window frame snapping back; if you don’t, you can get away with closing tabs just fine.

Adding a new tab after closing the last important one results in the window snapping back to center.

To isolate the factors, I deactivated making the new tab key. Then have a look at the windows’s frame values after creating 2 tabs:

Root window <NSWindow: 0x600003e00800> Window #1 has tabs:
-  <NSWindow: 0x600003e00800> Window #1 isKey = true , isMain = true  at  (420.0, 343.0, 840.0, 513.0)
-  <NSWindow: 0x600003e02100> Window #3 isKey = false , isMain = false  at  (196.0, 240.0, 480.0, 292.0)
-  <NSWindow: 0x600003e0a300> Window #2 isKey = false , isMain = false  at  (196.0, 240.0, 480.0, 292.0)
  • Only the original window is visually centered on screen, which is the post-app-launch default,
  • only the original window has a fitting height.

(I experimented with different window sizes and positions. The new tab’s windows always start with (196.0, 240.0, 480.0, 292.0) before they become visible for the first time.)

What happens when you switch tabs?

Root window <NSWindow: 0x600003e00800> Window #1 has tabs:
-  <NSWindow: 0x600003e00800> Window #1 isKey = false , isMain = true  at  (420.0, 343.0, 840.0, 513.0)
-  <NSWindow: 0x600003e02100> Window #3 isKey = true , isMain = false  at  (420.0, 343.0, 840.0, 513.0)
-  <NSWindow: 0x600003e0a300> Window #2 isKey = false , isMain = false  at  (196.0, 240.0, 480.0, 292.0)

Okay, so when a window’s tab activates, its frame is adjusted. Move around, switch again, move again, switch again, and you end up with this:

Root window <NSWindow: 0x600003e05600> Window #2 has tabs:
-  <NSWindow: 0x600003e04400> Window #1 isKey = false , isMain = true  at  (718.0, 294.0, 840.0, 638.0)
-  <NSWindow: 0x600003e01b00> Window #3 isKey = false , isMain = false  at  (-27.0, 225.0, 840.0, 638.0)
-  <NSWindow: 0x600003e05600> Window #2 isKey = true , isMain = false  at  (718.0, 294.0, 840.0, 638.0)

This means that after switching from #1 to #2, #2 did update its frame to the current position of the original main window on screen. Let’s switch back to #1 and close the tab so #3 becomes active. Does it snap back now? No, the window frame on screen stays puts.

Interestingly, the frame origin and size after snapping back do not correspond to previous values. The window frame resizes when it snaps back for the first time, getting a bit taller. Note that in my demo app, the windows are empty and the contentView does not produce any meaningful intrinsicContentSize. The problem could look a bit different if you have a content view and/or an autosave name for the window frame.

Whatever the factors, a quick fix for this is to force-restore the frame position:

func windowDidBecomeKey(_ notification: Notification) {
    guard let window = notification.object as? NSWindow else { return }
    guard window != self.window else { return }

    // When changing whichever window is owned by the controller,
    // restore the previous key window's frame to prevent snapping
    // back to the center.
    let oldFrame = self.window?.frame
    self.window = window
    if let oldFrame = oldFrame {
        window.setFrame(oldFrame, display: true)
    }
}

Conclusion

As I said in the introduction, I do not recommend building on top of this. The behavior I witnessed is too weird already. I don’t know into which kinds of trouble I’ll run if I used this in a complex application like The Archive, where the user interface needs to stay responsive. I don’t want to mess up window autosaving information, including the widths of split view parts. Since the window frame snaps back until you force it not to, I suspect that something like this might happen all too easily.

T’was a fun experiment, though. I learned a lot about tab groups and the real meaning of key and main window status, and that tabbing just replaces the on-screen NSWindow instance. It’s always a bit enlightening to see how my perception as a user differs from the actual implementation. In this case, AppKit replaces the on-screen window completely and does not simply switch out content views.

I hope that having published this post will help humankind in the long run. My naive implementation of tabbing started with this approach, because NSWindowController is such a convenient place to respond to newWindowForTab(_:)! Don’t be like me, be smart and implement a better window controller management in your AppDelegate, if that works for you, or in a custom service object which we’ll explore in the next post.

How I Never Forget to Pack Stuff in My Backpack

I almost never forget to pack stuff for my regular trips to the gym, family visits, or shopping. Of course I sometimes do forget to pack something for a weekend trip, or I overpack books when I visit friends over the weekend and am afraid to run out of reading material during the train ride.

But for the daily leaves of my appartment, I do not forget to pack keys, phone, money, writing utensils, sketchbook, etc.

This wasn’t always the case. I attribute the immense improvement in reliability to my 2017 christmas read of Mari Kondo’s The Life-Changing Magic of Tidying Up.1 At first, I thought it was funny to give my backpack time to rest and relax. That’s how she frames emptying the bag completely. I still do think it’s funny, but the practice stuck. I tried a couple of her tips, no matter how they were explained or tied to a value system probably related to Shinto beliefs. And lo and behold, emptying the backpack completely helped to make sure I have everything packed when I leave.

I think this is because I would be so accustomed to having all necessities ready to go at any given point in time. If I ever took a piece out of the backpack and forgot to immediately re-insert it, I’d forget about it missing completely. Relying on an always-ready backpack made me more vulnerable to forgetting to pack e.g. my purse. It happened all the time, really. I almost never forgot my keys, though, because I always leave them next to the door for picking up whenever I leave. Packing my every-day carry backpack every time before I leave works in the same way.

Because my backpack is empty by default, I have to run through my mental checklist of things to pack just before I put on my shoes and leave. This actually makes me more resilient in practice: I have to fetch all the items I need everytime and I do notice when they are misplaced, actually making it less likely to misplace them in the future.

Here is another tip taken from Mari Kondo’s book: Optimize your storage for putting away, not for retrieval (see page 142 of the book). Chaos emerges from too high a threshold to put a thing where it belongs. Storing away should be the easiest part. When you need something, you will fetch it no matter the cost anyway. – But I do mind the cost of picking up, so I trained myself to put things into similar spots more reliably over time. I execute almost the exact same movements everytime I pack my bag now, it seems.

So as counter-intuitive as it may seem, emptying my backpack every time I get home helps me not to forget to bring anything when I leave.

In case you wonder: My beloved backpack is a Maxpedition Falcon III1, by the way. It’s a tactical backpack, and I love the form factor. I got the backpack last year as a thank-you from a good friend for being his best man when he wed his bride. It’s one of the very best gifts I ever received. I enjoy packing and unpacking it, it’s crazy. And I really dig that I can attach pouches using the Molle webbing. It’s a cool toy for grown-ups.


  1. This is an affiliate link. I get a small kickback from any purchase from amazon.com without any additional cost to you. 

Convert MS Word DOCX Files to Markdown with Images

I don’t know when was the last time I received a Microsoft Word .docx file. However long the streak may have been: it has been broken today.

The document contained links and embedded images. I was instantly taken aback by the prospect of all the manual labor of extracting the images and saving them to files, not even knowing how MS Word behaves nowadays.

But I have pandoc installed.

And it’s great. It even extracts images and saves them into a subfolder. I love it. Didn’t know about this feature until I scrolled through the output of pandoc -h.

This is the command I used:

$ pandoc -o output.md --extract-media=./ inputfile.docx

It puts all images into a media/ subfolder anyway, so I set extract-media to the current directory. Just lovely.

Lightweight CrashReporter Library for Mac Released

The wording here is 100% Brent’s achievement. Love the humble tone.

I compiled a crash reporter library with CocoaPods support. Check it out:
https://github.com/CleanCocoa/CrashReporter/

The past couple of days, I’ve been working on integration of an automatic crash reporter. Turns out that on some machines The Archive is crashing regularly during search, and I want to track this down. I need data by more than the most courageous users who can venture into the Console to track down crash report files and send them to me.

So I bit the bullet and began work on a server script that would accept crash report files and email them to me. I think that’s better than making users email me crash reports directly. Not everyone has Mail.app set up on her Mac, after all.

Fortunately, Brent Simmons open-sourced NetNewsWire 5 and blogged about his crash reporter. Brent has decades of experience on the Mac, so whenever he publishes a practical tip, I listen.

Just have a look at the code: Collecting .crash files and sending them over? Sounded simple enough! NetNewsWire even sports a very humble crash reporter UI, and automatically sending reports on the user’s behalf if the user choses to do so. The required Swift types are simple enough to integrate once you figure out where Brent put all the helper extensions :)

I cannot stress enough how amazing open source is. All this experience, assembled in publicly available code repositories for everyone to grab and use. This is crazy.

I am now rolling this out for my apps, starting with the WordCounter and The Archive on their respective “beta” update branches to see how it works in practice. The Swift code is simple and I trust it doing its job. But the server script, well, it needs to be battle-tested now!

I’m going to add public-key encryption of messages and/or some sort of authentication to prevent malicious attackers from spamming me with emails that, well … I send to myself.

Move! Work Break Timer v1.4.0 Released

I just released an update to Move!, the work break app I develop and use. It fixes a couple of user experience issues, like displaying a Dock icon when you view the preferences so you don’t lose the window.

Also, the app’s preferences didn’t display license details properly on Mojave and above. The app now uses Swift 5 and updated external libraries under the hood, is properly notarized (hello, Catalina!) – and a couple of MiB larger than before. I am looking forward to when we can depend on the Swift runtime being available on user devices.

Fixed Code Highlighting on the Blog

I manually edited about 400 occurences of code block markers from #!swift into \“swift`. I hope I didn’t break anything in the process. So far, things look good.

If you find oddly looking posts or broken code blocks, please tell me about them. There’s no way I can find problems on my own in 10 years worth of blog posts :)

Find Out if Xcode/LLDB Debugger Is Attached to Current App

This time, I don’t want to find out if the app is compiled as debug/release mode; I want to find out if the debugger is attached.

I found an Objective-C answer on StackOverflow which is based on HockeyApp-iOS.

Here’s my Swift translation, effectively using lazy evaluation of global variables instead of a dispatch_once_t:

let isDebuggerAttached: Bool = {
    var debuggerIsAttached = false

    var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
    var info: kinfo_proc = kinfo_proc()
    var info_size = MemoryLayout<kinfo_proc>.size

    let success = name.withUnsafeMutableBytes { (nameBytePtr: UnsafeMutableRawBufferPointer) -> Bool in
        guard let nameBytesBlindMemory = nameBytePtr.bindMemory(to: Int32.self).baseAddress else { return false }
        return -1 != sysctl(nameBytesBlindMemory, 4, &info, &info_size, nil, 0)
    }

    if !success {
        debuggerIsAttached = false
    }

    if !debuggerIsAttached && (info.kp_proc.p_flag & P_TRACED) != 0 {
        debuggerIsAttached = true
    }

    return debuggerIsAttached
}()

I posted my version to StackOverflow, too, of course. Find it here.

Fold Current Level-1 Heading in Emacs Org-Mode Outlines

There’s no standard shortcut to fold the current subtree of an org-mode outline.

When I work in org-mode outlines, I usually am 3 or more levels deep into a so-called “subtree” and want to get back to the root item, fold it to hide the details, then drill down into another item. I use that when I am working on an app and want to have a look at a planned milestone nested deep down at a different point in the outline.

Usually, I hit Shift-tab to fold the whole buffer and then get to the other item I want. But oftentimes I do want to close the current branch only and leave the others open, not fold everything. The vanilla Shift-tab shortcut configuration executes org-shifttab, which folds the whole buffer through org-global-cycle to put you in “overview mode” first, then “contents” if you hit it again, then “show all”. See the docs on global and local cycling.

Here’s a demo of my solution:

First I hit M-h to mark the line, then S-<tab> to fold the first subtree up to its root level, then hit the shortcut again to cycle through the global folding states twice (see the minibuffer at the bottom saying “OVERVIEW” etc.), then fold all subtrees except the middle one.

I came up with this function to figure out the root-level heading of wherever your cursor is at the moment, then hide the whole subtree:

(defun ct/org-foldup ()
  "Hide the entire subtree from root headline at point."
  (interactive)
  (while (ignore-errors (outline-up-heading 1)))
  (org-flag-subtree t))

The default behavior of org-shifttab is useful, too, though.

Have a look at the code from org.el as of now, to learn a bit elisp on the side:

(defun org-shifttab (&optional arg)
  "Global visibility cycling or move to previous table field.
Call `org-table-previous-field' within a table.
When ARG is nil, cycle globally through visibility states.
When ARG is a numeric prefix, show contents of this level."
  (interactive "P")
  (cond
   ((org-at-table-p) (call-interactively 'org-table-previous-field))
   ((integerp arg)
    (let ((arg2 (if org-odd-levels-only (1- (* 2 arg)) arg)))
      (message "Content view to level: %d" arg)
      (org-content (prefix-numeric-value arg2))
      (org-cycle-show-empty-lines t)
      (setq org-cycle-global-status 'overview)))
   (t (call-interactively 'org-global-cycle))))

The function

  • moves to fields within tables,
  • cycles through global visibility states,
  • or shows contents of the current level.

I don’t want to break at leas the first two effects. I still want to cycle through the global folding modes when I’m at the root level, and navigating around at the table is very useful, too.

From the default implementation, we can copy the org-at-table-p predicate and write conditions to fold up to the root level only outside tables.

The following function remaps “backtab” (Shift-tab) in emacs org-mode to fold the current outline item up to its root-level heading unless you are in a table or at the root level already. The conditions, explained:

  • (org-at-table-p) returns t (true) iff your current insertion point is inside a table
  • (org-at-heading-p) returns t (true) iff your current point is at a heading line
  • (= 1 (org-current-level)) checks if your point is somewhere inside a level-1 heading or its direct contents; combine with org-at-heading-p to limit to being at the heading line itself.
(defun ct/org-shifttab (&optional arg)
  (interactive "P")
  (if (or (null (org-current-level))     ; point is before 1st heading, or
          (and (= 1 (org-current-level)) ; at level-1 heading, or
               (org-at-heading-p))
          (org-at-table-p))              ; in a table (to preserve cell movement)
      ; perform org-shifttab at root level elements and inside tables
      (org-shifttab arg)
      ; try to fold up elsewhere 
      (ct/org-foldup)))
(org-defkey org-mode-map (kbd "S-<tab>") 'ct/org-shifttab)

There you go: backtab to fold the current subtree up to the root level!

Combine E-Book for Free

There is a free ebook out there that teaches you the basics of Combine, Apple’s reactive framework introduced at WWDC 2019. It is pretty long already, given the time author Joseph Heck (@heckj) had to learn about Combine and then write about it. From what I saw, I think it’s a good introduction to reactive programming in general.

Seriously, you should read it: https://heckj.github.io/swiftui-notes/

And the book’s source is available, too! https://github.com/heckj/swiftui-notes

From the repository’s name, swiftui-notes, and this description:

A collection of notes, project pieces, playgrounds and ideas on learning and using SwiftUI and Combine.

… I can only assume it gets expanded even more over time to include both SwiftUI and Combine!

Check it out and share it; Joe Heck deserves some internet love for his efforts.

Post Overview Updated

I don’t know what’s going on this week, but I have written a lot for the blog. I also carved out an hour and a half today to update the structured overview of articles on this website. I always wanted to make it a good entry point but didn’t update it during the transition to another blog platform, then back again, and now it’s 2 years later already. Phew!


→ Blog Archive