Bug(?)--different stylesheets not remembered

(in 3.8.16)

I have three different documents, usually all open at once. I tweaked a different stylesheet for each of them. However, whenever I start up Taskpaper, they’ll all have the same stylesheet.

Let’s say I have docs 1, 2, and 3 with stylesheets A, B, and C. When I open it up they’ll all have (e.g.) stylesheet A. I’ll have to re-select the stylesheet for two of them. If the last one I adjust is to C, then the next time I launch the app they’ll all have C.

For use of StyleSheets see:

Using StyleSheets · TaskPaper User’s Guide

My understanding is that style-sheet choices relate to windows, and not to files (TaskPaper files are, of course, just tab-indented plain text files).

To illustrate this:

With File > New Window you can open a second view onto the same file, and, by selecting different stylesheets for each window, see the same file in two different styles at the same time.

Every file you open in a particular window will be viewed with the style attached to that window – it’s not the file that makes the difference.

(It might, I suppose, be possible to write a script or macro to open particular files in windows to which particular style-sheets were attached).

1 Like

They are working as designed, but I agree the design could use some work.

Currently when you choose a stylesheet it gets applied to the current window, and then saves that stylesheet name as default for all future windows. The stylesheet setting isn’t saved as “restorable state” when reopening windows… so when you restart TaskPaper all windows will take on the value of the last set stylesheet.

I think that’s how it’s working now. But I agree it would probably make more sense if each window the stylesheet that it’s using as restorable state. I’ll look into that for future release.

Jesse

1 Like

Thanks, I appreciate the consideration. It’s not the end of the world but I like the visual separation and I also correspond them to color schemes for the same projects in other apps.

@complexpoint, I think I understand what you’re saying, but the issue plays out the same way as I described even if I open the docs in separate TP windows.

The impression that you are selecting stylesheets for documents is understandable, but misleading.

TaskPaper doesn’t have stylesheets for documents.

(It only has stylesheets for windows, hence the the menu path: Window > Stylesheet)

With two different windows open, each with a different Window Stylesheet attached, you can simultaneously view the same document in two different sets of font sizes and colors etc

There is no connection at all between a document and a stylesheet.


To construct a connection of some kind, we might be able to write a script or Keyboard Maestro macro, but that would involve knowing slightly more about your workflow.

Does it just involve a small number of files which are regularly opened, or is the set of files more fluid and less predictable than that ?

How many different Window Stylesheets are you using ?

1 Like

Hi there,
Okay, I understand what you’re saying. I typically use three specific TP docs, each connected to a separate project domain. I use a distinct stylesheet for each of the docs.

That sounds manageable. Two basic options, both assuming that the script(s) contain pairings of:

  • The file path of a TaskPaper file, with
  • the filepath of the Window Stylesheet which you wish to use with that TP file.

Given a set of 3 or more pairings like that, you could have either:

  1. Three different keystrokes, each launching a script which opens a particular TaskPaper file, and applies a particular Window Stylesheet to the window in which that file is opened, or
  2. A single menu showing three (or more) files for you to open, and opening the selected file, applying the Window Stylesheet which is paired with it to the window in which it is opened.

Option 2 is more flexible, but will be fractionally slower – you may have to wait a second while the menu opens.

Do you use Keyboard Maestro or FastScripts ?

