Code Style Guide
Overview
Section titled “Overview”This document defines the naming conventions, formatting rules, and code patterns for the JellyRock codebase. All contributors must follow these rules. Naming conventions are enforced by code review (bslint does not support naming rules).
Core principle: PascalCase = type definitions. lowerCamelCase = everything else. UPPER_SNAKE_CASE = immutable predefined values.
Naming Conventions
Section titled “Naming Conventions”Variables & Parameters
Section titled “Variables & Parameters”lowerCamelCase for all variables, parameters, and local references.
audioStreamIdx = 1fullUrl = serverUrl + "/" + pathpreferredLang = resolveSubtitleLanguagePreference(settings, config)Functions, Methods & Subs
Section titled “Functions, Methods & Subs”lowerCamelCase for all function and sub names.
function getSetting(key, defaultValue = invalid)sub registryWrite(key, value, section = invalid)function buildAuthHeader() as stringClasses
Section titled “Classes”PascalCase for class names.
class ApiClient private global = invalid
sub new() m.global = GetGlobalAA().global end subend classPascalCase for enum names (they are type definitions). UPPER_SNAKE_CASE for enum members (they are immutable predefined values — constants).
enum MediaSegmentType UNKNOWN = "Unknown" INTRO = "Intro" OUTRO = "Outro"end enum
enum SubtitleSelection NOT_SET = -2 NONE = -1end enum
' Usageif segment.type = MediaSegmentType.INTROEnum values (the right side of =) must match external API contracts where applicable. Only the member names follow our convention.
Components
Section titled “Components”PascalCase for component names in XML, matching the file name.
<component name="ItemDetails" extends="JRScreen">Namespaces
Section titled “Namespaces”lowerCamelCase for namespace names. Namespaces are containers, not types.
namespace imageSize const POSTER_SM = { width: 108, height: 162 }end namespace
namespace sdk function getItems(params as object) end functionend namespaceConstants
Section titled “Constants”UPPER_SNAKE_CASE for all const declarations and namespace level constants.
const QUOTE = Chr(34)
namespace itemTypeOrder const SEARCH = ["Movie", "Series", "Episode"] const NO_RESUME = ["MusicAlbum", "MusicArtist"]end namespaceBooleans
Section titled “Booleans”Boolean variables and fields must use a prefix: is, has, should, can, or enable.
' CorrectisFolder = truehasSubtitles = falseshouldAutoPlay = truecanDelete = item.canDeleteenableNextEpisodeAutoPlay = true
' Incorrectfolder = truesubtitles = falseautoPlay = trueBoolean Naming Exceptions
Section titled “Boolean Naming Exceptions”The following boolean fields are exempt from the prefix rule:
- Signal fields: XML fields with
alwaysNotify="true"used purely as observable event triggers (e.g.,backPressed,refreshItemDetailsData). The value is irrelevant; the write event itself is the message. Examples:backPressed,exit,submit,reset,closeSidePanel,optionSelected,reloadHomeRequested,requestFocusReturn. - API-mirrored fields: Fields in
JellyfinUserConfigurationandJellyfinUserPolicythat mirror Jellyfin server API property names exactly (e.g.,playDefaultAudioTrack,enableNextEpisodeAutoPlay). These are external contracts. - Settings/registry keys:
JellyfinUserSettingsfield IDs that are 1:1 with Roku registry keys (e.g.,playbackCinemaMode,uiFontFallback). Renaming requires a registry migration to avoid user data loss.
Event-handler & Lifecycle Functions — on* Prefix
Section titled “Event-handler & Lifecycle Functions — on* Prefix”Functions registered as the callback target of an event-handler binding should start with on followed by an uppercase letter (camelCase). The bindings this convention applies to:
- XML
<field ... onChange="X" />—Xis invoked when the field changes. observeField(<field>, "X")—Xis invoked when the field changes (Roku Scene Graph two-argument string-callback form).- The
JRScreenlifecycle hookonDestroy()— required for everyJRScreensubclass; enforced by thejellyrock-jrscreen-on-destroyBSC plugin.
' Preferredsub onContentChanged() ...sub onItemSelectedChanged() ...sub onDestroy() ...<!-- Preferred --><field id="content" type="node" onChange="onContentChanged" />Not covered by this rule (intentional — these are not callback registrations):
init()— universal Roku component constructor.populate*,setup*,handle*,reset*,cleanup*— internal helpers, not callback registrations.callFunc(<name>, ...)invocations — general-purpose remote-procedure-call across the component graph; targets are setters / getters / methods, not event handlers.observeFieldoverloads passing a port or function reference (non-string-literal) — not a callback name registration.
This is convention, not strict enforcement outside of the onDestroy() lifecycle hook. The codebase has historical action-shaped callbacks (setColors, getData, updateMessage, etc.) that deliberately preserve their semantic names. Prefer on* for new callbacks; nudge existing ones at review time when they’re touched anyway. A binding-based BSC plugin to enforce this strictly was tried and dropped — the marginal value didn’t justify the friction of either renaming legacy action-shaped callbacks or sprinkling escape-hatch comments.
Private Class Members
Section titled “Private Class Members”Use the private keyword only. Do not use an underscore prefix — in BrighterScript, _ prefix means “unused parameter.”
class MyClass ' Correct private global = invalid private imageDefaults = {}
' Incorrect — _ means "unused", not "private" private _global = invalidend classUnused Parameters
Section titled “Unused Parameters”Use _ prefix for parameters that must exist in a signature but are not used.
function onKeyEvent(key as string, _press as boolean) as boolean ' _press is unused — only key matters here return key = "back"end functionFile Naming
Section titled “File Naming”Class, Component & Enum Files → PascalCase
Section titled “Class, Component & Enum Files → PascalCase”Files containing type definitions (classes, components, enums) use PascalCase.
source/api/ApiClient.bs ' class ApiClientsource/enums/MediaSegmentType.bs ' enum MediaSegmentTypecomponents/ItemDetails.xml ' component ItemDetailscomponents/ItemDetails.bs ' companion scriptUtility & Function Files → lowerCamelCase
Section titled “Utility & Function Files → lowerCamelCase”Files containing only functions, subs, or namespace definitions use lowerCamelCase. Single-word names are naturally valid.
source/utils/config.bs ' utility functionssource/utils/nodeHelpers.bs ' namespace nodeHelperssource/api/baseRequest.bs ' utility functionssource/constants/imageSize.bs ' namespace imageSizeXML + BS Pairing
Section titled “XML + BS Pairing”Component XML and BS files must share the same PascalCase base name. BrighterScript auto-scopes them together.
components/video/VideoPlayerView.xmlcomponents/video/VideoPlayerView.bsXML Conventions
Section titled “XML Conventions”Interface Field IDs → lowerCamelCase
Section titled “Interface Field IDs → lowerCamelCase”<interface> <field id="baseTitle" type="string" /> <field id="configKey" type="string" /> <field id="valueIndex" type="integer" /> <field id="globalSetting" type="boolean" value="false" /></interface>Child Element IDs → lowerCamelCase
Section titled “Child Element IDs → lowerCamelCase”<children> <LabelSecondarySmaller id="videoCodec" /> <LabelSecondarySmallest id="videoCodecCount" /></children>onChange Callbacks
Section titled “onChange Callbacks”onChange values are string references to function names. They must match the function definition exactly.
<field id="choices" type="array" onChange="updateTitle" />' Must match exactly — mismatch fails silently at runtimesub updateTitle() m.top.title = m.top.baseTitle + ": " + m.top.choices[m.top.valueIndex].displayend subFormatting
Section titled “Formatting”Indentation & Whitespace
Section titled “Indentation & Whitespace”- 2 spaces — no tabs (enforced by
.editorconfigandbsfmt.json) LFline endings (Unix-style)- Trim trailing whitespace
- Insert final newline
Strings & Comments
Section titled “Strings & Comments”- Double quotes for string values:
"hello" - Single quote (
') for comments:' This is a comment - Never use
REMor//for comments
Operators
Section titled “Operators”Single space around all binary operators:
result = a + bif isValid(item) and item.type = "Movie"url = serverUrl + "/" + pathvalue = apiData.Id ?? ""Import Ordering
Section titled “Import Ordering”Imports are sorted alphabetically (enforced by bsfmt.json with sortImports: true).
import "pkg:/source/api/baseRequest.bs"import "pkg:/source/utils/config.bs"import "pkg:/source/utils/misc.bs"Line Length
Section titled “Line Length”120 characters is the guideline. Not currently enforced by tooling — use developer judgment.
Comments
Section titled “Comments”JSDoc Style for Functions
Section titled “JSDoc Style for Functions”Every public function should have a JSDoc style comment describing its purpose, parameters, and return value.
' Filter registry keys to find those that should be deleted during a settings reset' Preserves session/identity keys, deletes everything else' @param allKeys - associative array of all registry key-value pairs' @param preserveKeys - array of key names to preserve' @return array of key names that should be deletedfunction getSettingKeysToDelete(allKeys as object, preserveKeys as object) as objectInline Comments
Section titled “Inline Comments”Use inline comments for complex logic, Roku-specific oddities, and non-obvious decisions.
' ContentNode fields return "" instead of invalid, so check bothif not isValid(serverUrl) or serverUrl = "" then return invalidSection Dividers
Section titled “Section Dividers”Use decorated comment blocks for major logical sections within large files.
' ════════════════════════════════════════' SECTION NAME' ════════════════════════════════════════BrightScript Specific Rules
Section titled “BrightScript Specific Rules”Roku Built-In Function & Method Casing
Section titled “Roku Built-In Function & Method Casing”BrightScript is case-insensitive for all identifiers, keywords, and function/method names. inStr(), Instr(), and INSTR() are identical at runtime. However, we use lowerCamelCase for Roku built-in functions and methods, consistent with our project-wide naming convention.
' Correct — lowerCamelCasepos = myString.inStr("search")idx = inStr(1, locale, "_")
' Incorrect — PascalCase / lowercasepos = myString.Instr("search")idx = Instr(1, locale, "_")Associative Array Key Casing
Section titled “Associative Array Key Casing”BrightScript stores AA keys in lowercase regardless of source code casing. { maxWidth: 1920 } stores the key as "maxwidth". This is a language limitation, not a convention violation.
When writing AA literals, still use lowerCamelCase in source for readability:
params = { maxWidth: 1920, imageType: "Primary"}' At runtime, keys are "maxwidth" and "imagetype"When checking AA keys by string, use lowercase:
if params.DoesExist("maxwidth") ' correct — matches runtime storageContentNode Field Defaults
Section titled “ContentNode Field Defaults”ContentNode fields cannot be invalid — they get type defaults when declared in XML. Use the “default = no data” pattern:
- Empty string
""= no data 0= no datafalse= no data- Check
item.typefor layout decisions, check field values for display decisions
API Boundary
Section titled “API Boundary”The Jellyfin API uses PascalCase field names (Id, Name, Type). These are transformed to lowerCamelCase at the API boundary in JellyfinDataTransformer. Never use PascalCase for internal data — the transformer is the conversion point.
' In JellyfinDataTransformer (the boundary)item.id = apiData.Id ?? "" ' PascalCase → `lowerCamelCase`item.name = apiData.Name ?? ""item.type = apiData.Type ?? ""
' In API request bodies (external contract — PascalCase required)FormatJson({ "Username": username, "Pw": password })Registry Keys — Do Not Rename
Section titled “Registry Keys — Do Not Rename”Registry keys like "saved_servers" and "active_user" are persisted on user devices. Renaming them requires a registry migration to avoid data loss. Treat existing registry key strings as frozen.
function vs sub
Section titled “function vs sub”Use function when returning a value. Use sub when performing a side effect with no return value.
function getSetting(key) as dynamic ' returns a value return registryRead(key, section)end function
sub setSetting(key, value) ' side effect only registryWrite(key, valueToString(value), section)end subGoto Labels
Section titled “Goto Labels”Use lowerCamelCase for goto labels. These are control flow markers, not constants or types.
startLogin: serverUrl = getSetting("server") ... goto startLoginTooling
Section titled “Tooling”| Tool | Config File | What It Enforces |
|---|---|---|
| bsfmt | bsfmt.json | Formatting (indentation, comment style, import sorting) |
| bslint | bslint.json | Case sensitivity, unused variables, unreachable code |
.editorconfig | .editorconfig | Indent style, line endings, trailing whitespace |
Not currently enforced by tooling: naming conventions (code review only), line length.
The IDE runs validate + bslint live; the .husky/pre-push hook runs the full lint suite on the files in your push range, with bsfmt auto-applied. You don’t need to run npm run lint manually except to debug a specific failure.
Markdown conventions
Section titled “Markdown conventions”These apply to .md files in docs/, every CLAUDE.md, AGENTS.md, and the root README.md.
Wrap code identifiers in backticks
Section titled “Wrap code identifiers in backticks”Anything that is code — variable / function / class / type / event names, file paths, environment variable names, npm script names, hook event names, file extensions, command names — goes in single backticks for inline references or fenced blocks for multi-line snippets. Plain English does not.
Concretely, the following are code and must be wrapped: Stop, sessionEnd, PostToolUse, stdout, stderr, JELLYROCK_TELEMETRY_DIR, .claude/settings.json, check-touched-lint.cjs, force: true, workflow_call, lint:docs.
Why this matters: the spell-checker (spellchecker-cli with retext-spell) treats text inside backticks as code and skips it, but flags the same identifier appearing in prose as a misspelling. Forgetting to wrap an identifier is the most common reason a previously-clean doc fails spell-check after an edit. The end-of-turn lint hook (scripts/lint/check-touched-lint.cjs) surfaces the failure the moment it happens, with a hint pointing back here.
When to add to dictionary.txt instead
Section titled “When to add to dictionary.txt instead”Add a word to dictionary.txt only when it’s a real English word the spell-checker doesn’t know — typically a legitimate technical or domain term (e.g., idempotence, untracked, transcoder). Code identifiers (e.g., sessionEnd, JRScreen, apiPool) should not be added — wrap them in backticks in prose instead. Putting identifiers in the dictionary defeats the spell-checker for any actual misspelling that happens to collide with an identifier.
Enforced by scripts/lint/dictionary-audit.cjs (npm run lint:dictionary) — a CI-blocking lint that flags PascalCase / camelCase / file-extension / path-shaped entries. Legitimate proper nouns (product names, brands, CS-domain conventions like camelCase) live in the script’s ALLOWLIST; acronym plurals (URIs, APIs, PNGs) and possessives (BSC's) are recognized via pattern. The end-of-turn lint hook surfaces the failure with a targeted “wrap in backticks” hint when the failing word looks identifier-shaped.