Click here for light mode

Description

kbin Enhancement Suite (KES) bundles together many userscripts that add various features to kbin and adds an integrated interface for configuring them.

As such, KES is three things:

  • A curated and audited collection of modifications (userscripts)

  • A menu for managing this collection of modifications

  • A framework for authors to add new modifications

For users, it provides a "single pane of glass" from which to manage various usability/customization options.

For script authors, it provides a simple, human-readable format to integrate new options with minimal overhead and describes a standardized framework for sharing settings between scripts.

Modifications are variously referred to as "features," "add-ons," and "mods" within this document.

Prerequisites

KES is itself a userscript that bundles other scripts. It requires any of the below browser extensions in the *monkey family to run:

Versions for Firefox- and Chromium-based browsers can be found on your respective add-on/extensions page.

Installation

Navigate to the install script and follow your browser extension’s onscreen prompts to install it. Subsequent updates should be handled seamlessly, and KES will notify you if there is a new version available.

New add-ons are deployed directly within the menu, and the suite of features should grow over time.

Usage

Main menu

To open the KES settings menu, press Ctrl-Shift-? or click the wrench icon in the top right corner of the page. On mobile devices narrower than 576px, the icon can be found inside of the hamburger menu on the top left of the screen, next to the kbin logo.

Pressing Ctrl-Shift-? again, Escape, or clicking on the close button or anywhere outside of the menu will close it.

The menu is divided into pages; each page contains a list of modifications. You can switch between pages by clicking on the page name in the list on the left side of the menu (or the top if on mobile).

Clicking the name of a modification will open an information panel with a description of the modification and a toggle to enable/disable it. If the modification has any additional settings, they can be configured here.

On devices where the menu doesn’t take up the whole screen, it can be docked to the bottom of the screen by clicking the arrow icon in the top right corner of the menu. Clicking the arrow again will undock the menu. Docking could also be used to hide the interface out of the way in order to see changes that would otherwise be occluded by the main menu.

Search: opens a keyword search field; search for add-ons using partial string match. Click the returned results to jump to the corresponding add-on’s settings page. You can also use the button that appears at the top of the search menu to search for any new add-ons that have been added as of the current major version of KES you are using. If new add-ons were incorporated in that release, this button will return a list of candidate add-ons.

Changelog: a link to the KES changelog, listing all user-facing changes to date.

Transparent Mode: hides the KES menu so that you can "see through" to the rest of the page and inspect how changes were applied. This mode draws a thin border around the screen edge to indicate you are in a special mode. Click anywhere on the screen to revert to the KES menu.

Docked Mode: docks the KES menu to the bottom of the screen in a smaller size.

Close: closes the KES menu.

Copy system info: click to copy system information to the clipboard for use when submitting a bug report. The information copied is:

  • Operating system

  • User agent (identifies the type of browser)

  • KES version number

  • Script handler (GreaseMonkey, TamperMonkey, etc.)

  • Incognito: whether private browsing is on (used to troubleshoot issues)

  • Settings: the currently saved KES settings, such as which options are on, and what settings they have

SETTINGS button: opens a submenu exposing the following options:

  • Export: exports the toggle state and sub-settings of each KES feature to a timestamped backup file (JSON). Used to backup your settings from a given point in time. Note that this includes global KES settings and per-add-on settings, but does not currently include storage internal to specific add-ons, such as blocklists.

  • Import: imports a file generated using the above Export button and applies those settings to KES. Used to restore your settings from a given point in time.

  • Reset: flushes the toggle state and sub-settings of each KES feature and resets them to the defaults.

  • Close: closes the settings sub-menu.

Settings

The toggle state of a modification and its settings will be saved and persist across browser sessions.

Troubleshooting

To submit a bug report/feature request or visit the KES home page, follow the links shown within the KES menu itself, or navigate here.

Developers

Basic workflow

If you wish to submit your scripts for integration into KES, a standardized framework is available that makes adaptation and PR submission easy:

The metadata related to a script is defined a priori in the file manifest.json, which itself is a concatenation of JSON objects sourced from each script’s self-named directory. KES automatically populates its pages and assigns your add-on to the category requested, filling its contents with the fields and values you set.

  1. Set up an entrypoint function in the script which enables/disables it (See Script calling logic)

  2. Receive a boolean toggle parameter from KES passed to the above function indicating whether the user has toggled the script on or off

  3. If the script defines custom input fields, use the getModSettings() function exposed by KES with your script’s namespace as the function parameter (See Retrieving custom settings). If you wish to call internal GreaseMonkey API functions, see Compatibility API for details on cross-compatibility and some utility functions that facilitate this.

  4. Parse the resulting settings object for your desired keys and use these settings in the business logic of your script

