{"record":{"id":"d83f67a4db31f616","repo":"charmbracelet/vhs","slug":"modifiers-must-come-before-other-characters","errorCode":null,"errorMessage":"Modifiers must come before other characters","messagePattern":"Modifiers must come before other characters","errorType":"validation","errorClass":"parser.Error","httpStatus":null,"severity":"error","filePath":"parser/parser.go","lineNumber":306,"sourceCode":"// parseCtrl parses a control command.\n// A control command takes one or multiples characters and/or modifiers to type while ctrl is held down.\n//\n//\tCtrl[+Alt][+Shift]+<char>\n//\tE.g:\n//\tCtrl+Shift+O\n//\tCtrl+Alt+Shift+P\nfunc (p *Parser) parseCtrl() Command {\n\tvar args []string\n\n\tinModifierChain := true\n\tfor p.peek.Type == token.PLUS {\n\t\tp.nextToken()\n\t\tpeek := p.peek\n\n\t\t// Get key from keywords and check if it's a valid modifier\n\t\tif k := token.Keywords[peek.Literal]; token.IsModifier(k) {\n\t\t\tif !inModifierChain {\n\t\t\t\tp.errors = append(p.errors, NewError(p.cur, \"Modifiers must come before other characters\"))\n\t\t\t\t// Clear args so the error is returned\n\t\t\t\targs = nil\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\targs = append(args, peek.Literal)\n\t\t\tp.nextToken()\n\t\t\tcontinue\n\t\t}\n\n\t\tinModifierChain = false\n\n\t\t// Add key argument.\n\t\tswitch {\n\t\tcase peek.Type == token.ENTER,\n\t\t\tpeek.Type == token.SPACE,\n\t\t\tpeek.Type == token.BACKSPACE,\n\t\t\tpeek.Type == token.MINUS,","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/charmbracelet/vhs/blob/c073383b5de0b1f57bf514113029c306bc986539/parser/parser.go#L288-L324","documentation":"Ctrl in vhs accepts chains of modifier keys (Ctrl, Alt, Shift) followed by a regular key, e.g. 'Ctrl+Shift+a'. Modifiers must appear at the start of the chain; once a non-modifier key has been consumed, a later modifier is invalid. parseCtrl enforces this with inModifierChain and raises this error when a modifier keyword appears after the chain has ended.","triggerScenarios":"A modifier keyword (ctrl/alt/shift) appears after a regular key in a Ctrl argument list, e.g. 'Ctrl+a+Shift' or 'Ctrl+a+alt+b'.","commonSituations":"Reordering keys from muscle memory ('Ctrl+c+alt' instead of 'Ctrl+Alt+c'); writing key combos in the order pressed on a keyboard rather than modifier-first order.","solutions":["Reorder so all modifiers (Ctrl/Alt/Shift) come first, then the character key: 'Ctrl+Shift+a'.","Split into separate Ctrl commands if the combination is not a modifier chain."],"exampleFix":"// before (.tape)\nCtrl+a+Shift\n// after (.tape)\nCtrl+Shift+a","handlingStrategy":"validation","validationCode":"func isModifierFirst(keys []string) bool { i := 0; for i < len(keys) && isMod(keys[i]) { i++ }; for ; i < len(keys); i++ { if isMod(keys[i]) { return false } }; return true }\nfunc isMod(k string) bool { switch k { case \"Ctrl\", \"Alt\", \"Shift\": return true }; return false }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Order key combos modifier-first: Ctrl, Alt, Shift, then the character key","Think of the combo name ('Ctrl+Shift+a'), not the physical press order","Split unrelated combos into separate commands"],"tags":["parser","ctrl-command","key-order"],"backgroundTag":"invalid-key-combination","analyzedSha":"c073383b5de0b1f57bf514113029c306bc986539","analyzedAt":"2026-09-02T01:53:05.682Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}