tonarchy
tonarchy
https://git.tonybtw.com/tonarchy.git
git://git.tonybtw.com/tonarchy.git
initial commit
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;
+}