# Builds the files for the home directory of the shell server's root user in ../resources: README, README.en, # CODE_OF_CONDUCT and CODE_OF_CONDUCT.en as the website's pages in terminal text with escape sequences, and the # files of ../home, which are copied as they are. # Usage: python3 server/scripts/make_files.py import pathlib import shutil import textwrap ESC = '\x1b' RESET = f'{ESC}[0m' BOLD = f'{ESC}[1m' DIM = f'{ESC}[38;2;31;154;63m' # the page's --dim HOT = f'{ESC}[38;2;255;42;42m' # the page's --hot WIDTH = 82 # the web terminal's width, as wide as the page column INNER = WIDTH - 4 # text width inside "│ " ... " │" SITE = 'https://berlin.ccc.de/kik/' IMPRESSUM = 'https://berlin.ccc.de/page/impressum/' TICKETS = 'https://tickets.berlin.ccc.de/cccb/kik-v0.1/' CFP = 'https://cfp.berlin.ccc.de/kik-v01/cfp' SCHEDULE = 'https://cfp.berlin.ccc.de/kik-v01/schedule' MAP = 'https://www.openstreetmap.org/node/3872556357#map=20/52.5217003/13.3828041&layers=H' def link(url, text, style=''): return f'{ESC}]8;;{url}{ESC}\\{style}{text}{RESET if style else ""}{ESC}]8;;{ESC}\\' class Line: """A line of text with its visible width, so boxes can be padded around styled text.""" def __init__(self, text='', width=None): self.text, self.width = text, len(text) if width is None else width def __add__(self, other): return Line(self.text + other.text, self.width + other.width) def styled(text, style): return Line(f'{style}{text}{RESET}', len(text)) def linked(url, text, style=''): return Line(link(url, text, style), len(text)) def joined(parts, gap=' '): line = parts[0] for part in parts[1:]: line = line + Line(gap) + part return line def wrap(text, width, links=()): """Wraps plain text, then turns the given (text, url) pairs into links where they appear.""" lines = [] for raw in textwrap.wrap(text, width, break_on_hyphens=False, break_long_words=False): rendered = raw for needle, url in links: rendered = rendered.replace(needle, link(url, needle)) lines.append(Line(rendered, len(raw))) return lines def paragraph(text, links=(), lead=None): """A paragraph, optionally starting with a bold lead-in like "Ticket:".""" full = f'{lead} {text}' if lead else text lines = wrap(full, INNER, links) if lead: first = lines[0] lines[0] = Line(first.text.replace(lead, f'{BOLD}{lead}{RESET}', 1), first.width) return lines def pairs(items, label, links=()): """A key-value list like the page's: dim "Label :" column, values wrapped with a hanging indent.""" lines = [] for key, value in items: wrapped = wrap(value, INNER - label, links) lines.append(styled(f'{key} :'.ljust(label), DIM) + wrapped[0]) lines += [Line(' ' * label) + rest for rest in wrapped[1:]] return lines def bullets(items): """A list like the page's: dim "> " marker, bold title, text wrapped with a hanging indent.""" lines = [] for title, text in items: wrapped = wrap(f'{title} {text}', INNER - 2) first = wrapped[0] lines.append(styled('> ', DIM) + Line(first.text.replace(title, f'{BOLD}{title}{RESET}', 1), first.width)) lines += [Line(' ') + rest for rest in wrapped[1:]] return lines def box(title, lines): rule = WIDTH - 2 - len(f'─┤ {title} ├') out = [Line('┌─') + styled('┤ ', DIM) + styled(title, BOLD) + styled(' ├', DIM) + Line('─' * rule + '┐')] out += [Line('│ ') + line + Line(' ' * (INNER - line.width) + ' │') for line in lines] out.append(Line('└' + '─' * (WIDTH - 2) + '┘')) return out def centered(line): return Line(' ' * ((WIDTH - line.width) // 2)) + line def nav(line): return [styled('─' * WIDTH, DIM), line, styled('─' * WIDTH, DIM)] def footer(parts): line = linked(parts[0][1], parts[0][0], DIM) for text, url in parts[1:]: line = line + styled(' · ', DIM) + linked(url, text, DIM) return [styled('─' * WIDTH, DIM), centered(line)] def page(lines): lines = [Line(), *lines, Line()] for line in lines: assert line.width <= WIDTH + 2, (line.width, line.text) return ''.join(f'{line.text}\n' for line in lines) def readme(t): label = 3 + max(len(key) for key, _ in t['infos'] + t['route']) return page([ styled('KHAOS IM ', BOLD) + styled('KIEZ', BOLD + HOT), styled(t['date'], DIM), Line(), *nav(joined([linked(f'{SITE}{t["page"]}#{anchor}', f'[{n}] {title}') for n, title, anchor in t['sections']])), Line(), *box('{} {}'.format(*t['sections'][0][:2]), [ *paragraph(t['intro'][0]), Line(), *paragraph(t['intro'][1]), Line(), *pairs(t['infos'], label, links=[('cfp.berlin.ccc.de', 'https://cfp.berlin.ccc.de'), ('Code of Conduct', f'{SITE}{t["coc"]}')]), ]), Line(), *box('{} {}'.format(*t['sections'][1][:2]), [ *paragraph(t['ticket'], lead=t['ticket_lead']), Line(), linked(TICKETS, f'[ {t["get_ticket"]} ]', HOT), Line(), *paragraph(t['talks'], lead=t['talks_lead']), Line(), joined([linked(CFP, f'[ {t["submit"]} ]'), linked(SCHEDULE, f'[ {t["schedule"]} ]')]), Line(), *paragraph(t['other'], lead=t['other_lead']), ]), Line(), *box('{} {}'.format(*t['sections'][2][:2]), [ *pairs(t['route'], label, links=[('BBBike.de', 'http://www.bbbike.de/')]), Line(), linked(MAP, f'[ {t["map"]} ]'), ]), Line(), *footer(t['footer']), ]) def code_of_conduct(t): return page([ *box('Code of Conduct', [ *paragraph(t['intro']), Line(), *bullets(t['rules']), Line(), *paragraph(t['house'], lead=t['house_lead']), Line(), *paragraph(t['problems'], lead=t['problems_lead']), ]), Line(), *footer(t['footer']), ]) DE = { 'page': '', 'coc': 'coc.html', 'date': 'CCC Berlin · 28. – 29.11.2026', 'sections': [(1, 'Infos', 'infos'), (2, 'Mitmachen', 'mitmachen'), (3, 'Anfahrt', 'anfahrt')], 'intro': [ 'Khaos im Kiez ist ein kleines Chaos-Event, back to the roots: inspiriert von anderen kleinen Chaos-Events. ' 'Ein Wochenende zum Chillen und um die Seite von Chaos-Events zu erleben, für die bei größeren Events oft ' 'keine Zeit mehr bleibt.', 'Du kannst einen Talk oder Workshop halten, an deinen Projekten schrauben, gemeinsam hacken, löten, basteln ' 'oder einfach vorbeikommen, zuhören und Leute kennenlernen. Alle sind willkommen, die Lust auf $Dinge, ' 'Neugier und Chaos mitbringen.', ], 'infos': [ ('Was', 'Khaos im Kiez (KiK)'), ('Wann', 'Sa 28.11. – So 29.11.2026, 11:00 – 23:59'), ('Wo', 'Marienstraße 11, 10117 Berlin'), ('Wer', 'Chaos Computer Club Berlin'), ('Preis', '0,00 € (Ticket trotzdem nötig, ~50 Plätze)'), ('Fahrplan', 'cfp.berlin.ccc.de'), ('Essen', 'Ewiges Frühstück, vegan'), ('Barrierefrei', 'Nicht barrierefrei.'), ('Regeln', 'Code of Conduct'), ], 'ticket_lead': 'Ticket:', 'ticket': 'Anmeldung über den Ticketshop. Das Ticket ist kostenlos, aber die Plätze sind begrenzt.', 'get_ticket': 'Ticket sichern', 'talks_lead': 'Talks & Workshops:', 'talks': 'Reiche deinen Beitrag im Call for Participation ein und schau dir später den Fahrplan an.', 'submit': 'Talk einreichen', 'schedule': 'Fahrplan', 'other_lead': 'Sonstiges:', 'other': 'Du willst anders helfen oder hast Fragen? Schreib an aprl [at] berlin [dot] ccc [dot] de.', 'route': [ ('Adresse', 'Clubräume im Hinterhof der Marienstraße 11, 10117 Berlin'), ('Koordinaten', '52,52194° N, 13,38261° O'), ('Fahrrad', 'Im Innenhof gibt es eine begrenzte Zahl an Fahrradständern. Auf dem Weg liegen viele ' 'Fahrradstraßen, nutz die am besten, z. B. mit BBBike.de.'), ('ÖPNV', 'Der Club liegt direkt beim S+U-Bahnhof Friedrichstraße (auch Regionalbahnen). Vom oberirdischen ' 'S-Bahn-Steig (S3/S5/S7) nimmst du den Ausgang Schiffbauerdamm und gehst unter der Bahnbrücke über ' 'die Spree. Über die Albrechtstraße kommst du zur Marienstraße.'), ('Fernverkehr', 'Aussteigen in Berlin Hauptbahnhof, dann mit S-Bahn oder Regionalbahn Richtung Osten eine ' 'Station bis Friedrichstraße. Bahnhof Richtung Spree verlassen, über die Fußgängerbrücke und ' 'der Albrechtstraße bis zur Stichstraße Marienstraße folgen.'), ('Flugzeug', 'BER: Airport Express (RE7, RB14) oder S9 bis Friedrichstraße, alternativ FEX bis Hauptbahnhof ' '(dann wie Fernverkehr). Achtung: Terminal 5 (früher SXF) ist nicht mit dem Rest des BER ' 'verbunden, dort fährt nur die S9. Flugplatz Strausberg (EDAY): S7 direkt bis Friedrichstraße.'), ('Auto', 'Zufahrt von Hauptbahnhof oder Brandenburger Tor über die Luisenstraße. Von der Friedrichstraße ' 'entweder über Schiffbauerdamm und Albrechtstraße (Außengastronomie und Kopfsteinpflaster) oder ' 'über Reinhardstraße und Luisenstraße (die Albrechtstraße ist nur in Gegenrichtung befahrbar).'), ('Parken', 'Achtung: Es gibt kaum Parkplätze, und die Marienstraße und alle umliegenden Straßen sind ' 'parkraumbewirtschaftet.'), ], 'map': 'Karte (OpenStreetMap)', 'footer': [('Impressum', IMPRESSUM), ('CCC Berlin', 'https://berlin.ccc.de/')], } EN = { 'page': 'en.html', 'coc': 'coc-en.html', 'date': 'CCC Berlin · 2026-11-28 – 2026-11-29', 'sections': [(1, 'Info', 'infos'), (2, 'Join in', 'join'), (3, 'Getting here', 'getting-here')], 'intro': [ 'Khaos im Kiez is a small chaos event, back to the roots: inspired by other small chaos events. A weekend to ' 'chill and to experience the side of chaos events that often gets no time at larger events.', 'Give a talk or workshop, work on your projects, hack together, solder, tinker, or simply drop by, listen and ' 'meet people. Everyone who brings a love of $things, curiosity and a bit of chaos is welcome.', ], 'infos': [ ('What', 'Khaos im Kiez (KiK)'), ('When', 'Sat 2026-11-28 – Sun 2026-11-29, 11:00 – 23:59'), ('Where', 'Marienstraße 11, 10117 Berlin, Germany'), ('Who', 'Chaos Computer Club Berlin'), ('Price', '€0.00 (ticket still required, ~50 seats)'), ('Schedule', 'cfp.berlin.ccc.de'), ('Food', 'Eternal breakfast, vegan'), ('Accessibility', 'Not accessible.'), ('Rules', 'Code of Conduct'), ], 'ticket_lead': 'Ticket:', 'ticket': 'Register via the ticket shop. The ticket is free, but seats are limited.', 'get_ticket': 'Get ticket', 'talks_lead': 'Talks & workshops:', 'talks': 'Submit your proposal via the call for participation and check the schedule later.', 'submit': 'Submit a talk', 'schedule': 'Schedule', 'other_lead': 'Other:', 'other': 'Want to help otherwise or have questions? Write to aprl [at] berlin [dot] ccc [dot] de.', 'route': [ ('Address', 'Club rooms in the back yard of Marienstraße 11, 10117 Berlin'), ('Coordinates', '52.52194° N, 13.38261° E'), ('Bicycle', 'A limited number of bike racks are in the courtyard. Many bicycle streets lead there, preferably ' 'use those, e.g. with BBBike.de.'), ('Local transit', 'The club is right next to S+U Friedrichstraße (regional trains stop there too). From the ' 'elevated S-Bahn platform (S3/S5/S7) take the Schiffbauerdamm exit and walk across the Spree ' 'beneath the railway bridge. Albrechtstraße leads to Marienstraße.'), ('Long distance', 'Get off at Berlin Hauptbahnhof, then take any S-Bahn or regional train eastbound one stop ' 'to Friedrichstraße. Leave the station towards the Spree, cross the footbridge and follow ' 'Albrechtstraße to the dead-end Marienstraße.'), ('Air', 'BER: Airport Express (RE7, RB14) or S9 to Friedrichstraße, or FEX to Hauptbahnhof (then as above). ' 'Note: Terminal 5 (formerly SXF) is not connected to the rest of BER, only the S9 runs there. ' 'Strausberg airfield (EDAY): S7 direct to Friedrichstraße.'), ('Car', 'Access from Hauptbahnhof or Brandenburger Tor via Luisenstraße. From Friedrichstraße either via ' 'Schiffbauerdamm and Albrechtstraße (outdoor seating and cobblestones) or via Reinhardstraße and ' 'Luisenstraße (Albrechtstraße is one-way the other direction).'), ('Parking', 'Warning: parking is scarce, and Marienstraße and all surrounding streets are subject to paid ' 'parking zones.'), ], 'map': 'Map (OpenStreetMap)', 'footer': [('Legal notice', IMPRESSUM), ('CCC Berlin', 'https://berlin.ccc.de/')], } COC_DE = { 'intro': 'Khaos im Kiez ist eine Veranstaltung des Chaos Computer Club Berlin e.V. (CCCB). Damit sich alle ' 'wohlfühlen, gelten diese Regeln für alle Teilnehmenden, Vortragenden und Helfenden.', 'rules': [ ('Sei nett zueinander.', 'Geh respektvoll und freundlich miteinander um, auch wenn du anderer Meinung bist.'), ('Keine Diskriminierung.', 'Rassismus, Sexismus, Queerfeindlichkeit, Ableismus und andere menschenverachtende ' 'Haltungen haben hier keinen Platz.'), ('Keine Belästigung.', 'Dazu gehören Beleidigungen, Bedrohungen, Stalking, unerwünschte körperliche ' 'Berührungen und unerwünschte sexuelle Aufmerksamkeit. Ein Nein ist ein Nein.'), ('Frag vor dem Fotografieren.', 'Fotos, Video- und Tonaufnahmen von Personen nur mit deren Einverständnis. ' 'Das gilt auch für Talks und Workshops.'), ('Achte auf Geräte und Eigentum.', 'Fass fremde Geräte nicht ohne Erlaubnis an. Geh pfleglich mit den Räumen ' 'und dem Inventar um.'), ('Sei neugierig, nicht übergriffig.', 'Chaos ja, Grenzenüberschreitungen nein.'), ], 'house_lead': 'Hausrecht:', 'house': 'Die Orga hat Hausrecht. Bei Verstößen gegen diese Regeln kann sie Verwarnungen aussprechen, Personen ' 'der Veranstaltung verweisen oder ein Hausverbot erteilen. Den Anweisungen der Orga ist Folge zu leisten.', 'problems_lead': 'Probleme?', 'problems': 'Wenn dir etwas passiert oder du etwas beobachtest, sprich eine Person aus der Orga an oder schreib an ' 'aprl [at] berlin [dot] ccc [dot] de. Hinweise werden vertraulich behandelt.', 'footer': [('Startseite', SITE), ('Impressum', IMPRESSUM)], } COC_EN = { 'intro': 'Khaos im Kiez is an event of the Chaos Computer Club Berlin e.V. (CCCB). To make everyone feel ' 'comfortable, these rules apply to all attendees, speakers and helpers.', 'rules': [ ('Be nice to each other.', 'Treat each other respectfully and kindly, even when you disagree.'), ('No discrimination.', 'Racism, sexism, queerphobia, ableism and other hateful attitudes have no place here.'), ('No harassment.', 'This includes insults, threats, stalking, unwanted physical contact and unwanted sexual ' 'attention. No means no.'), ('Ask before taking pictures.', 'Photos, video and audio recordings of people only with their consent. This ' 'also applies to talks and workshops.'), ('Respect devices and property.', "Do not touch other people's devices without permission. Treat the rooms " 'and furniture with care.'), ('Be curious, not intrusive.', 'Chaos yes, crossing boundaries no.'), ], 'house_lead': 'House rules:', 'house': 'The organizers have the right to exclude people from the premises (Hausrecht). In case of violations ' "they may issue warnings, ask people to leave, or ban them from the event. Follow the organizers' " 'instructions.', 'problems_lead': 'Problems?', 'problems': 'If something happens to you or you notice something, talk to a member of the organizing team or ' 'write to aprl [at] berlin [dot] ccc [dot] de. Reports are treated confidentially.', 'footer': [('Home', f'{SITE}en.html'), ('Legal notice', IMPRESSUM)], } out = pathlib.Path(__file__).parent.parent / 'resources' out.mkdir(exist_ok=True) for name, text in [('README', readme(DE)), ('README.en', readme(EN)), ('CODE_OF_CONDUCT', code_of_conduct(COC_DE)), ('CODE_OF_CONDUCT.en', code_of_conduct(COC_EN))]: (out / name).write_text(text, encoding='utf-8') # The files for the home directory are not generated, they are copied from ../home, where they are edited. home = pathlib.Path(__file__).parent.parent / 'home' for path in home.rglob('*'): target = out / path.relative_to(home) if path.is_dir(): target.mkdir(parents=True, exist_ok=True) else: target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(path, target)