tonarchy

tonarchy

https://git.tonybtw.com/tonarchy.git git://git.tonybtw.com/tonarchy.git

initial commit

Commit
c32000cfa164477b3d23c4e3b38789c6203f88f5
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-11-15 06:38:16

Diff

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba31254
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+*.o
+*.a
+*.so
+*.md
+compile_commands.json
+build/
+tonarchy
+.cache/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..608598a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
+CC = gcc
+CFLAGS = -std=c23 -Wall -Wextra -O2 -Iinclude
+LDFLAGS =
+
+SRC_DIR = src
+INC_DIR = include
+BUILD_DIR = build
+TARGET = tonarchy
+
+SRCS = $(wildcard $(SRC_DIR)/*.c)
+OBJS = $(SRCS:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
+
+.PHONY: all clean install
+
+all: $(BUILD_DIR) $(TARGET)
+
+$(BUILD_DIR):
+	mkdir -p $(BUILD_DIR)
+
+$(TARGET): $(OBJS)
+	$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
+
+$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
+	$(CC) $(CFLAGS) -c $< -o $@
+
+clean:
+	rm -rf $(BUILD_DIR) $(TARGET)
+
+install: $(TARGET)
+	install -Dm755 $(TARGET) /usr/local/bin/$(TARGET)
+	cp -r packages /usr/share/tonarchy/
+	cp -r configs /usr/share/tonarchy/
diff --git a/README.org b/README.org
new file mode 100644
index 0000000..c846ed9
--- /dev/null
+++ b/README.org
@@ -0,0 +1,205 @@
+#+TITLE: Tonarchy
+#+AUTHOR: Tony, btw
+#+OPTIONS: toc:2 num:nil
+
+* Tonarchy
+
+A zero-dependency Arch Linux installer with a clean TUI built from scratch.
+
+(Alternative to OMARCHY, but way less bloated and intelligently programmed, and doesn't require a team of devs making 7500 ai slop bash scripts, but instead just basic c.)
+
+*Status:* Work In Progress - TUI complete, installation flow in development
+
+* Philosophy
+
+Tonarchy is designed to take users from *zero to hero* with an opinionated beginner mode that sets up a complete, working Linux desktop environment. No choices, no confusion - just a solid foundation to start learning.
+
+For experienced users, intermediate and advanced modes provide full customization.
+
+* Features
+
+** Three Installation Levels
+
+- *Beginner* :: We'll pick everything for you - opinionated setup with Cinnamon desktop, essential applications, and sane defaults. Perfect for your first Linux installation.
+- *Intermediate* :: Choose desktop environment and toolsets - guided customization with clear options.
+- *Advanced* :: Full control - pick your window manager, display server, packages, and dotfiles.
+
+** Technical Highlights
+
+- *Zero dependencies* :: Raw terminal control using termios + ANSI codes (no ncurses)
+- *Clean C23 codebase* :: Modern C with snake_case conventions
+- *Modular package lists* :: Easy to customize and maintain
+- *Fuzzy finding* :: fzf integration for keyboard and timezone selection
+- *Form-based input* :: Inline cursors with field validation
+
+* Current Status
+
+** Completed ✓
+
+- [X] TUI Framework (raw terminal control)
+- [X] ASCII logo display
+- [X] User input form (6 fields with validation)
+  - Username (alphanumeric)
+  - Password (hidden)
+  - Confirm Password (hidden with validation)
+  - Hostname (default: tonarchy)
+  - Keyboard (fzf fuzzy finder, default: us)
+  - Timezone (fzf fuzzy finder, required)
+- [X] Form confirmation system
+- [X] Level selection menu (j/k navigation)
+- [X] Disk selection with confirmation
+
+** In Progress
+
+- [ ] Disk partitioning and formatting
+- [ ] Pacstrap installation
+- [ ] System configuration (timezone, locale, users)
+- [ ] Bootloader installation
+- [ ] Desktop environment setup
+- [ ] Cinnamon keybindings configuration
+- [ ] Application defaults (Firefox with arkenfox)
+
+* Requirements
+
+- UEFI system
+- Internet connection
+- Boot from Arch Linux ISO or custom Tonarchy ISO
+
+* Building
+
+#+BEGIN_SRC bash
+make clean && make
+./tonarchy
+#+END_SRC
+
+* Project Structure
+
+#+BEGIN_SRC
+tonarchy/
+├── src/
+│   └── main.c          # Main TUI and installer logic
+├── include/
+│   ├── installer.h     # Installation functions
+│   ├── tui.h          # Terminal UI functions
+│   ├── types.h        # Type definitions
+│   └── utils.h        # Utility functions
+├── packages/          # Modular package lists
+│   ├── base.txt
+│   ├── de_cinnamon.txt
+│   ├── de_gnome.txt
+│   ├── de_hyprland.txt
+│   ├── de_kde.txt
+│   ├── de_sway.txt
+│   ├── display_wayland.txt
+│   ├── display_xorg.txt
+│   └── suckless.txt
+├── configs/           # Configuration templates
+├── Makefile
+└── flake.nix         # Nix development shell
+#+END_SRC
+
+* Package Lists
+
+Package lists are modular text files in =packages/=:
+
+- =base.txt= :: Core system packages
+- =de_cinnamon.txt= :: Cinnamon desktop (beginner default)
+- =de_hyprland.txt= :: Hyprland + Wayland tools
+- =de_gnome.txt= :: GNOME desktop
+- =de_kde.txt= :: KDE Plasma
+- =de_sway.txt= :: Sway compositor
+- =display_wayland.txt= :: Wayland support packages
+- =display_xorg.txt= :: Xorg support packages
+
+* Beginner Mode Setup
+
+The beginner installation is completely opinionated and handles everything:
+
+** Desktop Environment
+- Cinnamon (familiar Windows-like interface)
+- LightDM display manager
+- Custom keybindings:
+  - Super+Return :: Terminal (Alacritty)
+  - Super+B :: Browser (Firefox)
+  - Super+E :: File Manager (Nemo)
+  - Super+Q :: Close window
+  - Super+F :: Fullscreen
+
+** Applications
+- Firefox (with optional arkenfox user.js)
+- Zed editor (fully open source)
+- Alacritty terminal
+- VLC media player
+- Nemo file manager
+- Xreader PDF viewer
+- Xviewer image viewer
+
+** System Configuration
+- Locale: =en_US.UTF-8= (hardcoded for simplicity)
+- Timezone: User selected via fzf
+- Keyboard: User selected via fzf (default: us)
+- NetworkManager enabled
+- Sudo configured for wheel group
+
+** Disk Layout
+- 1GB FAT32 EFI boot partition
+- 4GB swap partition
+- Remaining space for ext4 root partition
+
+* Design Decisions
+
+** Why no ncurses?
+Zero dependencies means:
+- Minimal ISO size
+- Easier debugging
+- Full control over terminal behavior
+- No external library issues
+
+** Why fzf for selection?
+- 598 timezones require fuzzy search
+- Ergonomic keyboard layout selection
+- Minimal dependency (~1MB)
+- Fast and responsive
+
+** Why hardcode locale?
+- Simplifies installer UX
+- Most users use en_US.UTF-8
+- Easy to change post-install if needed
+
+** Why Cinnamon for beginners?
+- More Windows-like than KDE
+- Stable and mature
+- Familiar to Linux Mint users
+- Lower learning curve
+
+** Why Zed instead of VSCodium?
+- Fully open source (no marketplace issues)
+- Modern architecture
+- Clean Arch Linux package
+
+* Development
+
+** Language
+C23 with snake_case naming conventions
+
+** Build System
+Simple Makefile for compilation
+
+** Nix Development Shell
+=flake.nix= provides development environment:
+
+#+BEGIN_SRC bash
+nix develop
+#+END_SRC
+
+* Contributing
+
+This is a personal project in active development. Feel free to fork and customize for your own use.
+
+* License
+
+GNU General Public License (GPL)
+
+* Safety Note
+
+The installer will display clear warnings before any destructive operations. Currently, disk partitioning is not implemented, making the TUI safe to test without risk of data loss.
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..9050b51
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+  "nodes": {
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1762977756,
+        "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..aef4783
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,33 @@
+{
+  description = "tonarchy - Minimal Arch Linux installer";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+  };
+
+  outputs = { self, nixpkgs }:
+    let
+      systems = [ "x86_64-linux" "aarch64-linux" ];
+      forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system});
+    in
+    {
+      devShells = forAllSystems (pkgs: {
+        default = pkgs.mkShell {
+          buildInputs = [
+            pkgs.gcc
+            pkgs.gnumake
+            pkgs.ncurses.dev
+            pkgs.notcurses
+            pkgs.pkg-config
+            pkgs.bear
+          ];
+          shellHook = ''
+            export PS1="(tonarchy-dev) $PS1"
+            echo "tonarchy development environment"
+            echo "Run 'make' to build"
+            echo "Run 'make -f Makefile.notcurses && ./notcurses_demo' to see notcurses demo"
+          '';
+        };
+      });
+    };
+}
diff --git a/include/installer.h b/include/installer.h
new file mode 100644
index 0000000..c8f1ac1
--- /dev/null
+++ b/include/installer.h
@@ -0,0 +1,11 @@
+#ifndef INSTALLER_H
+#define INSTALLER_H
+
+#include "types.h"
+
+bool install_system(const install_config *config);
+bool install_packages(const install_config *config);
+bool configure_system(const install_config *config);
+bool setup_bootloader(void);
+
+#endif
diff --git a/include/tui.h b/include/tui.h
new file mode 100644
index 0000000..8f99410
--- /dev/null
+++ b/include/tui.h
@@ -0,0 +1,10 @@
+#ifndef TUI_H
+#define TUI_H
+
+#include "types.h"
+
+void tui_init(void);
+void tui_cleanup(void);
+bool tui_run(install_config *config);
+
+#endif
diff --git a/include/types.h b/include/types.h
new file mode 100644
index 0000000..2263afb
--- /dev/null
+++ b/include/types.h
@@ -0,0 +1,38 @@
+#ifndef TYPES_H
+#define TYPES_H
+
+#include <stdbool.h>
+
+typedef enum {
+    LEVEL_BEGINNER,
+    LEVEL_INTERMEDIATE,
+    LEVEL_ADVANCED
+} install_level;
+
+typedef enum {
+    DE_CINNAMON,
+    DE_HYPRLAND,
+    DE_GNOME,
+    DE_KDE,
+    DE_SWAY,
+    DE_NONE
+} desktop_env;
+
+typedef enum {
+    DISPLAY_WAYLAND,
+    DISPLAY_XORG,
+    DISPLAY_BOTH
+} display_server;
+
+typedef struct {
+    install_level level;
+    desktop_env de;
+    display_server display;
+    bool use_custom_dotfiles;
+    char dotfiles_url[512];
+    bool install_docker;
+    bool install_dev_tools;
+    bool install_gaming;
+} install_config;
+
+#endif
diff --git a/include/utils.h b/include/utils.h
new file mode 100644
index 0000000..c378616
--- /dev/null
+++ b/include/utils.h
@@ -0,0 +1,12 @@
+#ifndef UTILS_H
+#define UTILS_H
+
+#include <stdbool.h>
+
+bool run_command(const char *cmd);
+bool file_exists(const char *path);
+void log_info(const char *msg);
+void log_error(const char *msg);
+char *read_file_list(const char *path);
+
+#endif
diff --git a/logo.txt b/logo.txt
new file mode 100644
index 0000000..d3daaab
--- /dev/null
+++ b/logo.txt
@@ -0,0 +1,10 @@
+                 ▄▄▄
+███████████▄   ▄█████▄    ███   ███   ▄███████   ▄███████   ▄███████   ███   ███  ███   ███
+    ███   ███  ███   ███  ████  ███  ███   ███  ███   ███  ███   ███  ███   ███  ███   ███
+    ███   ███  ███   ███  █████ ███  ███   ███  ███   ███  ███   █▀   ███   ███  ███   ███
+    ███   ███  ███   ███ ▄██ ██ ███ ▄███▄▄▄███ ▄███▄▄▄██▀  ███       ▄███▄▄▄███▄ ███▄▄▄███
+    ███   ███  ███   ███ ▀██  █████ ▀███▀▀▀███ ▀███▀▀▀▀    ███      ▀▀███▀▀▀███  ▀▀▀▀▀▀███
+    ███   ███  ███   ███  ██   ████  ███   ███ ██████████  ███   █▄   ███   ███  ▄██   ███
+    ███   ███  ███   ███  ██    ███  ███   ███  ███   ███  ███   ███  ███   ███  ███   ███
+    ███   █▀    ▀█████▀   ██    ███  ███   █▀   ███   ███  ███████▀   ███   █▀    ▀█████▀
+                                                ███   █▀
diff --git a/packages/base.txt b/packages/base.txt
new file mode 100644
index 0000000..b5232d5
--- /dev/null
+++ b/packages/base.txt
@@ -0,0 +1,17 @@
+base
+base-devel
+linux
+linux-firmware
+linux-headers
+networkmanager
+git
+vim
+neovim
+curl
+wget
+htop
+btop
+man-db
+man-pages
+openssh
+sudo
diff --git a/packages/de_cinnamon.txt b/packages/de_cinnamon.txt
new file mode 100644
index 0000000..45a28c4
--- /dev/null
+++ b/packages/de_cinnamon.txt
@@ -0,0 +1,10 @@
+cinnamon
+cinnamon-translations
+nemo
+nemo-fileroller
+gnome-terminal
+lightdm
+lightdm-gtk-greeter
+file-roller
+eog
+evince
diff --git a/packages/de_gnome.txt b/packages/de_gnome.txt
new file mode 100644
index 0000000..33b7f21
--- /dev/null
+++ b/packages/de_gnome.txt
@@ -0,0 +1,6 @@
+gnome
+gnome-tweaks
+gnome-shell-extensions
+gdm
+nautilus
+gnome-terminal
diff --git a/packages/de_hyprland.txt b/packages/de_hyprland.txt
new file mode 100644
index 0000000..affbf6d
--- /dev/null
+++ b/packages/de_hyprland.txt
@@ -0,0 +1,18 @@
+hyprland
+hyprlock
+hypridle
+hyprpaper
+waybar
+wofi
+dunst
+kitty
+alacritty
+wl-clipboard
+grim
+slurp
+swayidle
+swaylock
+polkit-kde-agent
+qt5-wayland
+qt6-wayland
+xdg-desktop-portal-hyprland
diff --git a/packages/de_kde.txt b/packages/de_kde.txt
new file mode 100644
index 0000000..7252fc9
--- /dev/null
+++ b/packages/de_kde.txt
@@ -0,0 +1,5 @@
+plasma-meta
+kde-applications-meta
+sddm
+konsole
+dolphin
diff --git a/packages/de_sway.txt b/packages/de_sway.txt
new file mode 100644
index 0000000..88d29c5
--- /dev/null
+++ b/packages/de_sway.txt
@@ -0,0 +1,11 @@
+sway
+swaylock
+swayidle
+waybar
+wofi
+foot
+mako
+wl-clipboard
+grim
+slurp
+xdg-desktop-portal-wlr
diff --git a/packages/dev_tools.txt b/packages/dev_tools.txt
new file mode 100644
index 0000000..a01fd79
--- /dev/null
+++ b/packages/dev_tools.txt
@@ -0,0 +1,17 @@
+gcc
+clang
+rustup
+go
+nodejs
+npm
+python
+python-pip
+gdb
+valgrind
+strace
+lazygit
+ripgrep
+fd
+fzf
+tmux
+zsh
diff --git a/packages/display_wayland.txt b/packages/display_wayland.txt
new file mode 100644
index 0000000..c307fcb
--- /dev/null
+++ b/packages/display_wayland.txt
@@ -0,0 +1,3 @@
+wayland
+wayland-protocols
+xorg-xwayland
diff --git a/packages/display_xorg.txt b/packages/display_xorg.txt
new file mode 100644
index 0000000..b07534c
--- /dev/null
+++ b/packages/display_xorg.txt
@@ -0,0 +1,4 @@
+xorg-server
+xorg-xinit
+xorg-xrandr
+xorg-xsetroot
diff --git a/packages/docker.txt b/packages/docker.txt
new file mode 100644
index 0000000..814d705
--- /dev/null
+++ b/packages/docker.txt
@@ -0,0 +1,3 @@
+docker
+docker-compose
+docker-buildx
diff --git a/packages/gaming.txt b/packages/gaming.txt
new file mode 100644
index 0000000..8e0498d
--- /dev/null
+++ b/packages/gaming.txt
@@ -0,0 +1,10 @@
+steam
+lutris
+wine
+wine-mono
+wine-gecko
+winetricks
+gamemode
+lib32-gamemode
+mangohud
+lib32-mangohud
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..d7ed675
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,730 @@
+#define _POSIX_C_SOURCE 200809L
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <ctype.h>
+
+static struct termios orig_termios;
+
+static void disable_raw_mode(void) {
+    tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
+}
+
+static void enable_raw_mode(void) {
+    tcgetattr(STDIN_FILENO, &orig_termios);
+    atexit(disable_raw_mode);
+
+    struct termios raw = orig_termios;
+    raw.c_lflag &= ~(ECHO | ICANON);
+    raw.c_cc[VMIN] = 1;
+    raw.c_cc[VTIME] = 0;
+
+    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
+}
+
+static void get_terminal_size(int *rows, int *cols) {
+    struct winsize ws;
+    ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
+    *rows = ws.ws_row;
+    *cols = ws.ws_col;
+}
+
+static void clear_screen(void) {
+    printf("\033[2J\033[H");
+    fflush(stdout);
+}
+
+static void draw_logo(int cols) {
+    const char *logo[] = {
+        "████████╗ ██████╗ ███╗   ██╗ █████╗ ██████╗  ██████╗██╗  ██╗██╗   ██╗",
+        "╚══██╔══╝██╔═══██╗████╗  ██║██╔══██╗██╔══██╗██╔════╝██║  ██║╚██╗ ██╔╝",
+        "   ██║   ██║   ██║██╔██╗ ██║███████║██████╔╝██║     ███████║ ╚████╔╝ ",
+        "   ██║   ██║   ██║██║╚██╗██║██╔══██║██╔══██╗██║     ██╔══██║  ╚██╔╝  ",
+        "   ██║   ╚██████╔╝██║ ╚████║██║  ██║██║  ██║╚██████╗██║  ██║   ██║   ",
+        "   ╚═╝    ╚═════╝ ╚═╝  ╚═══╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝   ╚═╝   "
+    };
+
+    int logo_height = 6;
+    int logo_start = (cols - 70) / 2;
+
+    printf("\033[1;32m");
+    for (int i = 0; i < logo_height; i++) {
+        printf("\033[%d;%dH%s", i + 2, logo_start, logo[i]);
+    }
+    printf("\033[0m");
+}
+
+static int draw_menu(const char **items, int count, int selected) {
+    int rows, cols;
+    get_terminal_size(&rows, &cols);
+
+    clear_screen();
+    draw_logo(cols);
+
+    int logo_start = (cols - 70) / 2;
+    int menu_start_row = 10;
+
+    for (int i = 0; i < count; i++) {
+        printf("\033[%d;%dH", menu_start_row + i, logo_start + 2);
+        if (i == selected) {
+            printf("\033[1;34m> %s\033[0m", items[i]);
+        } else {
+            printf("\033[37m  %s\033[0m", items[i]);
+        }
+    }
+
+    printf("\033[%d;%dH", menu_start_row + count + 2, logo_start);
+    printf("\033[33mj/k Navigate  Enter Select\033[0m");
+
+    fflush(stdout);
+    return 0;
+}
+
+static int select_from_menu(const char **items, int count) {
+    int selected = 0;
+
+    enable_raw_mode();
+    draw_menu(items, count, selected);
+
+    char c;
+    while (read(STDIN_FILENO, &c, 1) == 1) {
+        if (c == 'q' || c == 27) {
+            disable_raw_mode();
+            return -1;
+        }
+
+        if (c == 'j' || c == 66) {
+            if (selected < count - 1) {
+                selected++;
+                draw_menu(items, count, selected);
+            }
+        }
+
+        if (c == 'k' || c == 65) {
+            if (selected > 0) {
+                selected--;
+                draw_menu(items, count, selected);
+            }
+        }
+
+        if (c == '\r' || c == '\n') {
+            disable_raw_mode();
+            return selected;
+        }
+    }
+
+    disable_raw_mode();
+    return -1;
+}
+
+static void show_message(const char *message) {
+    int rows, cols;
+    get_terminal_size(&rows, &cols);
+
+    clear_screen();
+    draw_logo(cols);
+
+    int logo_start = (cols - 70) / 2;
+    printf("\033[%d;%dH", 10, logo_start);
+    printf("\033[37m%s\033[0m", message);
+    fflush(stdout);
+
+    sleep(2);
+}
+
+static void draw_form(
+        const char *username,
+        const char *password,
+        const char *confirmed_password,
+        const char *hostname,
+        const char *keyboard,
+        const char *timezone,
+        int current_field
+    ) {
+    int rows, cols;
+    get_terminal_size(&rows, &cols);
+
+    clear_screen();
+    draw_logo(cols);
+
+    int logo_start = (cols - 70) / 2;
+    int form_row = 10;
+
+    printf("\033[%d;%dH", form_row, logo_start);
+    printf("\033[37mSetup your system:\033[0m");
+
+    form_row += 2;
+
+    if (current_field == 0) {
+        printf("\033[%d;%dH\033[1;34m>\033[0m ", form_row, logo_start);
+    } else {
+        printf("\033[%d;%dH  ", form_row, logo_start);
+    }
+    printf("\033[37mUsername: \033[0m");
+    if (strlen(username) > 0) {
+        printf("\033[32m%s\033[0m", username);
+    } else if (current_field != 0) {
+        printf("\033[90m[not set]\033[0m");
+    }
+
+    form_row++;
+
+    if (current_field == 1) {
+        printf("\033[%d;%dH\033[1;34m>\033[0m ", form_row, logo_start);
+    } else {
+        printf("\033[%d;%dH  ", form_row, logo_start);
+    }
+    printf("\033[37mPassword: \033[0m");
+    if (strlen(password) > 0) {
+        printf("\033[32m%s\033[0m", "********");
+    } else if (current_field != 1) {
+        printf("\033[90m[not set]\033[0m");
+    }
+
+    form_row++;
+
+    if (current_field == 2) {
+        printf("\033[%d;%dH\033[1;34m>\033[0m ", form_row, logo_start);
+    } else {
+        printf("\033[%d;%dH  ", form_row, logo_start);
+    }
+    printf("\033[37mConfirm Password: \033[0m");
+    if (strlen(confirmed_password) > 0) {
+        printf("\033[32m%s\033[0m", "********");
+    } else if (current_field != 2) {
+        printf("\033[90m[not set]\033[0m");
+    }
+
+    form_row++;
+
+    if (current_field == 3) {
+        printf("\033[%d;%dH\033[1;34m>\033[0m ", form_row, logo_start);
+    } else {
+        printf("\033[%d;%dH  ", form_row, logo_start);
+    }
+    printf("\033[37mHostname: \033[0m");
+    if (strlen(hostname) > 0) {
+        printf("\033[32m%s\033[0m", hostname);
+    } else if (current_field != 3) {
+        printf("\033[90mtonarchy\033[0m");
+    }
+
+    form_row++;
+
+    if (current_field == 4) {
+        printf("\033[%d;%dH\033[1;34m>\033[0m ", form_row, logo_start);
+    } else {
+        printf("\033[%d;%dH  ", form_row, logo_start);
+    }
+    printf("\033[37mKeyboard: \033[0m");
+    if (strlen(keyboard) > 0) {
+        printf("\033[32m%s\033[0m", keyboard);
+    } else if (current_field != 4) {
+        printf("\033[90mus\033[0m");
+    }
+
+    form_row++;
+
+    if (current_field == 5) {
+        printf("\033[%d;%dH\033[1;34m>\033[0m ", form_row, logo_start);
+    } else {
+        printf("\033[%d;%dH  ", form_row, logo_start);
+    }
+    printf("\033[37mTimezone: \033[0m");
+    if (strlen(timezone) > 0) {
+        printf("\033[32m%s\033[0m", timezone);
+    } else if (current_field != 5) {
+        printf("\033[90m[not set]\033[0m");
+    }
+
+    fflush(stdout);
+}
+
+static int get_form_input(
+        char *username,
+        char *password,
+        char *confirmed_password,
+        char *hostname,
+        char *keyboard,
+        char *timezone
+    ) {
+    char temp_input[256];
+    char password_confirm[256];
+    int current_field = 0;
+    int rows, cols;
+    get_terminal_size(&rows, &cols);
+    int logo_start = (cols - 70) / 2;
+    int form_row = 12;
+
+    while (current_field < 6) {
+        draw_form(username, password, confirmed_password, hostname, keyboard, timezone, current_field);
+
+        if (current_field == 0) {
+            printf("\033[%d;%dH", form_row, logo_start + 13);
+            fflush(stdout);
+
+            struct termios old_term;
+            tcgetattr(STDIN_FILENO, &old_term);
+            struct termios new_term = old_term;
+            new_term.c_lflag |= (ECHO | ICANON);
+            new_term.c_lflag &= ~ISIG;
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+            if (fgets(temp_input, sizeof(temp_input), stdin) == NULL) {
+                tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                return 0;
+            }
+            temp_input[strcspn(temp_input, "\n")] = '\0';
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+
+            if (strlen(temp_input) > 0) {
+                int valid = 1;
+                for (int i = 0; temp_input[i]; i++) {
+                    if (!isalnum(temp_input[i]) && temp_input[i] != '-' && temp_input[i] != '_') {
+                        valid = 0;
+                        break;
+                    }
+                }
+                if (valid) {
+                    strcpy(username, temp_input);
+                    current_field++;
+                } else {
+                    show_message("Username must be alphanumeric");
+                }
+            }
+        } else if (current_field == 1) {
+            printf("\033[%d;%dH", form_row + 1, logo_start + 13);
+            fflush(stdout);
+
+            struct termios old_term;
+            tcgetattr(STDIN_FILENO, &old_term);
+            struct termios new_term = old_term;
+            new_term.c_lflag &= ~ECHO;
+            new_term.c_lflag |= ICANON;
+            new_term.c_lflag &= ~ISIG;
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+            if (fgets(temp_input, sizeof(temp_input), stdin) == NULL) {
+                tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                return 0;
+            }
+            temp_input[strcspn(temp_input, "\n")] = '\0';
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+
+            if (strlen(temp_input) == 0) {
+                show_message("Password cannot be empty");
+                continue;
+            }
+
+            strcpy(password, temp_input);
+            current_field++;
+        } else if (current_field == 2) {
+            printf("\033[%d;%dH", form_row + 2, logo_start + 20);
+            fflush(stdout);
+
+            struct termios old_term;
+            tcgetattr(STDIN_FILENO, &old_term);
+            struct termios new_term = old_term;
+            new_term.c_lflag &= ~ECHO;
+            new_term.c_lflag |= ICANON;
+            new_term.c_lflag &= ~ISIG;
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+            if (fgets(password_confirm, sizeof(password_confirm), stdin) == NULL) {
+                tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                return 0;
+            }
+            password_confirm[strcspn(password_confirm, "\n")] = '\0';
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+
+            if (strcmp(password, password_confirm) == 0) {
+                strcpy(confirmed_password, password_confirm);
+                current_field++;
+            } else {
+                show_message("Passwords do not match");
+            }
+        } else if (current_field == 3) {
+            printf("\033[%d;%dH", form_row + 3, logo_start + 13);
+            fflush(stdout);
+
+            struct termios old_term;
+            tcgetattr(STDIN_FILENO, &old_term);
+            struct termios new_term = old_term;
+            new_term.c_lflag |= (ECHO | ICANON);
+            new_term.c_lflag &= ~ISIG;
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+            if (fgets(temp_input, sizeof(temp_input), stdin) == NULL) {
+                tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                return 0;
+            }
+            temp_input[strcspn(temp_input, "\n")] = '\0';
+            tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+
+            if (strlen(temp_input) == 0) {
+                strcpy(hostname, "tonarchy");
+            } else {
+                int valid = 1;
+                for (int i = 0; temp_input[i]; i++) {
+                    if (!isalnum(temp_input[i]) && temp_input[i] != '-' && temp_input[i] != '_') {
+                        valid = 0;
+                        break;
+                    }
+                }
+                if (valid) {
+                    strcpy(hostname, temp_input);
+                } else {
+                    show_message("Hostname must be alphanumeric");
+                    continue;
+                }
+            }
+            current_field++;
+        } else if (current_field == 4) {
+            clear_screen();
+            FILE *fp = popen("localectl list-keymaps | fzf --height=40% --reverse --prompt='Keyboard: ' --header='Start typing to filter, Enter to select' --query='us'", "r");
+            if (fp == NULL) {
+                show_message("Failed to open keyboard selector");
+                continue;
+            }
+
+            if (fgets(temp_input, sizeof(temp_input), fp) != NULL) {
+                temp_input[strcspn(temp_input, "\n")] = '\0';
+                if (strlen(temp_input) > 0) {
+                    strcpy(keyboard, temp_input);
+                }
+            }
+            pclose(fp);
+
+            if (strlen(keyboard) == 0) {
+                strcpy(keyboard, "us");
+            }
+            current_field++;
+        } else if (current_field == 5) {
+            clear_screen();
+            FILE *fp = popen("timedatectl list-timezones | fzf --height=40% --reverse --prompt='Timezone: ' --header='Type your city/timezone, Enter to select'", "r");
+            if (fp == NULL) {
+                show_message("Failed to open timezone selector");
+                continue;
+            }
+
+            if (fgets(temp_input, sizeof(temp_input), fp) != NULL) {
+                temp_input[strcspn(temp_input, "\n")] = '\0';
+                if (strlen(temp_input) > 0) {
+                    strcpy(timezone, temp_input);
+                }
+            }
+            pclose(fp);
+
+            if (strlen(timezone) == 0) {
+                show_message("Timezone is required");
+            } else {
+                current_field++;
+            }
+        }
+    }
+
+    while (1) {
+        draw_form(username, password, confirmed_password, hostname, keyboard, timezone, 6);
+
+        int rows, cols;
+        get_terminal_size(&rows, &cols);
+        int logo_start = (cols - 70) / 2;
+
+        printf("\033[%d;%dH\033[33mPress Enter to continue, or field number to edit (0-5)\033[0m", 20, logo_start);
+        fflush(stdout);
+
+        enable_raw_mode();
+        char c;
+        if (read(STDIN_FILENO, &c, 1) == 1) {
+            if (c == '\r' || c == '\n') {
+                disable_raw_mode();
+                return 1;
+            }
+            if (c == 'q' || c == 27) {
+                disable_raw_mode();
+                return 0;
+            }
+            if (c >= '0' && c <= '5') {
+                disable_raw_mode();
+                int edit_field = c - '0';
+
+                if (edit_field == 0) {
+                    draw_form(username, password, confirmed_password, hostname, keyboard, timezone, 0);
+                    printf("\033[%d;%dH", form_row, logo_start + 13);
+                    fflush(stdout);
+
+                    struct termios old_term;
+                    tcgetattr(STDIN_FILENO, &old_term);
+                    struct termios new_term = old_term;
+                    new_term.c_lflag |= (ECHO | ICANON);
+                    new_term.c_lflag &= ~ISIG;
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+                    if (fgets(temp_input, sizeof(temp_input), stdin) != NULL) {
+                        temp_input[strcspn(temp_input, "\n")] = '\0';
+                        if (strlen(temp_input) > 0) {
+                            int valid = 1;
+                            for (int i = 0; temp_input[i]; i++) {
+                                if (!isalnum(temp_input[i]) && temp_input[i] != '-' && temp_input[i] != '_') {
+                                    valid = 0;
+                                    break;
+                                }
+                            }
+                            if (valid) {
+                                strcpy(username, temp_input);
+                            } else {
+                                show_message("Username must be alphanumeric");
+                            }
+                        }
+                    }
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                } else if (edit_field == 1) {
+                    draw_form(username, password, confirmed_password, hostname, keyboard, timezone, 1);
+                    printf("\033[%d;%dH", form_row + 1, logo_start + 13);
+                    fflush(stdout);
+
+                    struct termios old_term;
+                    tcgetattr(STDIN_FILENO, &old_term);
+                    struct termios new_term = old_term;
+                    new_term.c_lflag &= ~ECHO;
+                    new_term.c_lflag |= ICANON;
+                    new_term.c_lflag &= ~ISIG;
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+                    if (fgets(temp_input, sizeof(temp_input), stdin) != NULL) {
+                        temp_input[strcspn(temp_input, "\n")] = '\0';
+                        if (strlen(temp_input) > 0) {
+                            strcpy(password, temp_input);
+
+                            draw_form(username, password, confirmed_password, hostname, keyboard, timezone, 2);
+                            printf("\033[%d;%dH", form_row + 2, logo_start + 20);
+                            fflush(stdout);
+
+                            if (fgets(password_confirm, sizeof(password_confirm), stdin) != NULL) {
+                                password_confirm[strcspn(password_confirm, "\n")] = '\0';
+                                if (strcmp(password, password_confirm) == 0) {
+                                    strcpy(confirmed_password, password_confirm);
+                                } else {
+                                    show_message("Passwords do not match");
+                                }
+                            }
+                        }
+                    }
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                } else if (edit_field == 2) {
+                    draw_form(username, password, confirmed_password, hostname, keyboard, timezone, 1);
+                    printf("\033[%d;%dH", form_row + 1, logo_start + 13);
+                    fflush(stdout);
+
+                    struct termios old_term;
+                    tcgetattr(STDIN_FILENO, &old_term);
+                    struct termios new_term = old_term;
+                    new_term.c_lflag &= ~ECHO;
+                    new_term.c_lflag |= ICANON;
+                    new_term.c_lflag &= ~ISIG;
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+                    if (fgets(temp_input, sizeof(temp_input), stdin) != NULL) {
+                        temp_input[strcspn(temp_input, "\n")] = '\0';
+                        if (strlen(temp_input) > 0) {
+                            strcpy(password, temp_input);
+
+                            draw_form(username, password, confirmed_password, hostname, keyboard, timezone, 2);
+                            printf("\033[%d;%dH", form_row + 2, logo_start + 20);
+                            fflush(stdout);
+
+                            if (fgets(password_confirm, sizeof(password_confirm), stdin) != NULL) {
+                                password_confirm[strcspn(password_confirm, "\n")] = '\0';
+                                if (strcmp(password, password_confirm) == 0) {
+                                    strcpy(confirmed_password, password_confirm);
+                                } else {
+                                    show_message("Passwords do not match");
+                                }
+                            }
+                        }
+                    }
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                } else if (edit_field == 3) {
+                    draw_form(username, password, confirmed_password, hostname, keyboard, timezone, 3);
+                    printf("\033[%d;%dH", form_row + 3, logo_start + 13);
+                    fflush(stdout);
+
+                    struct termios old_term;
+                    tcgetattr(STDIN_FILENO, &old_term);
+                    struct termios new_term = old_term;
+                    new_term.c_lflag |= (ECHO | ICANON);
+                    new_term.c_lflag &= ~ISIG;
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+                    if (fgets(temp_input, sizeof(temp_input), stdin) != NULL) {
+                        temp_input[strcspn(temp_input, "\n")] = '\0';
+                        if (strlen(temp_input) == 0) {
+                            strcpy(hostname, "tonarchy");
+                        } else {
+                            int valid = 1;
+                            for (int i = 0; temp_input[i]; i++) {
+                                if (!isalnum(temp_input[i]) && temp_input[i] != '-' && temp_input[i] != '_') {
+                                    valid = 0;
+                                    break;
+                                }
+                            }
+                            if (valid) {
+                                strcpy(hostname, temp_input);
+                            } else {
+                                show_message("Hostname must be alphanumeric");
+                            }
+                        }
+                    }
+                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+                } else if (edit_field == 4) {
+                    clear_screen();
+                    FILE *fp = popen("localectl list-keymaps | fzf --height=40% --reverse --prompt='Keyboard: ' --header='Start typing to filter, Enter to select' --query='us'", "r");
+                    if (fp != NULL) {
+                        if (fgets(temp_input, sizeof(temp_input), fp) != NULL) {
+                            temp_input[strcspn(temp_input, "\n")] = '\0';
+                            if (strlen(temp_input) > 0) {
+                                strcpy(keyboard, temp_input);
+                            }
+                        }
+                        pclose(fp);
+                    }
+                    if (strlen(keyboard) == 0) {
+                        strcpy(keyboard, "us");
+                    }
+                } else if (edit_field == 5) {
+                    clear_screen();
+                    FILE *fp = popen("timedatectl list-timezones | fzf --height=40% --reverse --prompt='Timezone: ' --header='Type your city/timezone, Enter to select'", "r");
+                    if (fp != NULL) {
+                        if (fgets(temp_input, sizeof(temp_input), fp) != NULL) {
+                            temp_input[strcspn(temp_input, "\n")] = '\0';
+                            if (strlen(temp_input) > 0) {
+                                strcpy(timezone, temp_input);
+                            }
+                        }
+                        pclose(fp);
+                    }
+                    if (strlen(timezone) == 0) {
+                        show_message("Timezone is required");
+                    }
+                }
+                continue;
+            }
+        }
+        disable_raw_mode();
+    }
+
+    return 1;
+}
+
+static int select_disk(char *disk_name) {
+    clear_screen();
+
+    FILE *fp = popen("lsblk -d -n -o NAME,SIZE,MODEL | awk '{printf \"%s (%s) %s\\n\", $1, $2, substr($0, index($0,$3))}'", "r");
+    if (fp == NULL) {
+        show_message("Failed to list disks");
+        return 0;
+    }
+
+    char disks[32][256];
+    char names[32][64];
+    int disk_count = 0;
+
+    while (disk_count < 32 && fgets(disks[disk_count], sizeof(disks[0]), fp) != NULL) {
+        disks[disk_count][strcspn(disks[disk_count], "\n")] = '\0';
+        sscanf(disks[disk_count], "%s", names[disk_count]);
+        disk_count++;
+    }
+    pclose(fp);
+
+    if (disk_count == 0) {
+        show_message("No disks found");
+        return 0;
+    }
+
+    const char *disk_ptrs[32];
+    for (int i = 0; i < disk_count; i++) {
+        disk_ptrs[i] = disks[i];
+    }
+
+    int selected = select_from_menu(disk_ptrs, disk_count);
+    if (selected < 0) {
+        return 0;
+    }
+
+    strcpy(disk_name, names[selected]);
+
+    int rows, cols;
+    get_terminal_size(&rows, &cols);
+    clear_screen();
+    draw_logo(cols);
+
+    int logo_start = (cols - 70) / 2;
+    printf("\033[%d;%dH\033[37mWARNING: All data on \033[31m/dev/%s\033[37m will be destroyed!\033[0m", 10, logo_start, disk_name);
+    printf("\033[%d;%dH\033[37mType 'yes' to confirm: \033[0m", 12, logo_start);
+    fflush(stdout);
+
+    char confirm[256];
+    struct termios old_term;
+    tcgetattr(STDIN_FILENO, &old_term);
+    struct termios new_term = old_term;
+    new_term.c_lflag |= (ECHO | ICANON);
+    new_term.c_lflag &= ~ISIG;
+    tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term);
+
+    if (fgets(confirm, sizeof(confirm), stdin) == NULL) {
+        tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+        return 0;
+    }
+    confirm[strcspn(confirm, "\n")] = '\0';
+    tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
+
+    if (strcmp(confirm, "yes") != 0) {
+        show_message("Installation cancelled");
+        return 0;
+    }
+
+    return 1;
+}
+
+int main(void) {
+    char username[256] = "";
+    char password[256] = "";
+    char confirmed_password[256] = "";
+    char hostname[256] = "";
+    char keyboard[256] = "";
+    char timezone[256] = "";
+
+    if (!get_form_input(username, password, confirmed_password, hostname, keyboard, timezone)) {
+        return 1;
+    }
+
+    const char *levels[] = {
+        "Beginner (We'll pick everything for you.)",
+        "Intermediate (choose desktop & tools)",
+        "Advanced (full customization)"
+    };
+
+    int level = select_from_menu(levels, 3);
+    if (level < 0) {
+        return 1;
+    }
+
+    char disk[64] = "";
+    if (!select_disk(disk)) {
+        return 1;
+    }
+
+    clear_screen();
+    printf("Installation would proceed with:\n");
+    printf("Username: %s\n", username);
+    printf("Hostname: %s\n", hostname);
+    printf("Keyboard: %s\n", keyboard);
+    printf("Timezone: %s\n", timezone);
+    printf("Level: %s\n", levels[level]);
+    printf("Disk: /dev/%s\n", disk);
+
+    return 0;
+}