A first sketch – let us know if you need any help in customizing or installing this draft script.

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    ObjC.import("AppKit");

    // Rob Trew @2022
    // Ver 0.01

    // ------------- FILES AND WINDOW STYLES -------------

    // fileStyles :: Dict {FilePath :: FileNameStem}
    const fileStyles = {
        "~/Notes/alpha.taskpaper": "ScreenShare",
        "~/Notes/beta.taskpaper": "tomorrow-night",
        "~/Notes/gamma.taskpaper": "Default"
    };

    // ---------------------- MAIN -----------------------
    // main :: IO ()
    const main = () => {
        const [ls, rs] = partitionEithers(
            Object.keys(fileStyles).map(
                fp => openFileWithWindowStyleLR(
                    fileStyles[fp]
                )(fp)
            )
        );

        return (
            0 < ls.length ? (
                ls
            ) : rs
        ).join("\n");
    };


    // ------------- TASKPAPER WINDOW STYLES -------------

    // openFileWithWindowStyleLR :: String -> FilePath ->
    // Either String, IO (String, FilePath)
    const openFileWithWindowStyleLR = styleName =>
        fp => {
            const
                fpSheets = combine(
                    "~/Library/Application Support"
                )(
                    "TaskPaper/styleSheets"
                ),
                fullName = `${takeBaseName(styleName)}.less`,
                fpStyle = combine(fpSheets)(fullName),
                fpFull = filePath(fp);

            return doesFileExist(fp) ? (
                doesFileExist(fpStyle) ? (() => {
                    const
                        tp3 = Application("TaskPaper"),
                        doc = (
                            tp3.activate(),
                            tp3.open(fpFull)
                        );

                    return (
                        // delay(0.1),
                        menuItemClicked("TaskPaper")([
                            "Window", "StyleSheet",
                            fullName
                        ]),
                        // Closed and reopened to ensure
                        // full width re-rendering.
                        doc.close(),
                        tp3.open(fpFull),
                        Right([fullName, fp].join(" -> "))
                    );

                    // return Right(taskpaper.name());
                })() : Left(`Style not found: ${fpStyle}`)
            ) : Left(`File not found at path: ${fp}`);
        };

    // ----------------------- JXA -----------------------

    // menuItemClicked :: String -> [String] -> IO Bool
    const menuItemClicked = appName =>
        // Click an OS X app sub-menu item
        // 2nd argument is an array of arbitrary length
        // (exact menu item labels, giving full path)
        menuPath => {
            const n = menuPath.length;

            return 1 < n ? (() => {
                const
                    appProcs = Application("System Events")
                    .processes.where({
                        name: appName
                    });

                return 0 < appProcs.length ? (
                    Application(appName)
                    .activate(),
                    menuPath.slice(1, -1)
                    .reduce(
                        (a, x) => a.menuItems[x].menus[x],
                        appProcs[0].menuBars[0].menus.byName(
                            menuPath[0]
                        )
                    )
                    .menuItems[menuPath[n - 1]]
                    .click(),
                    true
                ) : false;
            })() : false;
        };


    // --------------------- GENERIC ---------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // combine (</>) :: FilePath -> FilePath -> FilePath
    const combine = fp =>
        // Two paths combined with a path separator.
        // Just the second path if that starts with
        // a path separator.
        fp1 => Boolean(fp) && Boolean(fp1) ? (
            "/" === fp1.slice(0, 1) ? (
                fp1
            ) : "/" === fp.slice(-1) ? (
                fp + fp1
            ) : `${fp}/${fp1}`
        ) : fp + fp1;


    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && 1 !== ref[0];
    };


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        ([x, y]) => Tuple(f(x))(y);


    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        ([x, y]) => Tuple(x)(f(y));


    // takeBaseName :: FilePath -> String
    const takeBaseName = fp =>
        ("" !== fp) ? (
            ("/" !== fp[fp.length - 1]) ? (() => {
                const fn = fp.split("/").slice(-1)[0];

                return fn.includes(".") ? (
                    fn.split(".").slice(0, -1)
                    .join(".")
                ) : fn;
            })() : ""
        ) : "";


    // partitionEithers :: [Either a b] -> ([a],[b])
    const partitionEithers = xs =>
        xs.reduce(
            (a, x) => (
                "Left" in x ? (
                    first(ys => [...ys, x.Left])
                ) : second(ys => [...ys, x.Right])
            )(a),
            [
                [],
                []
            ]
        );

    // MAIN ---
    return main();
})();

Oh, fantastic! I actually have not used Keyboard Maestro of Fastscripts. What’s the most straightforward way to implement this?

Keyboard Maestro is a very good investment – it makes things like customisation (one macro for each file for example) a lot easier.

(FastScripts is cheaper, I believe that the demo version may still be free for up to N scripts, where N is a smallish number)

If Keyboard Maestro looks affordable, I think you will find it rewarding. The Keyboard Maestro user forum is very helpful, and you will generally find that people will rather quickly offer solutions and macros if you ask a question there.

Let us know which you are starting with, and we can adapt a macro / script.

It would be helpful to know the names of the stylesheets, and the paths of the files to which you want to associate each window style.

(You can DM me here if you prefer not to write those details out in public)


Or, if you can do without a keyboard shortcut assignment, you could start by putting a command or two in the TaskPaper Command Pallette:

Using Scripts · TaskPaper User Guide

I’ll give Keyboard Maestro a shot, at least on a trial basis. I remember finding the syntax a bit difficult but let’s see.

The file paths are just

  • /Users/me/Dropbox/todo.taskpaper
  • /Users/me/Dropbox/chapter.taskpaper
  • /Users/me/Dropbox/teaching.taskpaper

By the way, thanks for all your help on this, it’s really above and beyond.

The file paths are just

and the matching window style .less names for each ?

Here, for example, is a first draft of a Keyboard Maestro macro which:

  • Opens three files, applying a particular window style to each,
  • and assigns a keystroke to that action. (⌘L initially, but you can reassign it).

(An alternative would be to create 3 distinct copies, assigning different keystrokes to each, and editing the fileWindowStylesJSON variable in each copy to specify only a single file and stylesheet to be opened by each).

To install:

  • unzip the archive file below by double clicking it (once downloaded)
  • double-clicking the .kmmacros file will bring it into Keyboard Maestro in an initially ‘disabled’ state
  • enable it by selecting it in the Keyboard Maestro app, ctrl-clicking it for a contextual menu, and choosing Enable Macro

Open set of files with particular window style for each.kmmacros.zip (5.2 KB)

Screenshot 2022-03-01 at 10.27.50

You should then be able to experiment with using ⌘L in TaskPaper.

Oh, amazing, thank you so much. I actually have different names for the corresponding .less sheets, but I can change those to match the .taskpaper files. Looking forward to trying this out.

1 Like

( Or just edit the macro to match the names of your existing .less files )

1 Like