Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821#!/usr/bin/env python3"""FedAC Setup TUIA terminal UI for WiFi configuration, piece selection, and kiosk management.Adapted from the FFOS ac-setup.py for Fedora.Features purple/pink matrix rain animation.
Run: ac-setup [--force]Access: Ctrl+Alt+F2 from kiosk mode"""
import cursesimport subprocessimport osimport jsonimport timeimport sysimport random
STATE_DIR = os.path.expanduser("~/.state")CONFIG_FILE = os.path.join(STATE_DIR, "ac-config.json")SETUP_DONE_FILE = os.path.join(STATE_DIR, "setup-done")APPIMAGE_PATH = "/opt/ac/bin/aesthetic-computer.AppImage"KIOSK_SERVICE = "ac-electron-kiosk"
# AC pieces (online)PIECES = [ ("prompt", "Prompt — conversational AI canvas"), ("notepat", "Notepat — musical notepad"), ("wand", "Wand — magical drawing tool"), ("painting", "Painting — digital canvas"), ("whistlegraph", "Whistlegraph — collaborative drawing"), ("metronome", "Metronome — tempo keeper"), ("starfield", "Starfield — hypnotic stars"), ("sage", "Sage — wisdom interface"), ("bleep", "Bleep — sound toy"), ("freaky-flowers", "Freaky Flowers — generative art"),]
# Offline pieces (bundled)OFFLINE_PIECES_DIR = "/opt/ac/offline-pieces"OFFLINE_PIECES = [ ("notepat", "Notepat — musical notepad"), ("roz", "$roz — KidLisp demo"), ("starfield", "Starfield — hypnotic stars"),]
# Matrix charactersMATRIX_CHARS = "0123456789ABCDEFabcdef@#$%&*+=<>[]{}|~"HAS_256_COLORS = False
class MatrixRain: """Matrix rain effect with purple/pink colors."""
def __init__(self, height, width): self.height = height self.width = width self.drops = [] self.grid = [[None for _ in range(width)] for _ in range(height)] self._init_drops()
def _init_drops(self): num_drops = max(5, self.width // 4) for _ in range(num_drops): self.drops.append(self._create_drop())
def _create_drop(self, col=None): char = random.choice(MATRIX_CHARS) return { "col": col if col is not None else random.randint(0, max(0, self.width - 1)), "row": random.uniform(-15, -1), "speed": random.uniform(0.2, 0.6), "char": char, "brightness": random.randint(0, 4), "length": random.randint(4, 12), }
def update(self): for y in range(self.height): for x in range(self.width): self.grid[y][x] = None
for drop in self.drops: drop["row"] += drop["speed"] if random.random() < 0.02: drop["char"] = random.choice(MATRIX_CHARS) if drop["row"] - drop["length"] > self.height: col = drop["col"] new_drop = self._create_drop(col) drop.update(new_drop) head_row = int(drop["row"]) for i in range(drop["length"]): y = head_row - i if 0 <= y < self.height and 0 <= drop["col"] < self.width: brightness = max(0, 4 - (i * 4 // drop["length"])) self.grid[y][drop["col"]] = (drop["char"], brightness)
def resize(self, height, width): self.height = height self.width = width self.grid = [[None for _ in range(width)] for _ in range(height)] target_drops = max(5, width // 4) while len(self.drops) < target_drops: self.drops.append(self._create_drop()) while len(self.drops) > target_drops: self.drops.pop()
# ── WiFi (nmcli — same API on Fedora) ──
def get_wifi_networks(): try: subprocess.run(["nmcli", "dev", "wifi", "rescan"], capture_output=True, timeout=5) time.sleep(2) result = subprocess.run( ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY", "dev", "wifi", "list"], capture_output=True, text=True, timeout=10, ) networks = [] seen = set() for line in result.stdout.strip().split("\n"): if line: parts = line.split(":") ssid = parts[0] if parts else "" if ssid and ssid not in seen: seen.add(ssid) signal = parts[1] if len(parts) > 1 else "?" security = parts[2] if len(parts) > 2 else "" networks.append( {"ssid": ssid, "signal": signal, "security": security} ) return sorted(networks, key=lambda x: int(x["signal"] or 0), reverse=True) except Exception: return []
def get_current_wifi(): try: result = subprocess.run( ["nmcli", "-t", "-f", "ACTIVE,SSID", "dev", "wifi"], capture_output=True, text=True, timeout=5, ) for line in result.stdout.strip().split("\n"): if line.startswith("yes:"): return line.split(":", 1)[1] except Exception: pass return None
def connect_wifi(ssid, pw): debug_lines = [] try: debug_lines.append(f"SSID: {ssid}") con_name = f"ac-{ssid}" subprocess.run( ["nmcli", "con", "delete", ssid], capture_output=True, text=True, timeout=10 ) subprocess.run( ["nmcli", "con", "delete", con_name], capture_output=True, text=True, timeout=10, ) add_cmd = [ "nmcli", "con", "add", "type", "wifi", "con-name", con_name, "ssid", ssid, "wifi-sec.key-mgmt", "wpa-psk", "wifi-sec.psk", pw, ] add_result = subprocess.run(add_cmd, capture_output=True, text=True, timeout=15) debug_lines.append(f"Add rc={add_result.returncode}")
if add_result.returncode != 0: cmd = ["nmcli", "dev", "wifi", "connect", ssid, "password", pw] result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) output = result.stderr or result.stdout or "No output" return result.returncode == 0, output.strip(), "\n".join(debug_lines)
up_result = subprocess.run( ["nmcli", "con", "up", con_name], capture_output=True, text=True, timeout=30, ) output = up_result.stderr or up_result.stdout or "No output" return up_result.returncode == 0, output.strip(), "\n".join(debug_lines) except subprocess.TimeoutExpired: return False, "Connection timed out", "\n".join(debug_lines) except Exception as e: return False, str(e), "\n".join(debug_lines)
# ── Config ──
def load_config(): try: with open(CONFIG_FILE, "r") as f: return json.load(f) except Exception: return {"piece": "prompt"}
def save_config(config): os.makedirs(STATE_DIR, exist_ok=True) with open(CONFIG_FILE, "w") as f: json.dump(config, f)
def apply_config(): """Apply config: restart Electron kiosk with the selected piece.""" config = load_config() piece = config.get("piece", "prompt") # The Electron app reads AC_PIECE env or navigates via --piece flag override_dir = os.path.expanduser( f"~/.config/systemd/user/{KIOSK_SERVICE}.service.d" ) os.makedirs(override_dir, exist_ok=True) with open(os.path.join(override_dir, "override.conf"), "w") as f: f.write( f"[Service]\n" f"Environment=AC_PIECE={piece}\n" f"ExecStart=\n" f"ExecStart={APPIMAGE_PATH} --no-sandbox --kiosk --piece={piece}\n" ) subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True) subprocess.run( ["systemctl", "--user", "restart", KIOSK_SERVICE], capture_output=True )
def mark_setup_done(): os.makedirs(STATE_DIR, exist_ok=True) with open(SETUP_DONE_FILE, "w") as f: f.write("done")
def is_setup_done(): return os.path.exists(SETUP_DONE_FILE)
# ── UI helpers ──
PURPLE_BG = 53
def init_colors(): global HAS_256_COLORS curses.start_color() curses.use_default_colors() HAS_256_COLORS = curses.COLORS >= 256
if HAS_256_COLORS: try: curses.init_pair(10, 53, PURPLE_BG) curses.init_pair(11, 91, PURPLE_BG) curses.init_pair(12, 129, PURPLE_BG) curses.init_pair(13, 177, PURPLE_BG) curses.init_pair(14, 219, PURPLE_BG) curses.init_pair(20, 255, PURPLE_BG) except Exception: HAS_256_COLORS = False
if not HAS_256_COLORS: curses.init_pair(10, curses.COLOR_BLACK, curses.COLOR_MAGENTA) curses.init_pair(11, curses.COLOR_MAGENTA, curses.COLOR_MAGENTA) curses.init_pair(12, curses.COLOR_MAGENTA, curses.COLOR_MAGENTA) curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_MAGENTA) curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_MAGENTA) curses.init_pair(20, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
if HAS_256_COLORS: curses.init_pair(1, 255, PURPLE_BG) curses.init_pair(2, 87, PURPLE_BG) curses.init_pair(3, 201, PURPLE_BG) else: curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_MAGENTA) curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_MAGENTA) curses.init_pair(3, curses.COLOR_MAGENTA, curses.COLOR_MAGENTA)
def draw_matrix(win, matrix): h, w = win.getmaxyx() for y in range(min(h - 1, matrix.height)): for x in range(min(w - 1, matrix.width)): cell = matrix.grid[y][x] if cell: char, brightness = cell color_pair = curses.color_pair(10 + brightness) try: win.addch(y, x, char, color_pair) except curses.error: pass
def draw_box(win, y, x, h, w, title="", fill=True): if fill: for row in range(y + 1, y + h - 1): try: win.addstr(row, x + 1, " " * (w - 2)) except Exception: pass try: win.addch(y, x, curses.ACS_ULCORNER) win.addch(y, x + w - 1, curses.ACS_URCORNER) win.addch(y + h - 1, x, curses.ACS_LLCORNER) win.addch(y + h - 1, x + w - 1, curses.ACS_LRCORNER) for i in range(1, w - 1): win.addch(y, x + i, curses.ACS_HLINE) win.addch(y + h - 1, x + i, curses.ACS_HLINE) for i in range(1, h - 1): win.addch(y + i, x, curses.ACS_VLINE) win.addch(y + i, x + w - 1, curses.ACS_VLINE) if title: win.addstr(y, x + 2, f" {title} ", curses.A_BOLD) except curses.error: pass
def center_text(win, y, text, attr=0): try: h, w = win.getmaxyx() x = max(0, (w - len(text)) // 2) win.addstr(y, x, text, attr) except curses.error: pass
def menu_select(stdscr, title, items, selected=0, matrix=None): curses.curs_set(0) h, w = stdscr.getmaxyx() menu_w = min(60, w - 4) menu_h = min(len(items) + 4, h - 4) menu_y = (h - menu_h) // 2 menu_x = (w - menu_w) // 2 visible_items = menu_h - 4 scroll_offset = 0 last_update = time.time() frame_time = 0.1
stdscr.nodelay(True) stdscr.timeout(50)
while True: now = time.time() if matrix and (now - last_update) >= frame_time: matrix.update() last_update = now
stdscr.erase() if matrix: draw_matrix(stdscr, matrix)
center_text( stdscr, 1, "* AESTHETIC COMPUTER *", curses.A_BOLD | curses.color_pair(14), ) draw_box(stdscr, menu_y, menu_x, menu_h, menu_w, title)
if selected < scroll_offset: scroll_offset = selected elif selected >= scroll_offset + visible_items: scroll_offset = selected - visible_items + 1
for i in range(min(visible_items, len(items))): idx = scroll_offset + i if idx >= len(items): break item = items[idx] y = menu_y + 2 + i x = menu_x + 2 try: if idx == selected: stdscr.attron(curses.A_REVERSE) stdscr.addstr(y, x, " " * (menu_w - 4)) stdscr.addstr(y, x + 1, item[: menu_w - 6]) stdscr.attroff(curses.A_REVERSE) else: stdscr.addstr(y, x + 1, item[: menu_w - 6]) except curses.error: pass
try: if scroll_offset > 0: stdscr.addstr(menu_y + 1, menu_x + menu_w - 3, "^") if scroll_offset + visible_items < len(items): stdscr.addstr(menu_y + menu_h - 2, menu_x + menu_w - 3, "v") except curses.error: pass
try: stdscr.addstr( h - 2, 2, "Up/Down Navigate Enter Select q Quit", curses.A_DIM ) except curses.error: pass
stdscr.refresh()
key = stdscr.getch() if key == -1: continue elif key == curses.KEY_UP and selected > 0: selected -= 1 elif key == curses.KEY_DOWN and selected < len(items) - 1: selected += 1 elif key == curses.KEY_HOME: selected = 0 elif key == curses.KEY_END: selected = len(items) - 1 elif key in (curses.KEY_ENTER, 10, 13): stdscr.nodelay(False) stdscr.timeout(-1) return selected elif key in (ord("q"), ord("Q"), 27): stdscr.nodelay(False) stdscr.timeout(-1) return -1 elif key == curses.KEY_RESIZE: h, w = stdscr.getmaxyx() if matrix: matrix.resize(h, w) menu_w = min(60, w - 4) menu_h = min(len(items) + 4, h - 4) menu_y = (h - menu_h) // 2 menu_x = (w - menu_w) // 2
def text_input(stdscr, title, prompt, hidden=False, matrix=None): curses.curs_set(1) h, w = stdscr.getmaxyx() box_w = min(50, w - 4) box_h = 7 box_y = (h - box_h) // 2 box_x = (w - box_w) // 2 text = "" last_update = time.time() frame_time = 0.1
stdscr.nodelay(True) stdscr.timeout(50)
while True: now = time.time() if matrix and (now - last_update) >= frame_time: matrix.update() last_update = now
stdscr.erase() if matrix: draw_matrix(stdscr, matrix)
center_text( stdscr, 1, "* AESTHETIC COMPUTER *", curses.A_BOLD | curses.color_pair(14), ) draw_box(stdscr, box_y, box_x, box_h, box_w, title) try: stdscr.addstr(box_y + 2, box_x + 2, prompt[: box_w - 4]) except curses.error: pass
input_y = box_y + 3 input_x = box_x + 2 input_w = box_w - 4 display = "*" * len(text) if hidden else text try: stdscr.addstr(input_y, input_x, "_" * input_w, curses.A_DIM) stdscr.addstr(input_y, input_x, display[:input_w]) stdscr.addstr(h - 2, 2, "Enter Confirm Esc Cancel", curses.A_DIM) stdscr.move(input_y, input_x + min(len(text), input_w - 1)) except curses.error: pass
stdscr.refresh()
key = stdscr.getch() if key == -1: continue elif key in (curses.KEY_ENTER, 10, 13): curses.curs_set(0) stdscr.nodelay(False) stdscr.timeout(-1) return text elif key == 27: curses.curs_set(0) stdscr.nodelay(False) stdscr.timeout(-1) return None elif key in (curses.KEY_BACKSPACE, 127, 8): text = text[:-1] elif 32 <= key < 127: if len(text) < input_w - 1: text += chr(key)
def show_message(stdscr, title, message, wait=True, matrix=None): curses.curs_set(0) h, w = stdscr.getmaxyx() box_w = min(50, w - 4) box_h = 6 box_y = (h - box_h) // 2 box_x = (w - box_w) // 2 last_update = time.time() show_start = time.time() frame_time = 0.1
stdscr.nodelay(True) stdscr.timeout(50)
while True: now = time.time() if matrix and (now - last_update) >= frame_time: matrix.update() last_update = now
stdscr.erase() if matrix: draw_matrix(stdscr, matrix)
center_text( stdscr, 1, "* AESTHETIC COMPUTER *", curses.A_BOLD | curses.color_pair(14), ) draw_box(stdscr, box_y, box_x, box_h, box_w, title)
words = message.split() lines = [] line = "" for word in words: if len(line) + len(word) + 1 <= box_w - 4: line = line + " " + word if line else word else: lines.append(line) line = word if line: lines.append(line)
for i, ln in enumerate(lines[: box_h - 4]): try: stdscr.addstr(box_y + 2 + i, box_x + 2, ln) except curses.error: pass
if wait: try: stdscr.addstr(h - 2, 2, "Press any key to continue", curses.A_DIM) except curses.error: pass
stdscr.refresh()
key = stdscr.getch() if wait: if key != -1: stdscr.nodelay(False) stdscr.timeout(-1) return else: if now - show_start > 1.5: stdscr.nodelay(False) stdscr.timeout(-1) return
# ── Screens ──
def wifi_setup(stdscr, matrix=None): show_message(stdscr, "WiFi Setup", "Scanning for networks...", wait=False, matrix=matrix) networks = get_wifi_networks() current = get_current_wifi()
if not networks: show_message(stdscr, "WiFi Setup", "No WiFi networks found.", matrix=matrix) return False
items = [] for net in networks: ssid = net["ssid"] signal = net["signal"] lock = "[*]" if net["security"] else " " connected = " <" if ssid == current else "" items.append(f"{ssid} {lock} ({signal}%){connected}") items.append("-" * 30) items.append("Skip WiFi setup")
selected = menu_select(stdscr, "Select WiFi Network", items, matrix=matrix) if selected == -1 or selected >= len(networks): return current is not None
ssid = networks[selected]["ssid"] needs_password = bool(networks[selected]["security"])
password = "" if needs_password: password = text_input( stdscr, "WiFi Password", f"Password for '{ssid}':", hidden=False, matrix=matrix ) if password is None: return False
show_message(stdscr, "Connecting", f"Connecting to {ssid}...", wait=False, matrix=matrix) success, msg, debug = connect_wifi(ssid, password)
if success: show_message(stdscr, "Connected", f"Connected to {ssid}", matrix=matrix) return True else: show_message(stdscr, "Failed", f"Could not connect: {msg}", matrix=matrix) return False
def piece_setup(stdscr, matrix=None): config = load_config() current_piece = config.get("piece", "prompt")
items = [] selected_idx = 0 for i, (code, desc) in enumerate(PIECES): marker = " <" if code == current_piece else "" items.append(f"{desc}{marker}") if code == current_piece: selected_idx = i
selected = menu_select( stdscr, "Select Default Piece", items, selected_idx, matrix=matrix ) if selected == -1: return
piece_code = PIECES[selected][0] config["piece"] = piece_code save_config(config) show_message(stdscr, "Saved", f"Default piece: {piece_code}", matrix=matrix)
def system_info(stdscr, matrix=None): """Show system information.""" info_lines = [] # Hostname try: hostname = subprocess.run( ["hostname"], capture_output=True, text=True, timeout=3 ).stdout.strip() info_lines.append(f"Hostname: {hostname}") except Exception: info_lines.append("Hostname: unknown")
# IP address try: ip = subprocess.run( ["hostname", "-I"], capture_output=True, text=True, timeout=3 ).stdout.strip().split()[0] info_lines.append(f"IP: {ip}") except Exception: info_lines.append("IP: not connected")
# WiFi wifi = get_current_wifi() info_lines.append(f"WiFi: {wifi or 'not connected'}")
# Electron if os.path.isfile(APPIMAGE_PATH): info_lines.append(f"Electron: installed") else: info_lines.append(f"Electron: NOT FOUND")
# Kiosk service try: result = subprocess.run( ["systemctl", "--user", "is-active", KIOSK_SERVICE], capture_output=True, text=True, timeout=3, ) status = result.stdout.strip() info_lines.append(f"Kiosk service: {status}") except Exception: info_lines.append("Kiosk service: unknown")
# Config config = load_config() info_lines.append(f"Current piece: {config.get('piece', 'prompt')}")
show_message( stdscr, "System Info", "\n".join(info_lines), matrix=matrix )
def main_menu(stdscr, matrix=None): while True: current_wifi = get_current_wifi() config = load_config() current_piece = config.get("piece", "prompt")
items = [ f"WiFi Setup [{current_wifi or 'Not connected'}]", f"Select Piece [{current_piece}]", "System Info", "-" * 40, "Start Aesthetic Computer", "-" * 40, "Exit to Shell", ]
selected = menu_select(stdscr, "FedAC Setup", items, matrix=matrix)
if selected == 0: wifi_setup(stdscr, matrix) elif selected == 1: piece_setup(stdscr, matrix) elif selected == 2: system_info(stdscr, matrix) elif selected == 4: mark_setup_done() apply_config() show_message( stdscr, "Starting", "Launching Aesthetic Computer...", wait=False, matrix=matrix, ) return True elif selected == 6 or selected == -1: return False
def show_welcome_screen(stdscr, matrix): h, w = stdscr.getmaxyx() last_update = time.time() frame_time = 0.1
stdscr.nodelay(True) stdscr.timeout(50)
while True: now = time.time() if (now - last_update) >= frame_time: matrix.update() last_update = now
stdscr.erase() draw_matrix(stdscr, matrix)
center_text( stdscr, h // 2 - 3, "* AESTHETIC COMPUTER *", curses.A_BOLD | curses.color_pair(14), ) center_text(stdscr, h // 2 - 1, "FedAC Setup", curses.color_pair(12)) center_text(stdscr, h // 2 + 2, "Press any key to begin...", curses.A_DIM)
stdscr.refresh()
key = stdscr.getch() if key != -1: stdscr.nodelay(False) stdscr.timeout(-1) return
def run_setup(stdscr): init_colors() stdscr.bkgd(" ", curses.color_pair(20)) h, w = stdscr.getmaxyx() matrix = MatrixRain(h, w) show_welcome_screen(stdscr, matrix) return main_menu(stdscr, matrix)
def main(): force = "--force" in sys.argv or "-f" in sys.argv
if is_setup_done() and not force: print("Setup already completed. Use --force to run again.") print("Starting kiosk...") apply_config() return
os.environ.setdefault("TERM", "xterm-256color")
try: result = curses.wrapper(run_setup) if result: print("\nSetup complete! Aesthetic Computer is starting...") else: print("\nSetup cancelled. Run 'ac-setup --force' to configure later.") except KeyboardInterrupt: print("\nSetup interrupted.")
if __name__ == "__main__": main()