Detailed explanations follow.

JSON manifest

manifest.json consists of an array of objects that each represent an add-on, that is, an atomic feature provided by a function in a third-party userscript. Add-ons must be given a globally unique entrypoint function name and, if using custom input fields, a globally unique namespace.

manifest.json is not manipulated by hand, being created automatically during the build process by maintainers. When submitting a script, the author is solely responsible for creating their own JSON file defining settings for their script.

Ensure that you submit your add-on using the following directory hierarchy, with all files prefixed with the name of the parent directory. Given a mod named softblock, the submitted files should be:

softblock/ (1)
├── softblock.json (2)
└── softblock.js (3)
1 The parent directory
2 The JSON object for your script
3 The script itself

If the add-on requires custom input fields like select, radio, or other input types, they can be added under the fields array, one custom field per object.

The namespace is used to store settings under a localStorage object, which is used to share settings between KES and third-party add-ons, or between third-party add-ons.

localStorage
Storage {
    "kes-settings": (1)
        '{
            "addMail":true,
            "initMags":true,
            "magInstanceEntry":true,
            "hideDownvotes":true,
            "hideUpvotes":true,
            "updateTime":true,
            "changeLogo":false,
            "dock":"up",
            "checksInit":true
        }',
    codehighlights: '{"style":"gruvbox"}', (2)
    languagefilter: '{"filter":"English"}',
    mail: '{"type":"Text","text":"PM","state":"on"}',
    timestamp: '{"offset":"Local time","state":"on"}',
    length: 6
}
1 In the above example, KES has saved the state of eight add-ons, seven of which are enabled by the user. In addition, it has stored the position of the KES window to up. (This is not controlled by third party add-ons.)
2 Finally, the four add-ons codehighlights, languagefilter, mail, and timestamp have respectively saved their own settings in custom namespaces. (The other three add-ons did not request any custom settings fields.)

KES handles toggling of add-ons and passes their boolean state to the recipient script on pageload events and mutations to the thread/post content area.

The recipient script therefore does not need to poll this state or watch for page changes, as it is called as an internal function of KES when needed.

The only responsibilities of the recipient script are:

  • Handle setup and teardown of the desired logic (show/hide elements, apply/unapply styling)

  • Parse its own namespace under localStorage and retrieve custom settings. To facilitate this, KES provides the getModSettings() function. See Retrieving custom settings.

mail.json
  {
    "name": "Add mail",
    "author": "shazbot",
    "version": "0.1.0",
    "label": "Add mail icon",
    "desc": "Add mail link to usernames if on kbin.social",
    "login": false,
    "recurs": true, (1)
    "link": "mypage.dotcom",
    "link_label" "My link"
    "entrypoint": "addMail",
    "namespace": "mail", (2)
    "fields": [ (3)
      {
        "type": "radio",
        "initial": "Text",
        "key": "type",
        "label": "Label type",
	"values": [
		"Text",
		"Icon"
	]
      },
      { (4)
        "type": "text",
        "initial": "PM",
        "key": "text",
        "label": "Link label"
      }
    ],
    "new_since": "4.0", (5)
    "depends_on": [
        "init_softblock" (6)
    ],
    "page": "general" (7)
  }
1 If the user has enabled infinite scroll and the add-on is expected to modify these new threads and/or comments, setting this value to true will ensure that the script is applied again.
2 A globally unique namespace under which the script’s custom field settings are stored.
3 See Adding custom input fields. In the above example, the descriptive text 'Label type' will be printed on one line, followed by a line break, then two radio buttons respectively labeled 'Text' and 'Icon', in that order, separated by line breaks, with the 'Text' radio button initially selected. The initial value of 'Text' will be saved under the mail.type key (i.e., prefer a text label instead of an icon) and updated if the user changes the radio button.
4 This is followed by a descriptive label reading 'Link label', a line break, and then a textarea initially set to the string 'PM', with this value stored under the mail.text key. In this example, the link label might be used by the recipient script if mail.type was set to Text. KES is agnostic to how these settings are parsed and merely populates the fields. As far as KES is concerned, functionality of one field does not depend on another; it is up to the author to add additional fields if necessary.
5 Indicates what major/minor version this mod has been available since. This is used to show new mods particular to a given release when users navigate the search menu within KES. This key should not be manually defined by mod authors, but is added by the maintainer when integrating mods into a new release.
6 If the script depends on another KES script being loaded first, specify that script’s entrypoint function here. Analogously, depends_off provides functionality for dependencies that must also be disabled when the calling script is turned off.
7 The contents of the metadata and custom fields will be added to the 'General' page of the sidebar under the feature label 'Add mail icon'. Available pages can be seen within the file ui.json.
Table 1. basic metadata
Key Optional? Type Value

