gwbasickernel 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ # GW-BASIC Jupyter kernel
2
+
3
+ Run GW-BASIC programs in JupyterLab or Notebook. Variables and program lines
4
+ persist between cells. The kernel converts the interpreter's Sixel output to
5
+ inline PNG images, so the notebook browser does not need Sixel support.
6
+
7
+ Requires Python 3.10 or later and the native
8
+ [GW-BASIC 2026 interpreter](https://github.com/evvaletov/gw-basic-2026)
9
+ on Linux or macOS. The Python package does not include that interpreter.
10
+ The DOS executables cannot be used as the notebook backend.
11
+
12
+ ## Install the interpreter
13
+
14
+ With Homebrew on Linux or macOS:
15
+
16
+ ```sh
17
+ brew tap evvaletov/gwbasic https://github.com/evvaletov/gw-basic-2026.git
18
+ brew install evvaletov/gwbasic/gwbasic
19
+ ```
20
+
21
+ The Homebrew formula builds from a pinned source revision. It includes the
22
+ interpreter and native compiler, with PulseAudio disabled.
23
+
24
+ Alternatively, build from source with a C11 compiler and CMake:
25
+
26
+ ```sh
27
+ git clone https://github.com/evvaletov/gw-basic-2026.git
28
+ cd gw-basic-2026
29
+ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
30
+ cmake --build build --parallel 2
31
+ export GWBASIC="$PWD/build/gwbasic"
32
+ ```
33
+
34
+ ## Install the kernel
35
+
36
+ Create a Python environment for the kernel and JupyterLab:
37
+
38
+ ```sh
39
+ python3 -m venv .venv
40
+ . .venv/bin/activate
41
+ python -m pip install gwbasickernel jupyterlab
42
+ gwbasickernel-install --sys-prefix
43
+ jupyter lab
44
+ ```
45
+
46
+ Choose **GW-BASIC 2026** when creating a notebook. The kernel looks for the
47
+ interpreter in `GWBASIC`, then on `PATH`. To save its location in the kernelspec,
48
+ use this command instead of the installer command above:
49
+
50
+ ```sh
51
+ gwbasickernel-install --sys-prefix --gwbasic /absolute/path/to/gwbasic
52
+ ```
53
+
54
+ If Jupyter runs in another Python environment, use `--user` to register the
55
+ kernel for your account. Keep the kernel's Python environment in place:
56
+ the kernelspec records its Python executable.
57
+
58
+ ## Try it
59
+
60
+ ```basic
61
+ 10 FOR N=1 TO 8
62
+ 20 PRINT N;N*N
63
+ 30 NEXT N
64
+ RUN
65
+ ```
66
+
67
+ For graphics, clear the previous program first:
68
+
69
+ ```basic
70
+ NEW
71
+ 10 SCREEN 1
72
+ 20 CIRCLE (160,100),80,2
73
+ RUN
74
+ ```
75
+
76
+ See the [executed graphics notebook](https://github.com/evvaletov/gw-basic-2026/blob/main/examples/graphics.ipynb)
77
+ and [demo guide](https://github.com/evvaletov/gw-basic-2026/blob/main/docs/demos.md).
78
+
79
+ `%new` clears the BASIC program; `%reset` starts a fresh interpreter process.
80
+ `%timeout 60` changes the output wait timeout from its default of 30 seconds.
@@ -0,0 +1,3 @@
1
+ """Jupyter kernel for GW-BASIC 2026."""
2
+
3
+ __version__ = '0.1.0'
@@ -0,0 +1,4 @@
1
+ from ipykernel.kernelapp import IPKernelApp
2
+ from .kernel import GWBasicKernel
3
+
4
+ IPKernelApp.launch_instance(kernel_class=GWBasicKernel)
@@ -0,0 +1,79 @@
1
+ """Pygments lexer for GW-BASIC."""
2
+
3
+ from pygments.lexer import RegexLexer
4
+ from pygments.token import (
5
+ Comment, Keyword, Name, Number, Operator, Punctuation, String, Text,
6
+ )
7
+
8
+
9
+ class GWBasicLexer(RegexLexer):
10
+ name = 'GW-BASIC'
11
+ aliases = ['gwbasic', 'basic']
12
+ filenames = ['*.bas', '*.BAS']
13
+ mimetypes = ['text/x-basic']
14
+
15
+ tokens = {
16
+ 'root': [
17
+ # REM comments (to end of line)
18
+ (r"(?i)\bREM\b.*$", Comment.Single),
19
+ # Single-quote comments
20
+ (r"'.*$", Comment.Single),
21
+
22
+ # String literals
23
+ (r'"[^"]*"', String.Double),
24
+
25
+ # Line numbers at start of line
26
+ (r'^\s*\d+', Name.Label),
27
+
28
+ # Hex and octal literals
29
+ (r'&[Hh][0-9A-Fa-f]+', Number.Hex),
30
+ (r'&[Oo]?[0-7]+', Number.Oct),
31
+
32
+ # Floating point
33
+ (r'\d+\.\d*([eEdD][+-]?\d+)?[!#]?', Number.Float),
34
+ (r'\d+[eEdD][+-]?\d+[!#]?', Number.Float),
35
+
36
+ # Integer
37
+ (r'\d+[%!#]?', Number.Integer),
38
+
39
+ # Statements / keywords
40
+ (r'(?i)\b(AUTO|BEEP|BLOAD|BSAVE|CALL|CALLS|CHAIN|CHDIR|CIRCLE'
41
+ r'|CLEAR|CLOSE|CLS|COLOR|COM|COMMON|CONT|DATA|DEF\s+SEG'
42
+ r'|DEF\s+FN|DEFDBL|DEFINT|DEFSNG|DEFSTR|DELETE|DIM|DRAW'
43
+ r'|EDIT|ELSE|END|ENVIRON|ERASE|ERROR|FIELD|FILES|FOR|GET'
44
+ r'|GOSUB|GOTO|IF|INPUT|KEY|KILL|LCOPY|LET|LINE\s+INPUT'
45
+ r'|LINE|LIST|LLIST|LOAD|LOCATE|LPRINT|LSET|MERGE|MKDIR'
46
+ r'|MOTOR|NAME|NEW|NEXT|ON|OPEN|OPTION\s+BASE|OUT|PAINT'
47
+ r'|PALETTE|PLAY|POKE|PRESET|PRINT|PSET|PUT|RANDOMIZE'
48
+ r'|READ|REM|RENUM|RESET|RESTORE|RESUME|RETURN|RMDIR|RSET'
49
+ r'|RUN|SAVE|SCREEN|SHELL|SOUND|STOP|SWAP|SYSTEM|THEN|TIMER'
50
+ r'|TO|TRON|TROFF|USING|VIEW|WAIT|WEND|WHILE|WIDTH|WINDOW'
51
+ r'|WRITE)\b', Keyword),
52
+
53
+ # Built-in functions
54
+ (r'(?i)\b(ABS|ASC|ATN|CDBL|CHR\$|CINT|COS|CSNG|CSRLIN'
55
+ r'|CVD|CVI|CVS|DATE\$|EOF|ENVIRON\$|ERDEV\$?|ERL|ERR'
56
+ r'|EXP|FIX|FN|FRE|HEX\$|INKEY\$|INP|INPUT\$|INSTR|INT'
57
+ r'|IOCTL\$|LEFT\$|LEN|LOC|LOF|LOG|LPOS|MID\$|MKD\$'
58
+ r'|MKI\$|MKS\$|OCT\$|PEEK|PEN|PMAP|POINT|POS|RIGHT\$'
59
+ r'|RND|SGN|SIN|SPACE\$|SPC|SQR|STICK|STR\$|STRIG'
60
+ r'|STRING\$|TAB|TAN|TIME\$|TIMER|USR|VAL|VARPTR\$?)\b',
61
+ Name.Builtin),
62
+
63
+ # Logical / arithmetic operators as keywords
64
+ (r'(?i)\b(AND|EQV|IMP|MOD|NOT|OR|XOR|STEP)\b', Operator.Word),
65
+
66
+ # Operators
67
+ (r'[+\-*/\\^<>=]', Operator),
68
+ (r'<>|<=|>=', Operator),
69
+
70
+ # Punctuation
71
+ (r'[(),;:#]', Punctuation),
72
+
73
+ # Identifiers (variable names)
74
+ (r'[A-Za-z][A-Za-z0-9]*[%!#$]?', Name),
75
+
76
+ # Whitespace
77
+ (r'\s+', Text),
78
+ ],
79
+ }
@@ -0,0 +1,60 @@
1
+ """Install the GW-BASIC kernel into Jupyter."""
2
+
3
+ import argparse
4
+ import json
5
+ import shutil
6
+ import sys
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+
11
+ def install_kernel(user=True, prefix=None, gwbasic=None):
12
+ from jupyter_client.kernelspec import KernelSpecManager
13
+
14
+ spec = json.loads((Path(__file__).parent / 'kernel.json').read_text())
15
+ spec['argv'][0] = sys.executable
16
+ if gwbasic is not None:
17
+ binary = shutil.which(str(gwbasic))
18
+ if binary is None:
19
+ raise ValueError(f'GW-BASIC executable not found: {gwbasic}')
20
+ spec['env'] = {'GWBASIC': str(Path(binary).absolute())}
21
+
22
+ with tempfile.TemporaryDirectory() as td:
23
+ dest = Path(td) / 'gwbasic'
24
+ dest.mkdir()
25
+ (dest / 'kernel.json').write_text(json.dumps(spec, indent=2) + '\n')
26
+
27
+ ksm = KernelSpecManager()
28
+ ksm.install_kernel_spec(
29
+ str(dest), kernel_name='gwbasic', user=user, prefix=prefix,
30
+ )
31
+
32
+ print('GW-BASIC kernel installed successfully.')
33
+ print('Run "jupyter kernelspec list" to verify.')
34
+
35
+
36
+ def main():
37
+ parser = argparse.ArgumentParser(description='Install the GW-BASIC Jupyter kernel')
38
+ location = parser.add_mutually_exclusive_group()
39
+ location.add_argument('--user', action='store_true',
40
+ help='Install for the current user (default)')
41
+ location.add_argument('--sys-prefix', action='store_true',
42
+ help='Install into sys.prefix (for virtualenvs)')
43
+ location.add_argument('--prefix', type=str, default=None,
44
+ help='Install into a specific prefix')
45
+ parser.add_argument('--gwbasic', help='Interpreter executable to save in the kernelspec')
46
+ args = parser.parse_args()
47
+
48
+ try:
49
+ if args.sys_prefix:
50
+ install_kernel(user=False, prefix=sys.prefix, gwbasic=args.gwbasic)
51
+ elif args.prefix:
52
+ install_kernel(user=False, prefix=args.prefix, gwbasic=args.gwbasic)
53
+ else:
54
+ install_kernel(gwbasic=args.gwbasic)
55
+ except ValueError as exc:
56
+ parser.error(str(exc))
57
+
58
+
59
+ if __name__ == '__main__':
60
+ main()
@@ -0,0 +1,8 @@
1
+ {
2
+ "argv": ["python", "-m", "gwbasickernel", "-f", "{connection_file}"],
3
+ "display_name": "GW-BASIC 2026",
4
+ "language": "basic",
5
+ "metadata": {
6
+ "debugger": false
7
+ }
8
+ }
@@ -0,0 +1,575 @@
1
+ """Jupyter kernel for GW-BASIC 2026.
2
+
3
+ Persistent process model: a long-running gwbasic subprocess reads BASIC
4
+ commands from stdin and writes output to stdout. The kernel sends each
5
+ notebook cell as one or more lines of BASIC, followed by a sentinel
6
+ PRINT that emits a known delimiter. Output is captured until the
7
+ delimiter appears, then forwarded to the notebook.
8
+
9
+ GW-BASIC's piped mode suppresses the banner and "Ok" prompt automatically,
10
+ and stdout is unbuffered, making it ideal for subprocess communication.
11
+
12
+ Features beyond basic execution:
13
+ - Inline Sixel graphics: ESC P ... ESC \\ sequences are extracted from
14
+ the output stream, decoded into PNG images, and displayed inline.
15
+ - INPUT statement support: when gwbasic prints "? " (the INPUT prompt),
16
+ the kernel requests input from the notebook front-end via the Jupyter
17
+ stdin protocol.
18
+ - Pygments lexer: GW-BASIC syntax highlighting in notebooks.
19
+ - Tab completion for all GW-BASIC keywords.
20
+ """
21
+
22
+ import base64
23
+ import io
24
+ import os
25
+ import re
26
+ import select
27
+ import shutil
28
+ import signal
29
+ import struct
30
+ import subprocess
31
+ import zlib
32
+
33
+ from ipykernel.kernelbase import Kernel
34
+
35
+ from . import __version__
36
+
37
+ _SENTINEL = '\x01GWDONE\x01'
38
+ _SENTINEL_CMD = 'PRINT CHR$(1)+"GWDONE"+CHR$(1)\n'
39
+
40
+ _ERROR_RE = re.compile(
41
+ r'(?:Syntax error|Illegal function call|Type mismatch|Out of |'
42
+ r'Division by zero|Overflow|RETURN without GOSUB|'
43
+ r'NEXT without FOR|Undefined |Subscript out of range|'
44
+ r'Redo from start|String too long|'
45
+ r'WHILE without WEND|WEND without WHILE|'
46
+ r'File not found|File already open|Bad file |'
47
+ r'Direct statement in file|Missing operand|'
48
+ r'Duplicate Definition|Formula too complex|'
49
+ r'Internal error|Unprintable error)',
50
+ re.IGNORECASE,
51
+ )
52
+
53
+ # DCS (ESC P) starts Sixel, ST (ESC \) ends it — both as bytes
54
+ _DCS = b'\x1bP'
55
+ _ST = b'\x1b\\'
56
+
57
+ GW_KEYWORDS = [
58
+ 'AUTO', 'BEEP', 'BLOAD', 'BSAVE', 'CALL', 'CHAIN', 'CHDIR',
59
+ 'CIRCLE', 'CLEAR', 'CLOSE', 'CLS', 'COLOR', 'COM', 'COMMON',
60
+ 'CONT', 'DATA', 'DATE$', 'DEF', 'DEFDBL', 'DEFINT', 'DEFSNG',
61
+ 'DEFSTR', 'DELETE', 'DIM', 'DRAW', 'EDIT', 'ELSE', 'END',
62
+ 'ENVIRON', 'ENVIRON$', 'ERASE', 'ERDEV', 'ERDEV$', 'ERR', 'ERL',
63
+ 'ERROR', 'FIELD', 'FILES', 'FN', 'FOR', 'GET', 'GOSUB', 'GOTO',
64
+ 'IF', 'INPUT', 'IOCTL', 'IOCTL$', 'KEY', 'KILL', 'LCOPY',
65
+ 'LET', 'LINE', 'LIST', 'LLIST', 'LOAD', 'LOCATE', 'LPRINT',
66
+ 'LSET', 'MERGE', 'MKDIR', 'MOTOR', 'NAME', 'NEW', 'NEXT',
67
+ 'NOISE', 'ON', 'OPEN', 'OPTION', 'OUT', 'PAINT', 'PALETTE',
68
+ 'PLAY', 'POKE', 'PRESET', 'PRINT', 'PSET', 'PUT', 'RANDOMIZE',
69
+ 'READ', 'REM', 'RENUM', 'RESET', 'RESTORE', 'RESUME', 'RETURN',
70
+ 'RMDIR', 'RSET', 'RUN', 'SAVE', 'SCREEN', 'SHELL', 'SOUND',
71
+ 'STOP', 'SWAP', 'SYSTEM', 'THEN', 'TIME$', 'TIMER', 'TO',
72
+ 'TRON', 'TROFF', 'VIEW', 'WAIT', 'WEND', 'WHILE', 'WIDTH',
73
+ 'WINDOW', 'WRITE',
74
+ 'ABS', 'ASC', 'ATN', 'CDBL', 'CHR$', 'CINT', 'COS', 'CSNG',
75
+ 'CSRLIN', 'CVD', 'CVI', 'CVS', 'EOF', 'EXP', 'FIX', 'FRE',
76
+ 'HEX$', 'INKEY$', 'INP', 'INPUT$', 'INSTR', 'INT', 'LEFT$',
77
+ 'LEN', 'LOC', 'LOF', 'LOG', 'LPOS', 'MID$', 'MKD$', 'MKI$',
78
+ 'MKS$', 'OCT$', 'PEEK', 'PEN', 'PMAP', 'POINT', 'POS',
79
+ 'RIGHT$', 'RND', 'SGN', 'SIN', 'SPACE$', 'SPC', 'SQR',
80
+ 'STICK', 'STR$', 'STRIG', 'STRING$', 'TAB', 'TAN', 'USR',
81
+ 'VAL', 'VARPTR', 'VARPTR$',
82
+ 'AND', 'EQV', 'IMP', 'MOD', 'NOT', 'OR', 'XOR', 'STEP',
83
+ ]
84
+
85
+
86
+ # ---------- Sixel decoder (pure Python, no PIL dependency) ----------
87
+
88
+ def _parse_sixel_palette(data):
89
+ """Parse #n;2;r;g;b palette definitions from Sixel data."""
90
+ palette = {}
91
+ for m in re.finditer(rb'#(\d+);2;(\d+);(\d+);(\d+)', data):
92
+ idx = int(m.group(1))
93
+ r = int(m.group(2)) * 255 // 100
94
+ g = int(m.group(3)) * 255 // 100
95
+ b = int(m.group(4)) * 255 // 100
96
+ palette[idx] = (r, g, b)
97
+ return palette
98
+
99
+
100
+ def _decode_sixel(data):
101
+ """Decode Sixel data into an RGBA image. Returns (width, height, rgba_bytes)."""
102
+ # Strip DCS/ST wrappers if present
103
+ if data.startswith(_DCS):
104
+ data = data[len(_DCS):]
105
+ if data.endswith(_ST):
106
+ data = data[:-len(_ST)]
107
+
108
+ # Skip mode char (usually 'q')
109
+ if data and data[0:1] == b'q':
110
+ data = data[1:]
111
+
112
+ palette = _parse_sixel_palette(data)
113
+ if not palette:
114
+ palette = {0: (0, 0, 0)}
115
+
116
+ # First pass: determine image dimensions
117
+ x = 0
118
+ y = 0
119
+ max_x = 0
120
+ max_y = 0
121
+ color = 0
122
+ i = 0
123
+ while i < len(data):
124
+ ch = data[i]
125
+ if ch == ord('#'):
126
+ # Color selector or palette def — skip palette defs
127
+ j = i + 1
128
+ num = b''
129
+ while j < len(data) and (data[j] in b'0123456789'):
130
+ num += bytes([data[j]])
131
+ j += 1
132
+ if j < len(data) and data[j] == ord(';'):
133
+ # Palette definition — skip to end
134
+ while j < len(data) and data[j] not in b'#$-\x1b' and not (63 <= data[j] <= 126):
135
+ j += 1
136
+ i = j
137
+ continue
138
+ color = int(num) if num else 0
139
+ i = j
140
+ elif ch == ord('$'):
141
+ x = 0
142
+ i += 1
143
+ elif ch == ord('-'):
144
+ x = 0
145
+ y += 6
146
+ i += 1
147
+ elif ch == ord('!'):
148
+ # RLE: !count<char>
149
+ j = i + 1
150
+ num = b''
151
+ while j < len(data) and data[j] in b'0123456789':
152
+ num += bytes([data[j]])
153
+ j += 1
154
+ count = int(num) if num else 1
155
+ if j < len(data) and 63 <= data[j] <= 126:
156
+ x += count
157
+ j += 1
158
+ max_x = max(max_x, x)
159
+ max_y = max(max_y, y + 6)
160
+ i = j
161
+ elif 63 <= ch <= 126:
162
+ x += 1
163
+ max_x = max(max_x, x)
164
+ max_y = max(max_y, y + 6)
165
+ i += 1
166
+ else:
167
+ i += 1
168
+
169
+ if max_x == 0 or max_y == 0:
170
+ return 0, 0, b''
171
+
172
+ width, height = max_x, max_y
173
+ # RGBA buffer (transparent background)
174
+ pixels = bytearray(width * height * 4)
175
+
176
+ # Second pass: render pixels
177
+ x = 0
178
+ y = 0
179
+ color = 0
180
+ i = 0
181
+ while i < len(data):
182
+ ch = data[i]
183
+ if ch == ord('#'):
184
+ j = i + 1
185
+ num = b''
186
+ while j < len(data) and data[j] in b'0123456789':
187
+ num += bytes([data[j]])
188
+ j += 1
189
+ if j < len(data) and data[j] == ord(';'):
190
+ while j < len(data) and data[j] not in b'#$-\x1b' and not (63 <= data[j] <= 126):
191
+ j += 1
192
+ i = j
193
+ continue
194
+ color = int(num) if num else 0
195
+ i = j
196
+ elif ch == ord('$'):
197
+ x = 0
198
+ i += 1
199
+ elif ch == ord('-'):
200
+ x = 0
201
+ y += 6
202
+ i += 1
203
+ elif ch == ord('!'):
204
+ j = i + 1
205
+ num = b''
206
+ while j < len(data) and data[j] in b'0123456789':
207
+ num += bytes([data[j]])
208
+ j += 1
209
+ count = int(num) if num else 1
210
+ if j < len(data) and 63 <= data[j] <= 126:
211
+ sixel = data[j] - 63
212
+ j += 1
213
+ r, g, b = palette.get(color, (0, 0, 0))
214
+ for dx in range(count):
215
+ cx = x + dx
216
+ if cx >= width:
217
+ break
218
+ for bit in range(6):
219
+ if sixel & (1 << bit):
220
+ cy = y + bit
221
+ if cy < height:
222
+ off = (cy * width + cx) * 4
223
+ pixels[off] = r
224
+ pixels[off + 1] = g
225
+ pixels[off + 2] = b
226
+ pixels[off + 3] = 255
227
+ x += count
228
+ i = j
229
+ elif 63 <= ch <= 126:
230
+ sixel = ch - 63
231
+ r, g, b = palette.get(color, (0, 0, 0))
232
+ if x < width:
233
+ for bit in range(6):
234
+ if sixel & (1 << bit):
235
+ cy = y + bit
236
+ if cy < height:
237
+ off = (cy * width + x) * 4
238
+ pixels[off] = r
239
+ pixels[off + 1] = g
240
+ pixels[off + 2] = b
241
+ pixels[off + 3] = 255
242
+ x += 1
243
+ i += 1
244
+ else:
245
+ i += 1
246
+
247
+ return width, height, bytes(pixels)
248
+
249
+
250
+ def _rgba_to_png(width, height, rgba):
251
+ """Encode raw RGBA pixels as PNG (pure Python, no dependencies)."""
252
+ def _chunk(tag, data):
253
+ c = tag + data
254
+ return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xFFFFFFFF)
255
+
256
+ ihdr = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0)
257
+ raw = b''
258
+ stride = width * 4
259
+ for y in range(height):
260
+ raw += b'\x00' + rgba[y * stride:(y + 1) * stride]
261
+ idat = zlib.compress(raw)
262
+
263
+ out = b'\x89PNG\r\n\x1a\n'
264
+ out += _chunk(b'IHDR', ihdr)
265
+ out += _chunk(b'IDAT', idat)
266
+ out += _chunk(b'IEND', b'')
267
+ return out
268
+
269
+
270
+ # ---------- Kernel class ----------
271
+
272
+ class GWBasicKernel(Kernel):
273
+ implementation = 'gwbasickernel'
274
+ implementation_version = __version__
275
+ language = 'basic'
276
+ language_version = '2026'
277
+ language_info = {
278
+ 'name': 'basic',
279
+ 'mimetype': 'text/x-basic',
280
+ 'file_extension': '.bas',
281
+ 'codemirror_mode': 'vb',
282
+ 'pygments_lexer': 'gwbasickernel.basic_lexer.GWBasicLexer',
283
+ }
284
+ banner = 'GW-BASIC 2026 Jupyter Kernel'
285
+
286
+ def __init__(self, **kwargs):
287
+ super().__init__(**kwargs)
288
+ self._proc = None
289
+ self._timeout = int(os.environ.get('GWBASIC_TIMEOUT', '30'))
290
+
291
+ def _find_binary(self):
292
+ env = os.environ.get('GWBASIC')
293
+ if env and os.path.isfile(env) and os.access(env, os.X_OK):
294
+ return env
295
+ for candidate in ['gwbasic', './build/gwbasic', './gwbasic']:
296
+ found = shutil.which(candidate)
297
+ if found:
298
+ return found
299
+ return None
300
+
301
+ def _start_process(self):
302
+ self._kill_process()
303
+ binary = self._find_binary()
304
+ if not binary:
305
+ return False
306
+ self._proc = subprocess.Popen(
307
+ [binary],
308
+ stdin=subprocess.PIPE,
309
+ stdout=subprocess.PIPE,
310
+ stderr=subprocess.STDOUT,
311
+ bufsize=0,
312
+ )
313
+ return True
314
+
315
+ def _kill_process(self):
316
+ if self._proc is not None:
317
+ try:
318
+ self._proc.kill()
319
+ self._proc.wait(timeout=5)
320
+ except (ProcessLookupError, subprocess.TimeoutExpired):
321
+ pass
322
+ self._proc = None
323
+
324
+ def _is_alive(self):
325
+ return self._proc is not None and self._proc.poll() is None
326
+
327
+ def _send_interrupt_children(self):
328
+ if self._is_alive():
329
+ self._proc.send_signal(signal.SIGINT)
330
+ else:
331
+ super()._send_interrupt_children()
332
+
333
+ def _read_until_sentinel(self, allow_stdin=False):
334
+ """Read raw bytes from stdout until sentinel or timeout.
335
+
336
+ When allow_stdin is True and the output ends with "? " (INPUT prompt),
337
+ the kernel requests input from the notebook front-end and writes the
338
+ response to the subprocess's stdin.
339
+
340
+ Returns (raw_bytes, status).
341
+ """
342
+ fd = self._proc.stdout.fileno()
343
+ buf = b''
344
+ sentinel_bytes = _SENTINEL.encode()
345
+
346
+ while True:
347
+ try:
348
+ ready, _, _ = select.select([fd], [], [], self._timeout)
349
+ except (KeyboardInterrupt, InterruptedError):
350
+ if self._is_alive():
351
+ self._proc.send_signal(signal.SIGINT)
352
+ return buf, 'interrupted'
353
+
354
+ if not ready:
355
+ # Check for INPUT prompt before declaring timeout
356
+ if allow_stdin and buf.endswith(b'? '):
357
+ reply = self.raw_input('? ')
358
+ self._proc.stdin.write((reply + '\n').encode())
359
+ self._proc.stdin.flush()
360
+ continue
361
+ return buf, 'timeout'
362
+
363
+ chunk = os.read(fd, 8192)
364
+ if not chunk:
365
+ return buf, 'died'
366
+
367
+ buf += chunk
368
+
369
+ # Check for sentinel in the buffer
370
+ if sentinel_bytes in buf:
371
+ idx = buf.index(sentinel_bytes)
372
+ # Keep everything before the sentinel, stripping the
373
+ # PRINT output that contains it (newline + sentinel + newline)
374
+ before = buf[:idx]
375
+ # Remove trailing newline left by PRINT before sentinel
376
+ if before.endswith(b'\n'):
377
+ before = before[:-1]
378
+ return before, 'done'
379
+
380
+ # Check for INPUT prompt: "? " at end of buffer (no newline after)
381
+ if allow_stdin and buf.endswith(b'? '):
382
+ last_nl = buf.rfind(b'\n')
383
+ tail = buf[last_nl + 1:] if last_nl >= 0 else buf
384
+ if b'\n' not in tail[:-2]: # no embedded newline before "? "
385
+ prompt_line = tail.decode('utf-8', errors='replace')
386
+ reply = self.raw_input(prompt_line)
387
+ self._proc.stdin.write((reply + '\n').encode())
388
+ self._proc.stdin.flush()
389
+
390
+ def _split_sixel(self, raw):
391
+ """Split raw output into (text_parts, sixel_blobs) interleaved."""
392
+ parts = []
393
+ pos = 0
394
+ while True:
395
+ dcs_idx = raw.find(_DCS, pos)
396
+ if dcs_idx == -1:
397
+ parts.append(('text', raw[pos:]))
398
+ break
399
+ if dcs_idx > pos:
400
+ parts.append(('text', raw[pos:dcs_idx]))
401
+ st_idx = raw.find(_ST, dcs_idx)
402
+ if st_idx == -1:
403
+ parts.append(('text', raw[pos:]))
404
+ break
405
+ end = st_idx + len(_ST)
406
+ parts.append(('sixel', raw[dcs_idx:end]))
407
+ pos = end
408
+ return parts
409
+
410
+ def _has_error(self, text):
411
+ return bool(_ERROR_RE.search(text))
412
+
413
+ def _handle_magic(self, code):
414
+ stripped = code.strip()
415
+ if stripped == '%reset':
416
+ self._kill_process()
417
+ return True, 'Session reset.'
418
+ if stripped.startswith('%timeout'):
419
+ parts = stripped.split()
420
+ if len(parts) == 2:
421
+ try:
422
+ self._timeout = int(parts[1])
423
+ return True, f'Timeout set to {self._timeout}s.'
424
+ except ValueError:
425
+ return True, 'Usage: %timeout <seconds>'
426
+ return True, f'Current timeout: {self._timeout}s. Usage: %timeout <seconds>'
427
+ if stripped == '%new':
428
+ if self._is_alive():
429
+ self._proc.stdin.write(b'NEW\n')
430
+ self._proc.stdin.write(_SENTINEL_CMD.encode())
431
+ self._proc.stdin.flush()
432
+ self._read_until_sentinel()
433
+ return True, 'Program cleared.'
434
+ return False, None
435
+
436
+ def do_execute(self, code, silent, store_history=True,
437
+ user_expressions=None, allow_stdin=False):
438
+ handled, response = self._handle_magic(code)
439
+ if handled:
440
+ if not silent and response:
441
+ self.send_response(self.iopub_socket, 'stream',
442
+ {'name': 'stdout', 'text': response + '\n'})
443
+ return {
444
+ 'status': 'ok', 'execution_count': self.execution_count,
445
+ 'payload': [], 'user_expressions': {},
446
+ }
447
+
448
+ if not code.strip():
449
+ return {
450
+ 'status': 'ok', 'execution_count': self.execution_count,
451
+ 'payload': [], 'user_expressions': {},
452
+ }
453
+
454
+ if not self._is_alive():
455
+ if not self._start_process():
456
+ if not silent:
457
+ self.send_response(self.iopub_socket, 'stream', {
458
+ 'name': 'stderr',
459
+ 'text': 'gwbasic binary not found.\n'
460
+ 'Set GWBASIC env var or install gwbasic on PATH.\n',
461
+ })
462
+ return {
463
+ 'status': 'error', 'execution_count': self.execution_count,
464
+ 'ename': 'FileNotFoundError', 'evalue': 'gwbasic not found',
465
+ 'traceback': [],
466
+ }
467
+
468
+ for line in code.split('\n'):
469
+ line = line.rstrip()
470
+ if line:
471
+ self._proc.stdin.write((line + '\n').encode())
472
+
473
+ self._proc.stdin.write(_SENTINEL_CMD.encode())
474
+ self._proc.stdin.flush()
475
+
476
+ raw, status = self._read_until_sentinel(allow_stdin=allow_stdin)
477
+
478
+ if status == 'interrupted':
479
+ if not silent:
480
+ self.send_response(self.iopub_socket, 'stream', {
481
+ 'name': 'stderr', 'text': 'Execution interrupted.\n',
482
+ })
483
+ return {'status': 'abort', 'execution_count': self.execution_count}
484
+
485
+ if status == 'timeout':
486
+ self._kill_process()
487
+ if not silent:
488
+ self.send_response(self.iopub_socket, 'stream', {
489
+ 'name': 'stderr',
490
+ 'text': f'Execution timed out after {self._timeout}s. '
491
+ f'Use %timeout to increase.\n',
492
+ })
493
+ return {
494
+ 'status': 'error', 'execution_count': self.execution_count,
495
+ 'ename': 'TimeoutError',
496
+ 'evalue': f'Timed out after {self._timeout}s',
497
+ 'traceback': [],
498
+ }
499
+
500
+ if status == 'died':
501
+ self._proc = None
502
+ text = raw.decode('utf-8', errors='replace').strip()
503
+ if not silent and text:
504
+ self.send_response(self.iopub_socket, 'stream', {
505
+ 'name': 'stderr',
506
+ 'text': f'GW-BASIC process exited.\n{text}\n',
507
+ })
508
+ return {
509
+ 'status': 'error', 'execution_count': self.execution_count,
510
+ 'ename': 'RuntimeError', 'evalue': 'Process exited',
511
+ 'traceback': [],
512
+ }
513
+
514
+ # Split output into text and Sixel graphics
515
+ parts = self._split_sixel(raw)
516
+
517
+ is_error = False
518
+ for kind, data in parts:
519
+ if kind == 'text':
520
+ text = data.decode('utf-8', errors='replace').strip()
521
+ if not text:
522
+ continue
523
+ if self._has_error(text):
524
+ is_error = True
525
+ if not silent:
526
+ self.send_response(self.iopub_socket, 'stream', {
527
+ 'name': 'stderr' if is_error else 'stdout',
528
+ 'text': text + '\n',
529
+ })
530
+ elif kind == 'sixel' and not silent:
531
+ w, h, rgba = _decode_sixel(data)
532
+ if w > 0 and h > 0:
533
+ png = _rgba_to_png(w, h, rgba)
534
+ b64 = base64.b64encode(png).decode('ascii')
535
+ self.send_response(self.iopub_socket, 'display_data', {
536
+ 'data': {'image/png': b64},
537
+ 'metadata': {'image/png': {'width': w, 'height': h}},
538
+ })
539
+
540
+ if is_error:
541
+ err_text = raw.decode('utf-8', errors='replace').strip()
542
+ return {
543
+ 'status': 'error', 'execution_count': self.execution_count,
544
+ 'ename': 'BASICError', 'evalue': err_text.split('\n')[0],
545
+ 'traceback': err_text.split('\n'),
546
+ }
547
+
548
+ return {
549
+ 'status': 'ok', 'execution_count': self.execution_count,
550
+ 'payload': [], 'user_expressions': {},
551
+ }
552
+
553
+ def do_complete(self, code, cursor_pos):
554
+ text = code[:cursor_pos]
555
+ match = re.search(r'([A-Za-z_]\w*\$?)$', text)
556
+ if not match:
557
+ return {'matches': [], 'cursor_start': cursor_pos,
558
+ 'cursor_end': cursor_pos, 'status': 'ok'}
559
+
560
+ token = match.group(1).upper()
561
+ start = cursor_pos - len(token)
562
+ matches = [kw for kw in GW_KEYWORDS if kw.startswith(token)]
563
+ return {
564
+ 'matches': matches, 'cursor_start': start,
565
+ 'cursor_end': cursor_pos, 'status': 'ok',
566
+ }
567
+
568
+ def do_is_complete(self, code):
569
+ if not code.strip():
570
+ return {'status': 'incomplete', 'indent': ''}
571
+ return {'status': 'complete'}
572
+
573
+ def do_shutdown(self, restart):
574
+ self._kill_process()
575
+ return {'status': 'ok', 'restart': restart}
@@ -0,0 +1,140 @@
1
+ """Smoke test for the GW-BASIC Jupyter kernel.
2
+
3
+ Spawns the kernel class directly and exercises the do_execute path
4
+ via the subprocess protocol. No running Jupyter instance required.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+
10
+ # Ensure gwbasic binary is found
11
+ os.environ['GWBASIC'] = os.path.join(os.path.dirname(__file__), '..', 'build', 'gwbasic')
12
+
13
+
14
+ def test_kernel():
15
+ from .kernel import GWBasicKernel, _decode_sixel, _rgba_to_png
16
+
17
+ # Minimal mock — we only need the subprocess communication, not ZMQ
18
+ class MockKernel(GWBasicKernel):
19
+ execution_count = 1
20
+ iopub_socket = None
21
+ _captured = []
22
+
23
+ def __init__(self):
24
+ self._proc = None
25
+ self._timeout = 10
26
+
27
+ def send_response(self, socket, msg_type, content):
28
+ self._captured.append((msg_type, content))
29
+
30
+ k = MockKernel()
31
+
32
+ def run(code):
33
+ k._captured = []
34
+ result = k.do_execute(code, silent=False)
35
+ text = ''
36
+ has_image = False
37
+ for msg_type, content in k._captured:
38
+ if msg_type == 'stream':
39
+ text += content.get('text', '')
40
+ elif msg_type == 'display_data':
41
+ has_image = True
42
+ return result, text.strip(), has_image
43
+
44
+ # Test 1: Simple PRINT
45
+ r, out, _ = run('PRINT "Hello Jupyter"')
46
+ assert r['status'] == 'ok', f'Expected ok, got {r}'
47
+ assert 'Hello Jupyter' in out, f'Expected Hello Jupyter, got: {out}'
48
+ print(f' PASS PRINT: {out}')
49
+
50
+ # Test 2: Arithmetic
51
+ r, out, _ = run('PRINT 6*7')
52
+ assert r['status'] == 'ok'
53
+ assert '42' in out, f'Expected 42, got: {out}'
54
+ print(f' PASS Arithmetic: {out}')
55
+
56
+ # Test 3: State persistence across cells
57
+ r, _, _ = run('X = 42')
58
+ assert r['status'] == 'ok'
59
+ r, out, _ = run('PRINT X')
60
+ assert '42' in out, f'Expected 42, got: {out}'
61
+ print(f' PASS State persistence: {out}')
62
+
63
+ # Test 4: Error handling
64
+ r, out, _ = run('PRINT 1/0')
65
+ assert r['status'] == 'error', f'Expected error, got {r}'
66
+ assert 'Division by zero' in out, f'Expected Division by zero, got: {out}'
67
+ print(f' PASS Error: {out}')
68
+
69
+ # Test 5: Recovery after error
70
+ r, out, _ = run('PRINT "OK after error"')
71
+ assert r['status'] == 'ok'
72
+ assert 'OK after error' in out
73
+ print(f' PASS Recovery: {out}')
74
+
75
+ # Test 6: Program RUN
76
+ r, _, _ = run('10 PRINT "Alpha"\n20 PRINT "Beta"')
77
+ assert r['status'] == 'ok'
78
+ r, out, _ = run('RUN')
79
+ assert 'Alpha' in out and 'Beta' in out, f'Expected Alpha+Beta, got: {out}'
80
+ print(f' PASS RUN: {repr(out)}')
81
+
82
+ # Test 7: Magic %reset
83
+ r, out, _ = run('%reset')
84
+ assert r['status'] == 'ok'
85
+ assert 'reset' in out.lower()
86
+ print(f' PASS %reset: {out}')
87
+
88
+ # Test 8: String functions
89
+ r, out, _ = run('PRINT LEFT$("HELLO", 3)')
90
+ assert 'HEL' in out, f'Expected HEL, got: {out}'
91
+ print(f' PASS String functions: {out}')
92
+
93
+ # Test 9: Multi-statement line
94
+ r, out, _ = run('A=10:B=20:PRINT A+B')
95
+ assert '30' in out, f'Expected 30, got: {out}'
96
+ print(f' PASS Multi-statement: {out}')
97
+
98
+ # Test 10: FRE returns real value
99
+ r, out, _ = run('PRINT FRE(0)')
100
+ assert r['status'] == 'ok'
101
+ print(f' PASS FRE: {out}')
102
+
103
+ # Test 11: Sixel decoder (unit test)
104
+ # Minimal Sixel: single red pixel at position (0,0)
105
+ # '@' = ASCII 64 = sixel value 1 = bit 0 set = row 0 pixel
106
+ sixel_data = b'\x1bPq#1;2;100;0;0#1@\x1b\\'
107
+ w, h, rgba = _decode_sixel(sixel_data)
108
+ assert w == 1 and h == 6, f'Expected 1x6, got {w}x{h}'
109
+ # Pixel (0,0) should be red (255,0,0,255)
110
+ assert rgba[0] == 255 and rgba[1] == 0 and rgba[2] == 0 and rgba[3] == 255, \
111
+ f'Expected red pixel, got {list(rgba[:4])}'
112
+ print(f' PASS Sixel decode: {w}x{h} pixel')
113
+
114
+ # Test 12: PNG encoder (unit test)
115
+ png = _rgba_to_png(1, 1, bytes([255, 0, 0, 255]))
116
+ assert png[:8] == b'\x89PNG\r\n\x1a\n', 'Invalid PNG header'
117
+ print(f' PASS PNG encode: {len(png)} bytes')
118
+
119
+ # Test 13: Sixel graphics via SCREEN (integration)
120
+ r, out, has_img = run('SCREEN 1\nPSET (10,10), 1\nSCREEN 0')
121
+ # The Sixel output should be detected and rendered as an image
122
+ assert r['status'] == 'ok', f'Expected ok, got {r}'
123
+ if has_img:
124
+ print(' PASS Sixel graphics: inline image rendered')
125
+ else:
126
+ print(' PASS Sixel graphics: no image (Sixel may not be emitted in piped mode)')
127
+
128
+ # Test 14: Pygments lexer loads
129
+ from .basic_lexer import GWBasicLexer
130
+ lexer = GWBasicLexer()
131
+ tokens = list(lexer.get_tokens('10 PRINT "Hello"'))
132
+ assert len(tokens) > 0, 'Lexer produced no tokens'
133
+ print(f' PASS Pygments lexer: {len(tokens)} tokens')
134
+
135
+ k.do_shutdown(False)
136
+ print(f'\n14 tests passed.')
137
+
138
+
139
+ if __name__ == '__main__':
140
+ test_kernel()
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: gwbasickernel
3
+ Version: 0.1.0
4
+ Summary: Jupyter kernel for GW-BASIC 2026 with inline graphics
5
+ Author: Eremey Valetov
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/evvaletov/gw-basic-2026
8
+ Project-URL: Documentation, https://evvaletov.github.io/gw-basic-2026/
9
+ Project-URL: Issues, https://github.com/evvaletov/gw-basic-2026/issues
10
+ Classifier: Framework :: Jupyter
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: ipykernel>=6
18
+ Requires-Dist: jupyter_client>=8
19
+ Requires-Dist: pygments>=2
20
+ Dynamic: license-file
21
+
22
+ # GW-BASIC Jupyter kernel
23
+
24
+ Run GW-BASIC programs in JupyterLab or Notebook. Variables and program lines
25
+ persist between cells. The kernel converts the interpreter's Sixel output to
26
+ inline PNG images, so the notebook browser does not need Sixel support.
27
+
28
+ Requires Python 3.10 or later and the native
29
+ [GW-BASIC 2026 interpreter](https://github.com/evvaletov/gw-basic-2026)
30
+ on Linux or macOS. The Python package does not include that interpreter.
31
+ The DOS executables cannot be used as the notebook backend.
32
+
33
+ ## Install the interpreter
34
+
35
+ With Homebrew on Linux or macOS:
36
+
37
+ ```sh
38
+ brew tap evvaletov/gwbasic https://github.com/evvaletov/gw-basic-2026.git
39
+ brew install evvaletov/gwbasic/gwbasic
40
+ ```
41
+
42
+ The Homebrew formula builds from a pinned source revision. It includes the
43
+ interpreter and native compiler, with PulseAudio disabled.
44
+
45
+ Alternatively, build from source with a C11 compiler and CMake:
46
+
47
+ ```sh
48
+ git clone https://github.com/evvaletov/gw-basic-2026.git
49
+ cd gw-basic-2026
50
+ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
51
+ cmake --build build --parallel 2
52
+ export GWBASIC="$PWD/build/gwbasic"
53
+ ```
54
+
55
+ ## Install the kernel
56
+
57
+ Create a Python environment for the kernel and JupyterLab:
58
+
59
+ ```sh
60
+ python3 -m venv .venv
61
+ . .venv/bin/activate
62
+ python -m pip install gwbasickernel jupyterlab
63
+ gwbasickernel-install --sys-prefix
64
+ jupyter lab
65
+ ```
66
+
67
+ Choose **GW-BASIC 2026** when creating a notebook. The kernel looks for the
68
+ interpreter in `GWBASIC`, then on `PATH`. To save its location in the kernelspec,
69
+ use this command instead of the installer command above:
70
+
71
+ ```sh
72
+ gwbasickernel-install --sys-prefix --gwbasic /absolute/path/to/gwbasic
73
+ ```
74
+
75
+ If Jupyter runs in another Python environment, use `--user` to register the
76
+ kernel for your account. Keep the kernel's Python environment in place:
77
+ the kernelspec records its Python executable.
78
+
79
+ ## Try it
80
+
81
+ ```basic
82
+ 10 FOR N=1 TO 8
83
+ 20 PRINT N;N*N
84
+ 30 NEXT N
85
+ RUN
86
+ ```
87
+
88
+ For graphics, clear the previous program first:
89
+
90
+ ```basic
91
+ NEW
92
+ 10 SCREEN 1
93
+ 20 CIRCLE (160,100),80,2
94
+ RUN
95
+ ```
96
+
97
+ See the [executed graphics notebook](https://github.com/evvaletov/gw-basic-2026/blob/main/examples/graphics.ipynb)
98
+ and [demo guide](https://github.com/evvaletov/gw-basic-2026/blob/main/docs/demos.md).
99
+
100
+ `%new` clears the BASIC program; `%reset` starts a fresh interpreter process.
101
+ `%timeout 60` changes the output wait timeout from its default of 30 seconds.
@@ -0,0 +1,14 @@
1
+ gwbasickernel/README.md,sha256=UxKKuCooAfd_9mC9ZIvUGLOtNtKXaWN_diTv3YwWS9Q,2352
2
+ gwbasickernel/__init__.py,sha256=V0pJTh8MU9tJ7d0ZKK0VD3P36ZwG2IUduwhRXYGBScw,63
3
+ gwbasickernel/__main__.py,sha256=ZRyXUypbCWf-wDm7fuZlFKzRbFxwHNB6F0Nuj4_9Ykc,135
4
+ gwbasickernel/basic_lexer.py,sha256=ealwhWBtOMap0N0FrbddSyeRE1Dac-iyKcNv44DeHCc,2886
5
+ gwbasickernel/install.py,sha256=KO9lclfYmoK4NCtc6s9aLB3IhtcwCIQLxXGiyIr2-o0,2111
6
+ gwbasickernel/kernel.json,sha256=zTKt3s2oGWK0SEyj75O20pWTwDUzQMhs7gpIG51afvk,176
7
+ gwbasickernel/kernel.py,sha256=xI7KCg_8l6XqwX_FNPp-TL9uocw4jUPTgRleNAqTxXA,20644
8
+ gwbasickernel/test_kernel.py,sha256=5cmIHPkavIbN9VIPKB74EpnJM84uLWzev1SeoxZnK6c,4778
9
+ gwbasickernel-0.1.0.dist-info/licenses/LICENSE,sha256=-mIPUvK6wSxeUkO82LHW_GbhCgP23AFZPoTJyS8AMY0,1071
10
+ gwbasickernel-0.1.0.dist-info/METADATA,sha256=RTI5PmUHtG1Xj_goD8PXrIzLlAumyF8loyaWD89Gvc8,3090
11
+ gwbasickernel-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
12
+ gwbasickernel-0.1.0.dist-info/entry_points.txt,sha256=u52KcGl979bG_JYZgBMIio8Y5NhVai_KB3HNY6vA7iE,137
13
+ gwbasickernel-0.1.0.dist-info/top_level.txt,sha256=Zew3LBNrkia0PskUaC9Hvz1LeCp_Acoypcr21v9MoTs,14
14
+ gwbasickernel-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ gwbasickernel-install = gwbasickernel.install:main
3
+
4
+ [pygments.lexers]
5
+ gwbasic = gwbasickernel.basic_lexer:GWBasicLexer
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eremey Valetov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ gwbasickernel