SLOPSHOPPER

regex-lab

Test a regular expression against text and see matched ranges.

newpanecommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · regex-lab
│ ┃ Regex Lab ✕ › fix the failing auth test and add an audit log call │ ┃ Regex Lab │ ┃ ⏺ Read(src/auth.ts) │ ┃ Test a regular expression against text and ⎿ Read 6 lines │ ┃ see matched ranges. ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ Supported subset: character classes, ⏺ Bash(bun test) │ ┃ anchors, escapes, and one repetition; no ⎿ 3 pass, 1 fail │ ┃ groups or alternation. │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ Pattern: APP-\d+ ⏎ apply │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ Flags: g ⏎ apply │ ┃ › /regex-lab │ ┃ Text: Fix APP-123 before APP-456 ⏎ apply │ ┃ │ ┃ 2 matches │ ┃ │ ┃ [4, 11) APP-123 │ ┃ [19, 26) APP-456 │ ┃ │ ┃ [ Copy result ] │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Regex Lab
Regex Lab Test a regular expression against text and see matched ranges. Supported subset: character classes, anchors, escapes, and one repetition; no groups or alternation. Pattern: APP-\d+ ⏎ apply Flags: g ⏎ apply Text: Fix APP-123 before APP-456 ⏎ apply 2 matches [4, 11) APP-123 [19, 26) APP-456 [ Copy result ]
README

Regex Lab

Test a regular expression against text and see matched ranges.

Use it when: Debug a pattern before putting it in code.

Regex Lab render preview

Screenshot of this mod's render tree in the fixture preview, with sample data. This is not a capture of the Claude Code application. How previews are made.

Install and open

claude plugin marketplace add whyashthakker/awesome-claude-code-mods
claude plugin install regex-lab@awesome-claude-code-mods

Run /reload-plugins in an existing session, then /regex-lab. Use Tab and Enter to operate controls; Esc closes the pane. Terminal and Desktop Code tab supported; pane UI is unavailable in claude -p and the VS Code chat panel.

Try the source without installing:

claude --plugin-dir ./mods/regex-lab

Access and behavior

Works on the text you enter, in memory. Uses the clipboard on request. No files, processes, network, persistence, or model calls.

Module variables reset on hot reload. All display output is bounded; viewers may truncate long content to 9,000 characters. Relative file paths and Git commands use the session working directory.

Verify

Tested with Claude Code 2.1.288. Minimum documented mods version: 2.1.287. The API can change; validate against your installed version.

claude plugin validate mods/regex-lab --strict
claude plugin test mods/regex-lab

Tests cover plugin commands, pane isolation, both UI surfaces, and the behavior described in tests/register.test.ts. Drawing tests validate element trees; they do not verify pixels painted by the native apps. Validation details.

MIT licensed. No runtime packages or build step required.