name

string

An internal, "official" name of the add-on, possibly more verbose than the user-facing string

author

single author: string; multiple authors: array of strings

The author of the add-on. This is user-facing and links back to the named profile on kbin. If you are on an instance other than kbin.social, include the full @<user>@<instance> designation here

version

string

An internal version number

label

string

A short, descriptive name of the feature, used when printing it in the list of options. This functions as the "name" of the feature seen by users

desc

string

A user-facing description of what the feature does. JSON newline escapes will be interpolated into a <br> element at runtime, but raw HTML must not be used.

login

boolean

Whether the option requires being logged into the site to function/display correctly. true and false will respectively be styled to the user-facing strings "yes" and "no"

recurs

boolean

If the feature should recur and apply to new elements in the tree in the event of DOM changes to the content area, such as new posts or threads when lazy load (infinite scrolling) is enabled

entrypoint

string

A globally unique entrypoint function in the recipient script used to toggle the feature on or off. This value is auto-populated by build scripts.

namespace

yes

string

A globally unique namespace used if the add-on exposes custom input fields (see below). This namespace is used when parsing localStorage

link

yes

string

A link to external content, such as a web site or help file

link_label

yes

string

A user-facing label for the link above

fields

yes

string

An array of objects containing custom input fields

depends_on

yes

array of strings

The qualified names of entrypoint functions used by other KES mods. These functions will be called in sequential order when the parent mod is going to be enabled, after which the parent mod is enabled.

depends_off

yes

array of strings

The qualified names of entrypoint functions used by other KES mods. These functions will be called in sequential order when the parent mod is going to be disabled, after which the parent mod is disabled.

Adding custom input fields

Custom input fields are themselves optional, but if the fields array above has been declared, it must be filled with some or all of the keys below.

Table 2. The fields array
Key Optional? Type Value

type

string

The input field type. Available types are select, radio, checkbox, reset, and miscellaneous single-value types defined here. If using reset, limit to one per script.

initial

string (if checkbox, bool)

The initial value the field is set to

key

string

A unique key for this setting, stored under the object namespace defined in Table 1. This key is parsed by the recipient script in the format namespace.key in order to extract user-defined settings

label

yes

string

A descriptive label of what the setting does, printed above the input field. If stacking multiple options above each other, such as checkboxes, omitting the label field and adding a single one in the first object is supported.

values

required if type is select or radio

array of strings

If the type is select or radio, an array of user-facing labels, which also function as values, used to populate each option

checkbox_label

required if type is checkbox

string

A user-facing label printed to the right of a checkbox

min

required if type is range or number

int

The minimum value in the range

max

required if type is range or number

int

The maximum value in the range

step

optional if type is number

int

The interval by which to increment/decrement the range

show_value

required if type is range

bool

Whether to print the current numerical value of the range slider

catch_reset

required if type is reset

array of strings

The verbatim names of input field keys that should respond to reset button events. When a reset button is pressed, those named fields will reset to their initial values

Script calling logic

A number of pre-existing examples can be found under the /mods directory of the repository.

KES calls the recipient script via the entrypoint function defined in manifest.json with a boolen argument.

function myEntryPoint (toggle) {
    function toggleOn(){
       let el = document.querySelector('.myelement')
       if (!el) {
           document.body.appendChild(el);
       }
    }
    function toggleOff () {
       $('.myelement').hide();
    }
    if (toggle) {
        toggleOn();
    } else {
        toggleOff();
    }
}

Bear in mind that if you have defined custom input fields, such as choosing between different label/icon types or supporting custom strings, or when an infinite scroll event occurs (Handling infinite scrolling), KES may attempt to call the entrypoint function again and apply the new settings.

Therefore, if the element being modified already exists, you should add logic to either override its current value or return gracefully, as seen in the boilerplate examples above and below. Otherwise, the same element may be created multiple times.

Retrieving custom settings

Parsing your script’s settings is as simple as calling getModSettings() with the desired namespace and applying those accordingly.

You can also leverage this function to retrieve the settings of other scripts for more synergistic functionality.

let myNs = "mymod";
let settings = getModSettings(myNs);
let color = settings["color"];
let mydiv = document.querySelector("mydiv");
mydiv.style.cssText = "background-color:" + color;

