From 5c2d5626d0a087d5e8d377b8b00189d1547891ac Mon Sep 17 00:00:00 2001 From: Tyrel Souza Date: Sat, 29 Jul 2023 23:26:31 -0400 Subject: [PATCH] init --- .gitignore | 8 +++ .gitmodules | 3 + 1x 40 right.txt | 24 ++++++++ 1x 40.txt | 24 ++++++++ 1x 80.txt | 24 ++++++++ 2x 40.txt | 24 ++++++++ Dockerfile | 17 ++++++ LICENSE | 21 +++++++ README.md | 19 ++++++ default_templates/intro.html | 18 ++++++ default_templates/main_sample.css | 44 ++++++++++++++ default_templates/story_index.html | 24 ++++++++ default_templates/story_page.html | 33 ++++++++++ dialazine/generate_html.py | 24 ++++++++ dialazine/lib/common_tools.py | 12 ++++ dialazine/lib/contents_reader.py | 92 ++++++++++++++++++++++++++++ dialazine/lib/html_generator.py | 93 +++++++++++++++++++++++++++++ dialazine/lib/text_screen_reader.py | 13 ++++ dialazine/lib/zine_functions.py | 54 +++++++++++++++++ dialazine/server.py | 19 ++++++ dialazine/tools/proof_ascii.html | 53 ++++++++++++++++ dialazine/tools/proof_ascii.js | 34 +++++++++++ requirements.txt | 2 + tyrel/index.json | 10 ++++ tyrel/intro.txt | 23 +++++++ tyrel/iss001/1.txt | 24 ++++++++ tyrel/iss001/2.txt | 24 ++++++++ 27 files changed, 760 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 1x 40 right.txt create mode 100644 1x 40.txt create mode 100644 1x 80.txt create mode 100644 2x 40.txt create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 default_templates/intro.html create mode 100644 default_templates/main_sample.css create mode 100644 default_templates/story_index.html create mode 100644 default_templates/story_page.html create mode 100755 dialazine/generate_html.py create mode 100644 dialazine/lib/common_tools.py create mode 100644 dialazine/lib/contents_reader.py create mode 100644 dialazine/lib/html_generator.py create mode 100644 dialazine/lib/text_screen_reader.py create mode 100644 dialazine/lib/zine_functions.py create mode 100755 dialazine/server.py create mode 100644 dialazine/tools/proof_ascii.html create mode 100644 dialazine/tools/proof_ascii.js create mode 100644 requirements.txt create mode 100644 tyrel/index.json create mode 100644 tyrel/intro.txt create mode 100644 tyrel/iss001/1.txt create mode 100644 tyrel/iss001/2.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..802e12b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vscode/ +tags +tags.* +venv/ +__pycache__/ +issue* +dial_a_zine_issue* +newsession_issue2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9afe07b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "issue1"] + path = issue1 + url = git@github.com:caraesten/dial_a_zine_issue1.git diff --git a/1x 40 right.txt b/1x 40 right.txt new file mode 100644 index 0000000..a31e771 --- /dev/null +++ b/1x 40 right.txt @@ -0,0 +1,24 @@ + o--------------------------------------o + | | + | | + | | + | | + | | + | | + | | + | | + | | + this page intentionally left blank | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + o--------------------------------------o \ No newline at end of file diff --git a/1x 40.txt b/1x 40.txt new file mode 100644 index 0000000..1a439a4 --- /dev/null +++ b/1x 40.txt @@ -0,0 +1,24 @@ +o--------------------------------------o +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +o--------------------------------------o \ No newline at end of file diff --git a/1x 80.txt b/1x 80.txt new file mode 100644 index 0000000..5652fae --- /dev/null +++ b/1x 80.txt @@ -0,0 +1,24 @@ +o-----------------------------------------------------------------------------o +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +o-----------------------------------------------------------------------------o \ No newline at end of file diff --git a/2x 40.txt b/2x 40.txt new file mode 100644 index 0000000..43d0c02 --- /dev/null +++ b/2x 40.txt @@ -0,0 +1,24 @@ +o--------------------------------------o--------------------------------------o +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +o--------------------------------------o--------------------------------------o \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56227e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1 + +ARG ISSUE_DIR=issue1 + +FROM python:3.8-slim-buster + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY dialazine dialazine +COPY $ISSUE_DIR $ISSUE_DIR + +EXPOSE 23/tcp + +CMD [ "python3", "dialazine/server.py" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f56b1f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Cara Esten Hurtle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1947019 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +## How to run + +To run locally, run server.py as a python file e.g. from the root directory + +`python3 dialazine/server.py` + +To access locally, run + +`telnet localhost 23` + +(or equivalent with another telnet client) + +in server.py, change CONTENT_FOLDER to "example_issue" to see example + +## Zine Structure + +In the index.json, the "hello" message is the filepath within the issue folder for what is first displayed, + +Then the contents is a list with each article, taking the "title" and "author" for the index, and the "directory" is the folder within the issue folder that contains the article pages - the pages must be called `1.txt`, `2.txt` etc. and will automatically display in that order diff --git a/default_templates/intro.html b/default_templates/intro.html new file mode 100644 index 0000000..1ffd419 --- /dev/null +++ b/default_templates/intro.html @@ -0,0 +1,18 @@ + + + + + + +
+
+ {{#intro_lines}} + {{{.}}}
+ {{/intro_lines}} +
+ +
+ + diff --git a/default_templates/main_sample.css b/default_templates/main_sample.css new file mode 100644 index 0000000..8a1ecf4 --- /dev/null +++ b/default_templates/main_sample.css @@ -0,0 +1,44 @@ +body { + font-family: monospace; + background-color: black; + color: #39ff14; +} + +a { + color: #ddd; +} + +h2 { + text-align: center; +} + +.main { + width: 82ch; /* fudge it a bit */ + margin-left: auto; + margin-right: auto; +} + +.viewport { + height: 30em; + margin-top: 2em; +} + +.stories-list li { + margin: 0.5em; +} + +.navigation { + margin-top: 2em; + text-align: center; +} + +.navigation a { + display: inline-block; +} + +.about { + color: #1c830a; + text-align: center; + font-size: 10pt; + margin-top: 1em; +} diff --git a/default_templates/story_index.html b/default_templates/story_index.html new file mode 100644 index 0000000..faf7dc1 --- /dev/null +++ b/default_templates/story_index.html @@ -0,0 +1,24 @@ + + + + + + +
+
+

{{header}}

+ +
+ +
+ Sample zine, generated by dial-a-zine telnet cms! +
+
+ + diff --git a/default_templates/story_page.html b/default_templates/story_page.html new file mode 100644 index 0000000..622382f --- /dev/null +++ b/default_templates/story_page.html @@ -0,0 +1,33 @@ + + + + + + +
+
+ {{#story_lines}} + {{{.}}}
+ {{/story_lines}} +
+ +
+ {{story_title}} by {{story_author}}, page {{page_number}} +
+
+ + diff --git a/dialazine/generate_html.py b/dialazine/generate_html.py new file mode 100755 index 0000000..5d52d65 --- /dev/null +++ b/dialazine/generate_html.py @@ -0,0 +1,24 @@ +#!/usr/bin/python3 +import os +import argparse +from pathlib import Path +from lib.html_generator import HtmlGenerator + +TEMPLATES = "default_templates" +CONTENT_FOLDER = "example_issue" + +parser = argparse.ArgumentParser(description="Use this tool to export html documents for your zine") +parser.add_argument("outputdir", type=Path, help="output directory for generated HTML") + +args = parser.parse_args() + +if not args.outputdir: + raise ValueError("Required param outputdir missing, run program with -h") + +html_directory = args.outputdir.as_posix() + +root_dir_path = Path(__file__).parent.parent.absolute() +index_directory = "%s/%s" % (root_dir_path.as_posix(), f"{CONTENT_FOLDER}/index.json") +templates_directory = "%s/%s" % (root_dir_path.as_posix(), TEMPLATES) +generator = HtmlGenerator(index_directory, html_directory, templates_directory) +generator.write_zine_html() \ No newline at end of file diff --git a/dialazine/lib/common_tools.py b/dialazine/lib/common_tools.py new file mode 100644 index 0000000..5f47150 --- /dev/null +++ b/dialazine/lib/common_tools.py @@ -0,0 +1,12 @@ + +def index_header(include_linebreaks=True): + linebreak_char = '' + if include_linebreaks: + linebreak_char = '\n' + return linebreak_char + " [ INDEX ] " + linebreak_char + +def index_item_string(option, title, author, include_linebreaks=True): + linebreak_char = '' + if include_linebreaks: + linebreak_char = '\n' + return linebreak_char + ("%s > %s < ...by %s" % (option, title, author)) + linebreak_char diff --git a/dialazine/lib/contents_reader.py b/dialazine/lib/contents_reader.py new file mode 100644 index 0000000..e98bd75 --- /dev/null +++ b/dialazine/lib/contents_reader.py @@ -0,0 +1,92 @@ +import json, os, string +from lib.common_tools import index_item_string +from lib.text_screen_reader import TextScreenReader +from lib.common_tools import index_header + +class ContentsReader: + def __init__(self, contents_file_path): + self.issue_directory = os.path.dirname(contents_file_path) + self.text_reader = TextScreenReader(self.issue_directory) + contents_file = open(contents_file_path, 'r') + self.contents_json = json.load(contents_file) + self.verify_contents() + + def verify_contents(self): + if not self.contents_json['hello']: + raise Exception("Contents JSON requires a hello message") + + def hello_file_path(self): + return self.contents_json['hello'] + + def read_hello_file(self, wrap_returns=True): + lines = self.text_reader.read_file_name(self.hello_file_path()) + if wrap_returns: + return self._wrap_carriage_returns(lines) + else: + return lines + + def read_index_lines(self): + index_lines = [index_header()] + option_number = 1 + for index_item in self.read_story_object(): + index_lines.append(index_item_string(self._index_to_option(option_number), index_item['title'], index_item['author'])) + option_number += 1 + index_lines.append("\n(or X to quit!)\n") + return self._wrap_carriage_returns(index_lines) + + def read_story_object(self, include_contents=False): + index_list = [] + story_number = 1 + for index_item in self.contents_json['contents']: + story_item = { + 'title': index_item['title'], + 'author': index_item['author'], + 'directory': index_item['directory'], + } + if include_contents: + page_number = 1 + story_lines = self.read_story(story_number, page_number, wrap_returns=False) + pages = [] + while len(story_lines) > 0: + pages.append(story_lines) + page_number += 1 + story_lines = self.read_story(story_number, page_number, wrap_returns=False) + story_item['contents'] = pages + story_number += 1 + index_list.append(story_item) + return index_list + + def read_story(self, story_number, page = 1, wrap_returns=True): + if story_number > len(self.contents_json['contents']): + return [] + story_obj = self.contents_json['contents'][story_number - 1] + file_path = "%s/%s.txt" % (story_obj['directory'], page) + + if self.text_reader.does_file_exist(file_path): + page_lines = self.text_reader.read_file_name(file_path) + if wrap_returns: + return self._wrap_carriage_returns(page_lines) + else: + return page_lines + else: + return [] + + def _wrap_carriage_returns(self, lines_list): + return [x + '\r' for x in lines_list] + + def map_input_to_numerical_index(self, input_string): + try: + return int(input_string) + except ValueError: + pass + try: + return 10 + string.ascii_uppercase[0:10].index(input_string) + except ValueError: + return -1 + + def _index_to_option(self, input_index): + if (input_index < 10): + return input_index + else: + return string.ascii_uppercase[input_index - 10] + diff --git a/dialazine/lib/html_generator.py b/dialazine/lib/html_generator.py new file mode 100644 index 0000000..3d88867 --- /dev/null +++ b/dialazine/lib/html_generator.py @@ -0,0 +1,93 @@ +import chevron +from pathlib import Path +from lib.common_tools import index_item_string, index_header +from lib.contents_reader import ContentsReader + +class HtmlGenerator: + def __init__(self, index_file_path, html_output_path, templates_path): + self.contents_reader = ContentsReader(index_file_path) + self.html_output_path = html_output_path + self.templates_path = templates_path + # TODO: this could be customizeable + self.index_url = "story_index.html" + + def write_zine_html(self): + intro_text = self.contents_reader.read_hello_file(wrap_returns=False) + zine_dict = self.contents_reader.read_story_object(include_contents=True) + + intro_template_path = f'{self.templates_path}/intro.html' + index_template_path = f'{self.templates_path}/story_index.html' + story_template_path = f'{self.templates_path}/story_page.html' + + Path(self.html_output_path).mkdir(parents=True, exist_ok=True) + + self._write_intro_html(intro_template_path, intro_text) + self._write_index_html(index_template_path, zine_dict) + self._write_stories_html(story_template_path, zine_dict) + + def _write_intro_html(self, intro_template_path, intro_text): + intro_html = '' + html_text = [self._htmlize_whitespace(x) for x in intro_text] + with open(intro_template_path, 'r') as f: + intro_html = chevron.render(f, {'intro_lines': html_text, 'index_url': self.index_url}) + file_output_path = self.html_output_path + "/index.html" + print("Writing intro page to: %s" % file_output_path) + with open(file_output_path, 'w') as f: + f.write(intro_html) + + def _write_index_html(self, index_template_path, zine_dict): + index_html = '' + with open(index_template_path, 'r') as f: + index_items = [] + for index in range(len(zine_dict)): + item = zine_dict[index] + first_page_link = "%s/1.html" % item['directory'] + item_dict = { + 'title_text': index_item_string(index, item['title'], item['author'], include_linebreaks=False), + 'story_link': first_page_link + } + index_items.append(item_dict) + index_html = chevron.render(f, { + 'header': index_header(include_linebreaks=False), + 'stories': index_items, + 'index_url': self.index_url + }) + file_output_path = self.html_output_path + "/" + self.index_url + print("Writing index page to: %s" % file_output_path) + with open(file_output_path, 'w') as f: + f.write(index_html) + + def _write_stories_html(self, story_page_template_path, zine_dict): + story_template = '' + with open(story_page_template_path, 'r') as f: + story_template = f.read() + + for item in zine_dict: + output_directory = item['directory'] + story_contents = item['contents'] + for index, page in enumerate(story_contents): + # pages are 1-indexed, just to make them a bit more human readable + current_page = index + 1 + previous_page = f'{str(current_page - 1)}.html' if index > 0 else '' + next_page = f'{str(current_page + 1)}.html' if index < len(story_contents) -1 else '' + page_html = chevron.render(story_template, { + 'back_link': previous_page, + 'next_link': next_page, + 'story_lines': [self._htmlize_whitespace(x.replace("\n", "")) for x in page], + 'index_url': '../story_index.html', + 'story_title': item['title'], + 'story_author': item['author'], + 'page_number': current_page, + }) + file_output_dir = self.html_output_path + "/" + output_directory + "/" + file_output_path = file_output_dir + str(current_page) + ".html" + Path(file_output_dir).mkdir(parents=True, exist_ok=True) + print("Writing story page to: %s" % file_output_path) + with open(file_output_path, 'w') as f: + f.write(page_html) + + def _htmlize_whitespace(self, string): + # linebreaks are already determined via newlines in source text files. + # we can (and should) just make all spaces non-breaking. + # TODO: evaluate this with screen readers + return string.replace(' ', ' ') diff --git a/dialazine/lib/text_screen_reader.py b/dialazine/lib/text_screen_reader.py new file mode 100644 index 0000000..edea61b --- /dev/null +++ b/dialazine/lib/text_screen_reader.py @@ -0,0 +1,13 @@ +import os + +class TextScreenReader: + def __init__(self, root_directory): + self.root_directory = root_directory + def read_file_name(self, path): + full_path = "%s/%s" % (self.root_directory, path) + with open(full_path, 'r') as f: + full_file = f.readlines() + f.close() + return full_file + def does_file_exist(self, file_path): + return os.path.exists("%s/%s" % (self.root_directory, file_path)) diff --git a/dialazine/lib/zine_functions.py b/dialazine/lib/zine_functions.py new file mode 100644 index 0000000..b29dbca --- /dev/null +++ b/dialazine/lib/zine_functions.py @@ -0,0 +1,54 @@ +from lib.contents_reader import ContentsReader +import asyncio + +CLEAR_SCREEN = "\u001b[2J" +NEW_LINE = "\r\n" + +class ZineFunctions: + def __init__(self, reader, writer, index_file_path): + self.reader = reader + self.writer = writer + self.contents_reader = ContentsReader(index_file_path) + + async def run_index(self): + for welcome_line in self.contents_reader.read_hello_file(): + self.writer.write(welcome_line) + await self.writer.drain() + # Read one byte (any key) + await self.reader.read(1) + running = True + while (running): + for index_line in self.contents_reader.read_index_lines(): + self.writer.write(index_line) + item_choice = await self.reader.read(1) + item_choice_int = -1 + if item_choice.upper() == 'X': + running = False + continue + item_choice_int = self.contents_reader.map_input_to_numerical_index(item_choice) + if item_choice_int == -1: + self.writer.write(f"{NEW_LINE}{NEW_LINE}Pick a story, or X to quit.{NEW_LINE}") + continue + self.writer.write(f"{NEW_LINE}{NEW_LINE}...you picked: %s" % (item_choice)) + self.writer.write(f"{NEW_LINE}{NEW_LINE}...press RETURN to start reading, and to continue after each page") + await self.reader.read(1) + self.writer.write(NEW_LINE + CLEAR_SCREEN) + await asyncio.sleep(1) + await self.run_story(item_choice_int) + self.disconnect() + + async def run_story(self, story_number): + page_number = 1 + + story_lines = self.contents_reader.read_story(story_number, page_number) + while len(story_lines) > 0: + self.writer.write(CLEAR_SCREEN) + for story_line in story_lines: + self.writer.write(story_line) + await self.writer.drain() + char_read = await self.reader.readline() + page_number += 1 + story_lines = self.contents_reader.read_story(story_number, page_number) + + def disconnect(self): + self.writer.close() diff --git a/dialazine/server.py b/dialazine/server.py new file mode 100755 index 0000000..e9e006d --- /dev/null +++ b/dialazine/server.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 +import asyncio, telnetlib3 +import os +import pathlib +from lib.zine_functions import ZineFunctions + +LOCALHOST_PORT = 23 +CONTENT_FOLDER = "tyrel" + +async def shell(reader, writer): + root_dir_path = pathlib.Path(__file__).parent.parent.absolute() + + zine = ZineFunctions(reader, writer, "%s/%s" % (root_dir_path.as_posix(), f"{CONTENT_FOLDER}/index.json")) + await zine.run_index() + +loop = asyncio.get_event_loop() +srv = telnetlib3.create_server(port=LOCALHOST_PORT, shell=shell, timeout=3600) +server = loop.run_until_complete(srv) +loop.run_until_complete(server.wait_closed()) diff --git a/dialazine/tools/proof_ascii.html b/dialazine/tools/proof_ascii.html new file mode 100644 index 0000000..c78c42f --- /dev/null +++ b/dialazine/tools/proof_ascii.html @@ -0,0 +1,53 @@ + + + + ASCII Scanner + + + + +
+

ASCII Scanner

+

Paste Text Below

+ +
+
+
+ + \ No newline at end of file diff --git a/dialazine/tools/proof_ascii.js b/dialazine/tools/proof_ascii.js new file mode 100644 index 0000000..8681991 --- /dev/null +++ b/dialazine/tools/proof_ascii.js @@ -0,0 +1,34 @@ +class AsciiScanner { + static ASCII_REGEX = /([\u{00e1}-\u{FFFF}]+)/gu; + constructor(inputElement, matchesOutput, textOutput) { + this.inputElement = inputElement; + this.matchesOutput = matchesOutput; + this.textOutput = textOutput; + } + + start() { + this.inputElement.addEventListener('input', (event) => { + const text = event.target.value; + this.onTextChange(text); + }) + } + + onTextChange(newText) { + const output = newText.replace(AsciiScanner.ASCII_REGEX, (match) => { + return `${match}`; + }); + this.textOutput.innerHTML = output; + + const errors = ((newText || '').match(AsciiScanner.ASCII_REGEX) || []).length; + this.matchesOutput.innerHTML = `Found: ${errors} non-ASCII blocks` + } +} + +window.onload = function() { + const scanner = new AsciiScanner( + document.getElementById('text-input'), + document.getElementById('matches-output'), + document.getElementById('text-output')); + + scanner.start(); +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1006d18 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +telnetlib3==1.0.4 +chevron==0.14.0 diff --git a/tyrel/index.json b/tyrel/index.json new file mode 100644 index 0000000..2a7c9bb --- /dev/null +++ b/tyrel/index.json @@ -0,0 +1,10 @@ +{ + "hello": "intro.txt", + "contents": [ + { + "title": "Issue 1 - Intro", + "author": "Tyrel Souza", + "directory": "iss001" + } + ] +} \ No newline at end of file diff --git a/tyrel/intro.txt b/tyrel/intro.txt new file mode 100644 index 0000000..a7275fe --- /dev/null +++ b/tyrel/intro.txt @@ -0,0 +1,23 @@ + + + + + + + + + + + + + Welcome to Tyrel's Zine. + Will keep updated at a very infrequent rate. + + + - Tyrel Souza + + + + + + diff --git a/tyrel/iss001/1.txt b/tyrel/iss001/1.txt new file mode 100644 index 0000000..8dbc208 --- /dev/null +++ b/tyrel/iss001/1.txt @@ -0,0 +1,24 @@ + o--------------------------------------o + | | + | | + | | + | ___ ___ ___ | + | /\ \ /\ \ /\ \ | + | \:\ \ /::\ \ /::\ \ | + | /::\__\ /::\:\__\ /\:\:\__\ | + | /:/\/__/ \/\::/ / \:\:\/__/ | + | \/__/ /:/ / \::/ / | + | \/__/ \/__/ | + this page intentionally left blank | | + | tyrel | + | anthony | + | souza | + | | + | | + | | + | | + | | + | | + | | + | 1 | +2023-07-29 o--------------------------------------o \ No newline at end of file diff --git a/tyrel/iss001/2.txt b/tyrel/iss001/2.txt new file mode 100644 index 0000000..8ba9fa0 --- /dev/null +++ b/tyrel/iss001/2.txt @@ -0,0 +1,24 @@ +o--------------------------------------o--------------------------------------o +| \ | +| Welcome to my zine / Wow so apparently I have a telnet | +| ------------------ \ zine now. | +| / | +| Everything in all its ASCII \ Not sure what to put here yet! | +| glory. / | +| \ You can find me --\ | +| / | | +| \ | | +| / | | +| \ https://tyrel.dev/links