Source 2 files
hooks/register.js 67 lines
1import { clean, text, code, row, frame, bar, pretty, bounded, entries, safePath } from './ui.js'
2const ID = "regex-lab"
3let pattern="APP-\\d+"
4let flags="g"
5let source="Fix APP-123 before APP-456"
6let output=""
7
8
9function transform() {
10 bounded(source,2000);bounded(pattern,160)
11 if(!/^[gimsuy]*$/.test(flags))throw new Error('Flags must be g, i, m, s, u, or y.')
12 // Use a predictable subset rather than executing arbitrary backtracking patterns.
13 let inClass=false,repeat=0
14 for(let i=0;i<pattern.length;i++) {
15  const c=pattern[i]
16  if(c==='\\') {
17   const escape=pattern[++i]
18   if(/[1-9k]/.test(escape||''))throw new Error('Backreferences are refused.')
19   continue
20  }
21  if(c==='[')inClass=true
22  else if(c===']')inClass=false
23  else if(!inClass) {
24   if('()|'.includes(c))throw new Error('Groups and alternation are outside the supported subset.')
25   if('*+?{'.includes(c) && ++repeat>1)throw new Error('Use at most one repetition operator in this lab.')
26  }
27 }
28 const re=new RegExp(pattern,flags.includes('g')?flags:flags+'g')
29 const found=Array.from(source.matchAll(re)).slice(0,40)
30 return found.length+' matches\n\n'+found.map(m=>'['+m.index+', '+(m.index+m[0].length)+') '+m[0]).join('\n')
31}
32
33async function calculate($) {
34 try { output=await transform() } catch(error){output='Invalid input: '+error.message}
35 $.ui.invalidate('ui.render')
36}
37
38export function register(on) {
39  on('session.start', async ($, e, next) => {
40
41    await $.command.register({name:ID, description:"Test a regular expression against text and see matched ranges.", immediate:true, argumentHint:""})
42    return next(e)
43  })
44  on('command.run', {command:"regex-lab"}, async ($, e) => {
45    await calculate($)
46    await $.ui.open({id:ID,title:"Regex Lab",focus:true,closeOnEscape:true})
47    return {}
48  })
49
50  on('ui.render', {component:'Pane'}, async ($, e, next) => {
51    if(e.requestId !== ID) return next(e)
52    const E = $.ui.resolve(e)
53    try {
54      const body = await draw($, E)
55      return frame(E,"Regex Lab","Test a regular expression against text and see matched ranges.",body)
56    } catch(error) {
57      return frame(E,"Regex Lab",'Unable to inspect this data.',[text(E,error.message,{color:'yellow'}),E.Button({key:'refresh',label:'Refresh',onPress:()=>$.ui.invalidate('ui.render')})])
58    }
59  })
60}
61async function draw($, E) {
62  const refresh = E.Button({key:'refresh',label:'Refresh',hotkey:'r',plain:true,onPress:()=>$.ui.invalidate('ui.render')})
63return [text(E,'Supported subset: character classes, anchors, escapes, and one repetition; no groups or alternation.'),E.Input({key:"pattern",label:"Pattern",value:clean(pattern),submitLabel:'apply',onSubmit:async(v)=>{pattern=v;await calculate($)}}),
64E.Input({key:"flags",label:"Flags",value:clean(flags),submitLabel:'apply',onSubmit:async(v)=>{flags=v;await calculate($)}}),
65E.Input({key:"source",label:"Text",value:clean(source),submitLabel:'apply',onSubmit:async(v)=>{source=v;await calculate($)}}),code(E,output),E.Button({key:'copy',label:'Copy result',onPress:async()=>{const r=await $.ui.copy({text:output});if(!r.isCopied)$.ui.toast('Clipboard unavailable.')}})]
66}
67
hooks/ui.js 36 lines
1// Pure UI helpers. No engine access; each plugin contains its own copy.
2export function clean(value, limit = 9000) {
3  return String(value ?? '').replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '').slice(0, limit)
4}
5export function text(E, value, props = {}) {
6  return E.Text({ ...props, children: [clean(value)] })
7}
8export function code(E, value, language = 'text') {
9  return E.Code({ source: clean(value), language })
10}
11export function row(E, children) {
12  return E.Box({ flexDirection: 'row', columnGap: 2, flexWrap: 'wrap', children })
13}
14export function frame(E, title, subtitle, children) {
15  return E.Box({ flexDirection: 'column', gap: 1, children: [
16    text(E, title, { bold: true, color: 'cyan' }),
17    text(E, subtitle, { dimColor: true }), ...children,
18  ] })
19}
20export function bar(percent, width = 24) {
21  const p = Math.max(0, Math.min(100, Number(percent) || 0))
22  const n = Math.round(p / 100 * width)
23  return '[' + '#'.repeat(n) + '-'.repeat(width - n) + '] ' + p.toFixed(1) + '%'
24}
25export function pretty(value) { return JSON.stringify(value, null, 2) }
26export function bounded(value, max = 9000) {
27  if (value.length > max) throw new Error('Input exceeds ' + max + ' characters. Use a smaller selection.')
28  return value
29}
30export function entries(value) { return value && typeof value === 'object' ? Object.entries(value) : [] }
31export function safePath(value) {
32  const path = value.trim()
33  if (!path || path.startsWith('-') || /[\x00-\x1f]/.test(path)) throw new Error('Enter a path without control characters or leading flags.')
34  return path
35}
36