Taking the example function from an earlier section, we can combine it with the above to ensure that if the element does not exist, it is created, and if it does exist, it is updated with the latest setting the user applied. With this basic flow, a user can change colors/labels/other parameters within the KES menu and see them updated immediately.

function toggleOn(){
   let el = document.querySelector('.myelement')
   if (!el) {
       document.body.appendChild(el);
   }
   el.style.cssText = "bacground-color:" + color;
}

Handling infinite scrolling

The recurs boolean (see JSON manifest) is used to specify whether the script’s entrypoint function should be called again when the thread ('[data-controller="subject-list"]') or post comments area ('#comments') have DOM changes. This allows your mod to be applied again in the event of post replies, new threads being loaded in, et cetera.

Simply set this value in the manifest and the script will be called automatically and applied to the new content.

There is no need to include additional onload event listeners or mutation observers to the script itself or watch for page events, as they are handled at the top level by KES.

Prototyping local code

To facilitate prototyping local changes without committing code, two build scripts are provided in /build/scripts. Both of these tools use relative paths and should be invoked from the repository root. You could also chain these scripts together into one if you so desire.

  • concat-funcs

This is the same script used by the KES build routine to concatenate the contents of each mod into a global functions file. Prior to running the script below, this should be run in order to ingest the latest changes to all functions.

  • gen_gist.sh

Requires the environment variable KES_GIST_GEN to be set. Navigate to this page to set up a fine-grained personal access token.

Uploads a copy of the functions file above as a Gist to GitHub, then inserts this URL into the KES script headers as a remote resource.

If the tool xclip is available on your system, the contents are then copied to the system clipboard. Otherwise, the contents are written to stdout, where they can be redirected into a file.

These contents can then be pasted directly into your *monkey extension to test any changes. To obviate the need to manage multiple Gists, only one copy of the file will be uploaded at a given time; the same remote file is subsequently overwritten each time the script is invoked, and the new ID is stored in the file .gist_id in the scripts directory.

Compatibility API

For compatibility between *monkey extensions, KES provides a shim function called safeGM() into which GM API commands can be passed. safeGM accepts the name of a GM API function and its respective parameters. Under the hood, safeGM merely passes the function call to the respective GM API prefix depending on whether the extension uses the underscore namespace or the GM 4.0 Promise API.

In addition, safeGM natively reimplements some function calls that were dropped in GM 4.0 but are available in other extensions (as of this writing, GM_addStyle). See API reference.

These changes happen invisibly when calling safeGM, so it is enough to pass one of the function names below with the arguments you would typically pass to such a function:

  • setValue

  • getValue

  • addStyle

  • removeStyle*

  • xmlhttpRequest

  • setClipboard

These are the functions currently supported, but more may be added if necessary. Given that KES handles the setting and fetching of configs, we have not seen many scripts need to leverage extra functionality through GreaseMonkey itself.

Implementation details of GM API functions can be found in your respective script manager’s documentation.

Additionally, GM_info can be invoked using safeGM("info") and the properties of the resulting object parsed out, e.g., safeGM("info").script.version.

Note that if using setValue, getValue, and xmlHttpRequest, you will need to call safeGM asynchronously and await the results to support GM 4.0. However, using these methods should generally not be necessary, as KES handles storage of settings.

Also note that getResourceText is not available in GM 4.0, so in the unlikely event that you need to use this function in a script, rather than defining @resource fields, you should use the utility function, genericXMLRequest(), and parse the results on callback.

removeStyle is not a native GM function, but is provided by KES to extend the functionality of addStyle with its logical counterpart: this method is mapped to the underlying function removeCustomCSS, but is called by the parameter removeStyle to safeGM for semantic purposes, given that it is intended to do the opposite.

API reference

addStyle()
function addStyle (string css, string stylesheet) -> void

Parameters:

  • css — the concatenated block of CSS to apply to the document head

  • stylesheet — a stringwise identifier used for later lookup of the stylesheet

Return value: none

Appends a stylesheet to the document head. Used to add ad-hoc style changes to elements on a page and remove this sheet later by calling removeStyle() with the identifier as its parameter.

This function can only be called via safeGM().
removeStyle()
function removeStyle (string stylesheet) -> void

Parameters:

  • stylesheet — a stringwise identifier used to remove the stylesheet by name

Removes a named stylesheet from the document head. When used in conjunction with addStyle(), allows for seamlessly inserting and cleaning up rules to ensure that changes to the page occur on the fly, and obviating more brute-force methods like DOM manipulation or show/hide with jQuery. When stepping through your script, it is generally good practice to call removeStyle before addStyle in order to clean up any previously existing stylesheets by that name before re-inserting them.

This function can only be called via safeGM().
genericXMLRequest()
function genericXMLRequest (string url, function callback) -> void

Parameters:

  • url — the URL to fetch

  • callback — the function to call back upon completion of the GET request; this function will receive the response data

Return value: none

Wraps xmlhttpRequest to perform a GET request and obviates setting up a complex object: simply pass the remote URL and callback function as parameters and process the response in your callback function. This is generally not needed by scripts, but is used when fetching, e.g., the contents of another page within the same instance, such as for pagination purposes.

getHex()
function getHex (string variable) -> string

Parameters:

  • variable — the stringwise variable name of a theme color

Return value: string

Translates internal kbin theme variables to their corresponding hex color values. This allows you to set sane defaults which, instead of hardcoding color values, respect the user’s theme settings. For example, setting initial: var(--kbin-upvoted-color) for the value of a custom field will ensure that whatever color is being used by the current theme is interpolated correctly.

getHex() is also used internally by KES, and can be used by scripts when passing around settings information in order to parse the incoming value without resorting to hardcoding.

getInstanceType()
function getInstanceType() -> string

Parameters: none

Return value: string

Returns either "kbin" or "mbin", depending on what type of instance the domain is running. Used by scripts to disambiguate between instance types and apply changes that may require certain compatibility adjustments on a particular instance type.

Submitting a PR

  1. Clone the repo and prepare a patch against the testing branch.

  2. If you are submitting a userscript, limit PRs to one per atomic script. If a collection of functions in the script are semantically related to each other, you may choose to group them into one script, but they must be given unique objects, entrypoints, and namespaces within your JSON object (one feature per add-on). Generally speaking, different features should be limited to atomic scripts.

  3. Scripts should not wantonly change the appearance and style of the page in the way a CSS theme would. Limit features to small functionality changes that leverage the advantages of JS over CSS. KES works best in the aggregate, when its add-ons synergize with each other.

  4. For testing purposes, you can define remote resources in the @require fields of your local copy of the kes.user.js headers when debugging, but the PR itself must not include any modifications to this file or to the VERSION file. Only submit a named directory containing a JSON object and a js file in the /mods directory.

  5. Ensure that the entrypoint and namespace (if applicable) defined in your manifest object are globally unique.

  6. If your script has external dependencies (@require) that are not included in KES, please request these to be added when making the PR so that they can be added at the top level. Note that jQuery is provided by default and can be used to reduce the verbosity of your script.

If you are submitting a PR changing an internal feature of KES itself, feel free to include changes to other files than the above.

Precautions and best practices

  • Encapsulate logic in a single private function and use local variables to reduce the possibility of collisions

KES ingests all of the script functions together into its scope, so unique identifiers and self-contained functions are important. While scripts are integration tested before deployment, you can make the testing process easier by using unique names and limiting the available scope.

  • No need to handle extra event listeners

Unless you are creating a special button or widget triggering on, e.g., clicks, there is no need to actively watch the page for changes (like onload), as KES handles this for you and will apply your changes accordingly in the event of infinite scrolling, reload events, etc.

  • Aim for minimal, concise features that do one simple thing well

Most scripts can be ported over as-is with little or no changes, but remember that KES is designed to take the complexity of setup out of the equation, allowing many small mods to be incorporated and synergize with each other. It is enough to create an entrypoint function that triggers some changes, request the desired UI via the JSON manifest, and the rest should work out of the box. Therefore, think of scripts as atomic features rather than complex workflows; scripts that make highly opinionated changes or themselves create complex menus may be difficult to adapt.

  • If changing colors on a page, it is best to respect kbin’s internal themes by using the variables provided. These can be explored under the var(--kbin-*) prefix. A number of variables for success, alerts, backgrounds, text, hover, etc. are provided. These map to different colors within the theme a user has selected. If you hardcode an element to a specific color, there is a high likelihood it may not be visible on a dark or light theme, respectively. Similarly, do not set fonts verbatim, but use the var(--kbin-body-font-family) for a set of fallback fonts. Refer to getHex() in the section API reference for details on how to convert color abstractions into hex values on the fly.

Conventions

  • PREFER: conventional commits

  • PREFER: 100 line width

  • MUST: 4-space indentation on *.js files

  • MUST: 2-space indentation on *.json files

  • MUST: space before function paren

  • MUST: space before code blocks

  • MUST NOT: comma dangle

  • MUST NOT: spaces in curly objects

See .eslintrc.json in the repository root for details.