From a6c7f0851872048e685ce4cbb6de704c62c1a1a2 Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Mon, 12 May 2025 17:54:14 +0200 Subject: [PATCH] Implement Windows 9x Manager with GUI for launching Windows 95/98 in DOSBox-X - Removed win95_launcher.sh script and replaced it with a new Python application (win9xman.py) for better functionality and user experience. - Added a new assets directory creation script (create_assets_dir.sh) to ensure necessary directories are set up. - Created a default DOSBox-X configuration file (dosbox.conf) and a template for future configurations (dosbox_template.conf). - Added templates for autoexec configurations for Windows 95 and 98. - Updated requirements.txt to indicate no external dependencies beyond the Python standard library. - Implemented various features in the GUI including HDD image creation, ISO mounting, snapshot management, and settings configuration. - Enhanced user interaction with progress dialogs and confirmation prompts for critical actions. --- .gitignore | 34 +- README.md | 83 +-- assets/create_assets_dir.sh | 3 + config/dosbox.conf | 71 +++ requirements.txt | 2 + templates/dosbox_template.conf | 71 +++ templates/win95_autoexec_template.txt | 9 + templates/win98_autoexec_template.txt | 9 + win95_launcher.sh | 383 ------------ win9xman.py | 814 ++++++++++++++++++++++++++ 10 files changed, 1054 insertions(+), 425 deletions(-) create mode 100644 assets/create_assets_dir.sh create mode 100644 config/dosbox.conf create mode 100644 requirements.txt create mode 100644 templates/dosbox_template.conf create mode 100644 templates/win95_autoexec_template.txt create mode 100644 templates/win98_autoexec_template.txt delete mode 100644 win95_launcher.sh create mode 100644 win9xman.py diff --git a/.gitignore b/.gitignore index a0bf3f9..54b9fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# DOSBox-X Windows Manager .gitignore +# Windows 9x Manager .gitignore # Disk images *.img @@ -11,11 +11,10 @@ # Snapshots snapshots/* +snapshots_win95/* # Generated/temporary config files temp_dosbox.conf -temp_dosbox_win95.conf -temp_config_tune.conf # Backup files *.bak @@ -31,12 +30,35 @@ temp_config_tune.conf win98_drive/* win95_drive/* +# Python specific +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +venv/ +.venv/ +.env/ +.python-version + # Editor specific files .vscode/ .idea/ *.swp *.swo .DS_Store - -# Zenity temporary files -.zenity_* diff --git a/README.md b/README.md index 97d0dfb..76ee84d 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,109 @@ -# DOSBox-X Windows 9x Manager +# Windows 9x Manager -A graphical launcher tool for running Windows 95/98 in DOSBox-X with advanced management features. +A Python-based GUI tool for running Windows 95/98 in DOSBox-X with advanced management features. -![DOSBox-X Windows Manager](logo.png) +![Windows 9x Manager](assets/logo.png) ## Overview -DOSBox-X Windows Manager provides a user-friendly GUI for running Windows 95 and 98 in DOSBox-X. It handles disk image creation, ISO mounting, snapshots, and performance tuning to make retro Windows usage seamless and accessible. +Windows 9x Manager provides a user-friendly GUI for running Windows 95 and 98 in DOSBox-X. It handles disk image creation, ISO mounting, snapshots, and performance tuning to make retro Windows usage seamless and accessible. ## Features +- **Modern Python Interface**: Easy to use Tkinter GUI +- **Dual OS Support**: Handles both Windows 95 and Windows 98 - **Easy Installation**: Boot directly from installation ISO files - **HDD Image Management**: Create and format hard disk images with customizable sizes - **Snapshot System**: Save and restore system states with named snapshots - **CD-ROM Support**: Mount ISO files to install software or games -- **Performance Tuning**: Optimize DOSBox-X settings for Windows 95 (in win95_launcher) -- **User-friendly Interface**: Simple GUI powered by Zenity +- **User-friendly Interface**: Simple, cross-platform GUI ## Requirements +- Python 3.6 or higher - DOSBox-X (https://dosbox-x.com/) -- Zenity for GUI dialogs -- Bash shell -- Windows 95/98 installation media (ISO format) +- Tkinter (usually included with Python) ## Installation 1. Clone this repository: ``` - git clone https://github.com/yourusername/dosbox-x-windows-manager.git - cd dosbox-x-windows-manager + git clone https://github.com/yourusername/win9xman.git + cd win9xman ``` -2. Make the launcher scripts executable: +2. Make sure you have Python 3.6+ installed: ``` - chmod +x win98_launcher.sh win95_launcher.sh + python --version ``` -3. Run the appropriate launcher: +3. Install DOSBox-X for your platform if not already installed: + - Windows: Download from [DOSBox-X website](https://dosbox-x.com/) + - Linux: Use your package manager or follow instructions on DOSBox-X website + - macOS: Use Homebrew: `brew install dosbox-x` + +4. Run the Windows 9x Manager: + ``` + python win9xman.py ``` - # For Windows 98: - ./win98_launcher.sh - # For Windows 95: - ./win95_launcher.sh + On Linux/macOS you can make it executable first: + ``` + chmod +x win9xman.py + ./win9xman.py ``` ## Directory Structure -- `win98_launcher.sh` - Windows 98 launcher script -- `win95_launcher.sh` - Windows 95 launcher script -- `dosbox.conf` - DOSBox-X configuration file +- `win9xman.py` - Main Python launcher script +- `config/` - Configuration files directory + - `dosbox.conf` - DOSBox-X configuration file - `win98_drive/` - Directory for Windows 98 files (optional) - `win95_drive/` - Directory for Windows 95 files (optional) - `iso/` - Directory for ISO files - `disks/` - Directory for disk images - `snapshots/` - Directory for Windows 98 snapshots - `snapshots_win95/` - Directory for Windows 95 snapshots +- `assets/` - Icons and graphics for the application ## Usage Guide ### First-time Setup -1. Run the launcher for your Windows version -2. Select "Boot from Windows 9x ISO" option -3. Choose your Windows installation ISO -4. Follow the Windows setup process +1. Run `win9xman.py` +2. Select your Windows version (95 or 98) +3. Click "Install Windows from ISO" +4. Choose your Windows installation ISO +5. Follow the Windows setup process ### Creating Snapshots 1. Make changes to your Windows system 2. Exit to the launcher -3. Select "Create snapshot of current disk image" +3. Select "Create Snapshot" 4. Enter a name for your snapshot ### Restoring Snapshots -1. Select "Restore snapshot" from the launcher +1. Select "Restore Snapshot" from the launcher 2. Choose the snapshot you wish to restore 3. Confirm the restoration -## Performance Tuning (Windows 95) +## Troubleshooting -The Windows 95 launcher includes performance tuning options: +- **DOSBox-X not found**: Ensure DOSBox-X is installed and in your PATH +- **Windows fails to install**: Ensure your disk image is large enough +- **Installation errors**: Try the "Format Hard Disk" option to create a fresh disk image +- **UI errors**: Make sure you have Tkinter installed (`python -m tkinter` should show a test window) -- **Standard**: Balanced settings for most users -- **Fast**: Better performance but less accuracy -- **Accurate**: Slower but more accurate emulation +## Development -## Troubleshooting +To contribute to Windows 9x Manager: -- **Windows fails to install**: Ensure your disk image is large enough -- **Installation errors**: Try the "Format C: drive" option to create a fresh disk image -- **Performance issues**: Use the tuning options in the Windows 95 launcher +1. Fork the repository +2. Create a feature branch +3. Add your changes +4. Submit a pull request ## License diff --git a/assets/create_assets_dir.sh b/assets/create_assets_dir.sh new file mode 100644 index 0000000..664d6cf --- /dev/null +++ b/assets/create_assets_dir.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# This is just a placeholder to create the assets directory +mkdir -p /home/enne2/Development/win9xman/assets diff --git a/config/dosbox.conf b/config/dosbox.conf new file mode 100644 index 0000000..baeca36 --- /dev/null +++ b/config/dosbox.conf @@ -0,0 +1,71 @@ +# DOSBox-X configuration file for Windows 9x Manager + +[sdl] +fullscreen=false +fulldouble=true +fullresolution=desktop +windowresolution=1024x768 +output=opengl +autolock=true + +[dosbox] +language= +machine=svga_s3 +captures=capture +memsize=64 + +[render] +frameskip=0 +aspect=true +scaler=normal3x + +[cpu] +core=dynamic +cputype=pentium_mmx +cycles=max 80% limit 33000 +cycleup=500 +cycledown=500 + +[mixer] +nosound=false +rate=44100 +blocksize=1024 +prebuffer=40 + +[midi] +mpu401=intelligent +mididevice=default + +[sblaster] +sbtype=sb16 +sbbase=220 +irq=7 +dma=1 +hdma=5 +sbmixer=true +oplmode=auto +oplemu=default +oplrate=44100 + +[gus] +gus=false +gusrate=44100 +gusbase=240 +irq1=5 +dma1=1 + +[speaker] +pcspeaker=true +pcrate=44100 +tandy=auto +tandyrate=44100 +disney=true + +[dos] +xms=true +ems=true +umb=true +keyboardlayout=auto + +[ipx] +ipx=false diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1603c7e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# No external dependencies required beyond Python standard library +# Python 3.6+ with Tkinter (usually included with Python) diff --git a/templates/dosbox_template.conf b/templates/dosbox_template.conf new file mode 100644 index 0000000..bbeb8b5 --- /dev/null +++ b/templates/dosbox_template.conf @@ -0,0 +1,71 @@ +# DOSBox-X configuration file for Windows 9x Manager + +[sdl] +fullscreen=false +fulldouble=true +fullresolution=desktop +windowresolution=${windowresolution} +output=${output} +autolock=true + +[dosbox] +language= +machine=${machine} +captures=capture +memsize=${memsize} + +[render] +frameskip=0 +aspect=true +scaler=normal3x + +[cpu] +core=dynamic +cputype=pentium_mmx +cycles=${cycles} +cycleup=500 +cycledown=500 + +[mixer] +nosound=false +rate=44100 +blocksize=1024 +prebuffer=40 + +[midi] +mpu401=intelligent +mididevice=default + +[sblaster] +sbtype=sb16 +sbbase=220 +irq=7 +dma=1 +hdma=5 +sbmixer=true +oplmode=auto +oplemu=default +oplrate=44100 + +[gus] +gus=false +gusrate=44100 +gusbase=240 +irq1=5 +dma1=1 + +[speaker] +pcspeaker=true +pcrate=44100 +tandy=auto +tandyrate=44100 +disney=true + +[dos] +xms=true +ems=true +umb=true +keyboardlayout=auto + +[ipx] +ipx=false diff --git a/templates/win95_autoexec_template.txt b/templates/win95_autoexec_template.txt new file mode 100644 index 0000000..c070cfc --- /dev/null +++ b/templates/win95_autoexec_template.txt @@ -0,0 +1,9 @@ +# Mount the Windows 95 HDD image as drive C +imgmount c "${hdd_image}" -t hdd -fs fat + +# Mount the Windows 95 drive as E +mount e "${drive_dir}" + +${iso_mount} + +${boot_command} diff --git a/templates/win98_autoexec_template.txt b/templates/win98_autoexec_template.txt new file mode 100644 index 0000000..5e02795 --- /dev/null +++ b/templates/win98_autoexec_template.txt @@ -0,0 +1,9 @@ +# Mount the Windows 98 HDD image as drive C +imgmount c "${hdd_image}" -t hdd -fs fat + +# Mount the Windows 98 drive as E +mount e "${drive_dir}" + +${iso_mount} + +${boot_command} diff --git a/win95_launcher.sh b/win95_launcher.sh deleted file mode 100644 index 383d8a3..0000000 --- a/win95_launcher.sh +++ /dev/null @@ -1,383 +0,0 @@ -#!/bin/bash - -# Win95 DOSBox-X Launcher -# A GUI tool to launch Windows 95 in DOSBox-X with different scenarios - -# Check if zenity is installed -if ! command -v zenity &> /dev/null; then - echo "Zenity is not installed. Please install it using:" - echo "sudo apt-get install zenity" - exit 1 -fi - -# Base directory -BASE_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -DOSBOX_CONF="$BASE_DIR/dosbox.conf" -WIN95_DRIVE="$BASE_DIR/win95_drive" -ISO_DIR="$BASE_DIR/iso" -IMG_DIR="$BASE_DIR/disks" -HDD_IMAGE="$IMG_DIR/win95.img" -SNAPSHOT_DIR="$BASE_DIR/snapshots_win95" -DEFAULT_HDD_SIZE=1000 # Default size in MB for HDD image (smaller for Win95) -MIN_HDD_SIZE=300 # Minimum size in MB (Win95 needs less space) -MAX_HDD_SIZE=2000 # Maximum size in MB - -# Make sure directories exist -mkdir -p "$WIN95_DRIVE" "$ISO_DIR" "$IMG_DIR" "$SNAPSHOT_DIR" - -# Function to create HDD image if it doesn't exist -create_hdd_image() { - if [ ! -f "$HDD_IMAGE" ]; then - # Let user choose the size with slider - HDD_SIZE=$(zenity --scale --title="Select HDD Size" \ - --text="Choose the size of your Windows 95 hard disk (MB):" \ - --min-value=$MIN_HDD_SIZE --max-value=$MAX_HDD_SIZE --value=$DEFAULT_HDD_SIZE \ - --step=100) - - # Check if user cancelled the dialog - if [ $? -ne 0 ]; then - return 1 - fi - - # If no size was selected, use default - if [ -z "$HDD_SIZE" ]; then - HDD_SIZE=$DEFAULT_HDD_SIZE - fi - - zenity --question --text="Create new disk image of ${HDD_SIZE}MB?" --title="Confirm HDD Creation" --ok-label="Yes" --cancel-label="No" - if [ $? -ne 0 ]; then - return 1 - fi - - # Show progress dialog while creating the image - ( - echo "# Creating disk image of ${HDD_SIZE}MB..." - # Create disk image using DOSBox-X's imgmake command - dosbox-x -c "imgmake \"$HDD_IMAGE\" -size ${HDD_SIZE} -fat 16 -t hd" > /dev/null 2>&1 - if [ $? -ne 0 ]; then - zenity --error --text="Failed to create HDD image. Please check your permissions." - return 1 - fi - - echo "100" - echo "# Disk image created successfully!" - ) | zenity --progress --title="Creating HDD Image" --text="Creating disk image..." --percentage=0 --auto-close --no-cancel - - return 0 - fi - return 0 -} - -# Function to create temporary config file based on scenario -create_temp_config() { - local autoexec_content="$1" - local temp_conf="$BASE_DIR/temp_dosbox_win95.conf" - - # Copy the original config file - cp "$DOSBOX_CONF" "$temp_conf" - - # Replace the [autoexec] section - sed -i '/\[autoexec\]/,$ d' "$temp_conf" - echo "[autoexec]" >> "$temp_conf" - echo "$autoexec_content" >> "$temp_conf" - - echo "$temp_conf" -} - -# Function to start Windows 95 if installed -start_win95() { - # Check if HDD image exists - if [ ! -f "$HDD_IMAGE" ]; then - zenity --error --text="HDD image not found. Please create one first." - return - fi - - local autoexec=$(cat << EOF -# Mount the Windows 95 HDD image as drive C -imgmount c "$HDD_IMAGE" -t hdd -fs fat -c: -# Start Windows 95 -win -EOF -) - local temp_conf=$(create_temp_config "$autoexec") - dosbox-x -conf "$temp_conf" - rm "$temp_conf" -} - -# Function to browse and mount an ISO -mount_iso() { - # Check if HDD image exists - if [ ! -f "$HDD_IMAGE" ]; then - if ! create_hdd_image; then - return - fi - fi - - local iso_path=$(zenity --file-selection --title="Select ISO file" --file-filter="ISO files (*.iso) | *.iso" --filename="$ISO_DIR/") - - if [ -z "$iso_path" ]; then - zenity --error --text="No ISO file selected." - return - fi - - local autoexec=$(cat << EOF -# Mount the Windows 95 HDD image as drive C -imgmount c "$HDD_IMAGE" -t hdd -fs fat - -# Mount the ISO as drive D -imgmount d "$iso_path" -t iso - -c: -EOF -) - local temp_conf=$(create_temp_config "$autoexec") - dosbox-x -conf "$temp_conf" - rm "$temp_conf" -} - -# Function to boot from ISO -boot_iso() { - # Check if we should create a new HDD image - if [ ! -f "$HDD_IMAGE" ]; then - if ! create_hdd_image; then - return - fi - fi - - local iso_path=$(zenity --file-selection --title="Select Windows 95 Installation ISO" --file-filter="ISO files (*.iso) | *.iso" --filename="$ISO_DIR/") - - if [ -z "$iso_path" ]; then - zenity --error --text="No ISO file selected." - return - fi - - local autoexec=$(cat << EOF -# Mount the Windows 95 HDD image as drive C -imgmount c "$HDD_IMAGE" -t hdd -fs fat - -# Mount the ISO as drive D -imgmount d "$iso_path" -t iso -# Run the setup program -d: -# Start the Windows 95 setup (might be different paths for different Win95 CDs) -if exist setup.exe setup.exe -if exist win95\setup.exe cd win95 && setup.exe -EOF -) - local temp_conf=$(create_temp_config "$autoexec") - dosbox-x -conf "$temp_conf" - rm "$temp_conf" -} - -# Function to format C drive (the disk image) -format_c() { - # Check if HDD image exists - if [ ! -f "$HDD_IMAGE" ]; then - if ! create_hdd_image; then - return - fi - zenity --info --text="New disk image created. It's already blank and ready to use." - return - fi - - local confirm=$(zenity --question --text="This will delete the existing disk image and create a new blank one.\nAll data will be lost.\nDo you want to continue?" --title="Confirm Format" --ok-label="Yes" --cancel-label="No") - - if [ $? -ne 0 ]; then - return - fi - - # Remove existing image - rm -f "$HDD_IMAGE" - - # Create new image with user-selected size - create_hdd_image - - if [ $? -eq 0 ]; then - zenity --info --text="Format completed. A new blank disk image has been created." - fi -} - -# Function to create a snapshot of the current disk image -create_snapshot() { - # Check if HDD image exists - if [ ! -f "$HDD_IMAGE" ]; then - zenity --error --text="HDD image not found. Cannot create snapshot." - return 1 - fi - - # Get snapshot name from user - local snapshot_name=$(zenity --entry --title="Create Snapshot" \ - --text="Enter a name for this snapshot:" \ - --entry-text="Windows95_Snapshot") - - # Check if user cancelled - if [ $? -ne 0 ] || [ -z "$snapshot_name" ]; then - return 1 - fi - - # Create a valid filename (replace spaces and special chars) - snapshot_name=$(echo "$snapshot_name" | tr ' ' '_' | tr -cd '[:alnum:]_-') - - # Add timestamp to snapshot name to make it unique - local timestamp=$(date "+%Y%m%d_%H%M%S") - local snapshot_file="$SNAPSHOT_DIR/${timestamp}_${snapshot_name}.img" - - # Show progress while copying the image - ( - echo "# Creating snapshot: $snapshot_name..." - cp "$HDD_IMAGE" "$snapshot_file" - if [ $? -ne 0 ]; then - zenity --error --text="Failed to create snapshot. Check disk space and permissions." - return 1 - fi - echo "100" - echo "# Snapshot created successfully!" - ) | zenity --progress --title="Creating Snapshot" --text="Creating snapshot..." --percentage=0 --auto-close --no-cancel - - zenity --info --title="Snapshot Created" --text="Snapshot '$snapshot_name' created successfully.\nLocation: $snapshot_file" - return 0 -} - -# Function to restore a snapshot -restore_snapshot() { - # Check if snapshots directory exists and has at least one snapshot - if [ ! -d "$SNAPSHOT_DIR" ] || [ -z "$(ls -A "$SNAPSHOT_DIR")" ]; then - zenity --error --text="No snapshots found." - return 1 - fi - - # Create a list of available snapshots - local snapshots=() - for snap in "$SNAPSHOT_DIR"/*.img; do - local snap_name=$(basename "$snap") - snapshots+=("$snap" "$snap_name") - done - - # Let user select a snapshot - local selected_snapshot=$(zenity --list --title="Restore Snapshot" \ - --text="Select a snapshot to restore:" \ - --column="Path" --column="Snapshot Name" \ - "${snapshots[@]}" \ - --hide-column=1 --width=500 --height=300) - - # Check if user cancelled - if [ $? -ne 0 ] || [ -z "$selected_snapshot" ]; then - return 1 - fi - - # Confirm before restoring - zenity --question --title="Confirm Restore" \ - --text="This will replace your current disk image with the selected snapshot.\nAll unsaved changes will be lost.\n\nContinue?" \ - --ok-label="Restore" --cancel-label="Cancel" - - if [ $? -ne 0 ]; then - return 1 - fi - - # Show progress while restoring the snapshot - ( - echo "# Restoring snapshot..." - # Create backup of current image first - if [ -f "$HDD_IMAGE" ]; then - mv "$HDD_IMAGE" "${HDD_IMAGE}.bak" - fi - - # Copy the snapshot to the disk image location - cp "$selected_snapshot" "$HDD_IMAGE" - if [ $? -ne 0 ]; then - zenity --error --text="Failed to restore snapshot. Check permissions." - # Try to restore the backup - if [ -f "${HDD_IMAGE}.bak" ]; then - mv "${HDD_IMAGE}.bak" "$HDD_IMAGE" - fi - return 1 - fi - - # Remove backup if restore was successful - rm -f "${HDD_IMAGE}.bak" - - echo "100" - echo "# Snapshot restored successfully!" - ) | zenity --progress --title="Restoring Snapshot" --text="Restoring snapshot..." --percentage=0 --auto-close --no-cancel - - zenity --info --title="Snapshot Restored" --text="Snapshot restored successfully." - return 0 -} - -# Function to tune DOSBox-X settings for optimal Windows 95 performance -tune_settings() { - local choice=$(zenity --list --title="Tune Windows 95 Settings" \ - --text="Select a performance profile:" \ - --column="Profile" --column="Description" \ - 1 "Standard (Balanced settings)" \ - 2 "Fast (Better performance but less accurate)" \ - 3 "Accurate (Slower but more accurate emulation)" \ - --width=500 --height=200) - - if [ -z "$choice" ]; then - return - fi - - local temp_conf="$BASE_DIR/temp_config_tune.conf" - cp "$DOSBOX_CONF" "$temp_conf" - - case "$choice" in - 1) # Standard profile - sed -i 's/^cycles=.*/cycles=max 80% limit 15000/' "$temp_conf" - sed -i 's/^core=.*/core=normal/' "$temp_conf" - ;; - 2) # Fast profile - sed -i 's/^cycles=.*/cycles=max 100% limit 30000/' "$temp_conf" - sed -i 's/^core=.*/core=dynamic/' "$temp_conf" - ;; - 3) # Accurate profile - sed -i 's/^cycles=.*/cycles=max 70% limit 10000/' "$temp_conf" - sed -i 's/^core=.*/core=normal/' "$temp_conf" - ;; - esac - - # Ask if user wants to make the changes permanent - zenity --question --title="Save Changes" \ - --text="Would you like to make these settings permanent?\nIf you select 'No', the changes will only apply to the next session." \ - --ok-label="Yes" --cancel-label="No" - - if [ $? -eq 0 ]; then - cp "$temp_conf" "$DOSBOX_CONF" - zenity --info --title="Settings Saved" --text="Performance settings have been saved." - else - zenity --info --title="Temporary Settings" --text="Settings will be applied for the next session only." - fi -} - -# Main menu function -main_menu() { - while true; do - local choice=$(zenity --list --title="Windows 95 DOSBox-X Launcher" \ - --text="Select an option:" \ - --column="Option" --column="Description" \ - 1 "Start Windows 95 (if installed)" \ - 2 "Mount ISO and start Windows 95" \ - 3 "Boot from Windows 95 ISO (for installation)" \ - 4 "Format C: drive" \ - 5 "Create snapshot of current disk image" \ - 6 "Restore snapshot" \ - 7 "Tune performance settings" \ - 8 "Exit" \ - --width=500 --height=380) - - case "$choice" in - 1) start_win95 ;; - 2) mount_iso ;; - 3) boot_iso ;; - 4) format_c ;; - 5) create_snapshot ;; - 6) restore_snapshot ;; - 7) tune_settings ;; - 8|"") exit 0 ;; - esac - done -} - -# Start the main menu -main_menu diff --git a/win9xman.py b/win9xman.py new file mode 100644 index 0000000..a88f263 --- /dev/null +++ b/win9xman.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python3 +""" +Windows 9x Manager - A GUI tool to launch Windows 95/98 in DOSBox-X +""" + +import os +import sys +import subprocess +import shutil +import time +import configparser +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +from tkinter.simpledialog import askstring +from datetime import datetime +from pathlib import Path +import string + +class Win9xManager: + def __init__(self, root): + self.root = root + self.root.title("Windows 9x Manager") + self.root.geometry("600x450") + self.root.minsize(600, 450) + + # Set up base paths + self.base_dir = Path(os.path.dirname(os.path.realpath(__file__))) + self.templates_dir = self.base_dir / "templates" + self.dosbox_conf = self.base_dir / "config" / "dosbox.conf" + self.win98_drive = self.base_dir / "win98_drive" + self.win95_drive = self.base_dir / "win95_drive" + self.iso_dir = self.base_dir / "iso" + self.img_dir = self.base_dir / "disks" + self.win98_hdd = self.img_dir / "win98.img" + self.win95_hdd = self.img_dir / "win95.img" + self.snapshot_dir = self.base_dir / "snapshots" + self.snapshot_win95_dir = self.base_dir / "snapshots_win95" + + # Default settings + self.default_hdd_size = 2000 # Default size in MB for HDD image + self.min_hdd_size = 500 # Minimum size in MB + self.max_hdd_size = 4000 # Maximum size in MB + + # Current OS selection + self.current_os = tk.StringVar(value="win98") + + # Create necessary directories + self._create_directories() + + # Create UI + self._create_ui() + + def _create_directories(self): + """Create necessary directories if they don't exist""" + directories = [ + self.win98_drive, + self.win95_drive, + self.iso_dir, + self.img_dir, + self.snapshot_dir, + self.snapshot_win95_dir, + self.base_dir / "config", + self.templates_dir + ] + + for directory in directories: + directory.mkdir(exist_ok=True, parents=True) + + # Create default DOSBox-X config if it doesn't exist + if not self.dosbox_conf.exists(): + self._create_default_config() + + def _create_default_config(self): + """Create a default DOSBox-X configuration file from template""" + config_dir = self.base_dir / "config" + config_dir.mkdir(exist_ok=True) + + # Check if template exists + template_path = self.templates_dir / "dosbox_template.conf" + if not template_path.exists(): + self._create_default_template() + + # Create config from template + template_vars = { + 'memsize': '64', + 'cycles': 'max 80% limit 33000', + 'machine': 'svga_s3', + 'windowresolution': '1024x768', + 'output': 'opengl' + } + self._generate_config_from_template('dosbox_template.conf', self.dosbox_conf, template_vars) + + def _create_default_template(self): + """Create the default DOSBox-X template file""" + template_path = self.templates_dir / "dosbox_template.conf" + + # Make sure templates directory exists + self.templates_dir.mkdir(exist_ok=True, parents=True) + + # Write template content + with open(template_path, 'w') as f: + f.write("""# DOSBox-X configuration file for Windows 9x Manager + +[sdl] +fullscreen=false +fulldouble=true +fullresolution=desktop +windowresolution=${windowresolution} +output=${output} +autolock=true + +[dosbox] +language= +machine=${machine} +captures=capture +memsize=${memsize} + +[render] +frameskip=0 +aspect=true +scaler=normal3x + +[cpu] +core=dynamic +cputype=pentium_mmx +cycles=${cycles} +cycleup=500 +cycledown=500 + +[mixer] +nosound=false +rate=44100 +blocksize=1024 +prebuffer=40 + +[midi] +mpu401=intelligent +mididevice=default + +[sblaster] +sbtype=sb16 +sbbase=220 +irq=7 +dma=1 +hdma=5 +sbmixer=true +oplmode=auto +oplemu=default +oplrate=44100 + +[gus] +gus=false +gusrate=44100 +gusbase=240 +irq1=5 +dma1=1 + +[speaker] +pcspeaker=true +pcrate=44100 +tandy=auto +tandyrate=44100 +disney=true + +[dos] +xms=true +ems=true +umb=true +keyboardlayout=auto + +[ipx] +ipx=false +""") + + def _generate_config_from_template(self, template_name, output_path, variables): + """Generate a configuration file from a template with variable substitution + + Args: + template_name: Name of the template file in templates directory + output_path: Path where to save the generated config + variables: Dictionary of variables to substitute in the template + """ + template_path = self.templates_dir / template_name + + # Read template content + if not template_path.exists(): + raise FileNotFoundError(f"Template file {template_path} not found") + + with open(template_path, 'r') as f: + template_content = f.read() + + # Use string.Template for variable substitution + template = string.Template(template_content) + output_content = template.safe_substitute(variables) + + # Write the output file + with open(output_path, 'w') as f: + f.write(output_content) + + def _create_ui(self): + """Create the UI elements""" + # OS Selection frame + os_frame = ttk.LabelFrame(self.root, text="Select Windows Version") + os_frame.pack(fill=tk.X, padx=10, pady=5) + + ttk.Radiobutton(os_frame, text="Windows 98", variable=self.current_os, + value="win98").pack(side=tk.LEFT, padx=20, pady=5) + ttk.Radiobutton(os_frame, text="Windows 95", variable=self.current_os, + value="win95").pack(side=tk.LEFT, padx=20, pady=5) + + # Main actions frame + actions_frame = ttk.Frame(self.root) + actions_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Create buttons with descriptions + button_data = [ + ("Start Windows", "Launch Windows if already installed", self.start_windows), + ("Mount ISO & Start Windows", "Mount ISO as CD-ROM drive and start Windows", self.mount_iso), + ("Install Windows from ISO", "Boot from ISO to install Windows", self.boot_iso), + ("Format Hard Disk", "Create or reset disk image", self.format_disk), + ("Create Snapshot", "Save current system state", self.create_snapshot), + ("Restore Snapshot", "Restore previous system state", self.restore_snapshot), + ("Settings", "Configure DOSBox-X settings", self.open_settings), + ("Exit", "Close the application", self.root.quit) + ] + + for i, (text, desc, command) in enumerate(button_data): + frame = ttk.Frame(actions_frame) + frame.pack(fill=tk.X, pady=5) + + btn = ttk.Button(frame, text=text, command=command, width=20) + btn.pack(side=tk.LEFT, padx=5) + + ttk.Label(frame, text=desc).pack(side=tk.LEFT, padx=5) + + def get_current_hdd(self): + """Get current HDD path based on selected OS""" + return self.win98_hdd if self.current_os.get() == "win98" else self.win95_hdd + + def get_current_drive_dir(self): + """Get current drive directory based on selected OS""" + return self.win98_drive if self.current_os.get() == "win98" else self.win95_drive + + def get_snapshot_dir(self): + """Get snapshot directory based on selected OS""" + return self.snapshot_dir if self.current_os.get() == "win98" else self.snapshot_win95_dir + + def create_hdd_image(self): + """Create HDD image if it doesn't exist""" + hdd_image = self.get_current_hdd() + + if hdd_image.exists(): + return True + + # Create slider dialog for HDD size + size_dialog = tk.Toplevel(self.root) + size_dialog.title("Select HDD Size") + size_dialog.geometry("400x150") + size_dialog.resizable(False, False) + size_dialog.transient(self.root) + size_dialog.grab_set() + + ttk.Label(size_dialog, text="Choose the size of your Windows hard disk (MB):").pack(pady=10) + + size_var = tk.IntVar(value=self.default_hdd_size) + slider = ttk.Scale(size_dialog, from_=self.min_hdd_size, to=self.max_hdd_size, + variable=size_var, orient=tk.HORIZONTAL, length=300) + slider.pack(pady=10, padx=20) + + size_label = ttk.Label(size_dialog, text=f"{self.default_hdd_size} MB") + size_label.pack() + + def update_label(*args): + size_label.config(text=f"{size_var.get()} MB") + + slider.bind("", update_label) + + result = [False] # Use list to store result (Python 3.x closure behavior) + + def on_ok(): + result[0] = True + size_dialog.destroy() + + def on_cancel(): + size_dialog.destroy() + + button_frame = ttk.Frame(size_dialog) + button_frame.pack(fill=tk.X, pady=10) + ttk.Button(button_frame, text="Create", command=on_ok).pack(side=tk.LEFT, padx=20) + ttk.Button(button_frame, text="Cancel", command=on_cancel).pack(side=tk.RIGHT, padx=20) + + self.root.wait_window(size_dialog) + + if not result[0]: + return False + + hdd_size = size_var.get() + + # Confirm creation + if not messagebox.askyesno("Confirm HDD Creation", + f"Create new disk image of {hdd_size}MB?"): + return False + + # Show progress while creating the image + progress_window = tk.Toplevel(self.root) + progress_window.title("Creating HDD Image") + progress_window.geometry("400x100") + progress_window.transient(self.root) + progress_window.grab_set() + + ttk.Label(progress_window, text=f"Creating disk image of {hdd_size}MB...").pack(pady=10) + progress = ttk.Progressbar(progress_window, mode="indeterminate", length=300) + progress.pack(pady=10, padx=20) + progress.start() + + # Schedule the actual creation task + def create_task(): + try: + # Create disk image using DOSBox-X's imgmake command + cmd = ["dosbox-x", "-c", f"imgmake \"{hdd_image}\" -size {hdd_size} -fat 32 -t hd", "-c", "exit"] + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + progress_window.destroy() + messagebox.showinfo("Success", f"Disk image of {hdd_size}MB created successfully.") + return True + except subprocess.CalledProcessError: + progress_window.destroy() + messagebox.showerror("Error", "Failed to create HDD image. Please check your permissions.") + return False + + self.root.after(100, create_task) + self.root.wait_window(progress_window) + + return hdd_image.exists() + + def create_temp_config(self, autoexec_content): + """Create a temporary DOSBox-X configuration file with custom autoexec section""" + # Make sure the base config exists + if not self.dosbox_conf.exists(): + self._create_default_config() + + # Create a temp config based on current settings + temp_conf = self.base_dir / "temp_dosbox.conf" + + # Read current config to preserve settings + current_config = {} + config = configparser.ConfigParser() + config.read(self.dosbox_conf) + + for section in config.sections(): + if section != 'autoexec': + current_config[section] = dict(config[section]) + + # Generate the temporary config file from template + template_vars = { + 'memsize': current_config.get('dosbox', {}).get('memsize', '64'), + 'cycles': current_config.get('cpu', {}).get('cycles', 'max 80% limit 33000'), + 'machine': current_config.get('dosbox', {}).get('machine', 'svga_s3'), + 'windowresolution': current_config.get('sdl', {}).get('windowresolution', '1024x768'), + 'output': current_config.get('sdl', {}).get('output', 'opengl') + } + + # Create temp config + self._generate_config_from_template('dosbox_template.conf', temp_conf, template_vars) + + # Add autoexec section to the generated config + with open(temp_conf, 'a') as f: + f.write("\n[autoexec]\n") + for line in autoexec_content.split('\n'): + if line.strip(): + f.write(f"{line}\n") + + return temp_conf + + def start_windows(self): + """Start Windows if installed""" + hdd_image = self.get_current_hdd() + drive_dir = self.get_current_drive_dir() + + # Check if HDD image exists + if not hdd_image.exists(): + if not messagebox.askyesno("HDD Missing", + "HDD image not found. Do you want to create one?"): + return + + if not self.create_hdd_image(): + return + + # Create autoexec content + autoexec = f""" +# Mount the Windows HDD image as drive C +imgmount c "{hdd_image}" -t hdd -fs fat +# Mount the local directory as drive E +mount e "{drive_dir}" +boot c: +""" + + # Create temporary config + temp_conf = self.create_temp_config(autoexec) + + # Launch DOSBox-X with the config + try: + subprocess.run(["dosbox-x", "-conf", temp_conf], check=True) + finally: + # Clean up temp config + if temp_conf.exists(): + temp_conf.unlink() + + def mount_iso(self): + """Mount ISO and start Windows""" + hdd_image = self.get_current_hdd() + drive_dir = self.get_current_drive_dir() + + # Check if HDD image exists + if not hdd_image.exists(): + if messagebox.askyesno("HDD Missing", + "HDD image not found. Do you want to create one?"): + if not self.create_hdd_image(): + return + else: + return + + # Open file dialog to select ISO + iso_path = filedialog.askopenfilename( + title="Select ISO file", + filetypes=[("ISO files", "*.iso")], + initialdir=self.iso_dir + ) + + if not iso_path: + messagebox.showinfo("Cancelled", "No ISO file selected.") + return + + # Create autoexec content + autoexec = f""" +# Mount the Windows HDD image as drive C +imgmount c "{hdd_image}" -t hdd -fs fat +# Mount the ISO as drive D +imgmount d "{iso_path}" -t iso +# Mount the local directory as drive E +mount e "{drive_dir}" +boot c: +""" + + # Create temporary config + temp_conf = self.create_temp_config(autoexec) + + # Launch DOSBox-X with the config + try: + subprocess.run(["dosbox-x", "-conf", temp_conf], check=True) + finally: + # Clean up temp config + if temp_conf.exists(): + temp_conf.unlink() + + def boot_iso(self): + """Boot from ISO to install Windows""" + hdd_image = self.get_current_hdd() + + # Create or confirm HDD image exists + if not hdd_image.exists(): + if messagebox.askyesno("HDD Missing", + "HDD image not found. Do you want to create one?"): + if not self.create_hdd_image(): + return + else: + return + + # Open file dialog to select ISO + iso_path = filedialog.askopenfilename( + title="Select Windows Installation ISO", + filetypes=[("ISO files", "*.iso")], + initialdir=self.iso_dir + ) + + if not iso_path: + messagebox.showinfo("Cancelled", "No ISO file selected.") + return + + # Create autoexec content for booting from ISO + autoexec = f""" +# Mount the Windows HDD image as drive C +imgmount c "{hdd_image}" -t hdd -fs fat +# Mount the ISO as drive D +imgmount d "{iso_path}" -t iso +# Start the setup program +d: +setup.exe +""" + + # Create temporary config + temp_conf = self.create_temp_config(autoexec) + + # Launch DOSBox-X with the config + try: + subprocess.run(["dosbox-x", "-conf", temp_conf], check=True) + finally: + # Clean up temp config + if temp_conf.exists(): + temp_conf.unlink() + + def format_disk(self): + """Format hard disk image (creates a new one)""" + hdd_image = self.get_current_hdd() + + if hdd_image.exists(): + if not messagebox.askyesno("Confirm Format", + "This will delete the existing disk image and create a new blank one.\n" + "All data will be lost.\nDo you want to continue?"): + return + + # Remove existing image + hdd_image.unlink() + + # Create new disk image + if self.create_hdd_image(): + messagebox.showinfo("Format Complete", + "Format completed. A new blank disk image has been created.") + + def create_snapshot(self): + """Create a snapshot of the current disk image""" + hdd_image = self.get_current_hdd() + snapshot_dir = self.get_snapshot_dir() + + # Check if HDD image exists + if not hdd_image.exists(): + messagebox.showerror("Error", "HDD image not found. Cannot create snapshot.") + return + + # Get snapshot name + snapshot_name = askstring("Create Snapshot", "Enter a name for this snapshot:") + + if not snapshot_name: + return # User cancelled + + # Create valid filename + snapshot_name = ''.join(c if c.isalnum() or c in '_-' else '_' for c in snapshot_name) + + # Add timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + snapshot_file = snapshot_dir / f"{timestamp}_{snapshot_name}.img" + + # Show progress dialog + progress_window = tk.Toplevel(self.root) + progress_window.title("Creating Snapshot") + progress_window.geometry("400x100") + progress_window.transient(self.root) + progress_window.grab_set() + + ttk.Label(progress_window, text=f"Creating snapshot: {snapshot_name}...").pack(pady=10) + progress = ttk.Progressbar(progress_window, mode="indeterminate", length=300) + progress.pack(pady=10, padx=20) + progress.start() + + def copy_task(): + try: + shutil.copy2(hdd_image, snapshot_file) + progress_window.destroy() + messagebox.showinfo("Snapshot Created", + f"Snapshot '{snapshot_name}' created successfully.\n" + f"Location: {snapshot_file}") + except Exception as e: + progress_window.destroy() + messagebox.showerror("Error", f"Failed to create snapshot: {str(e)}") + + self.root.after(100, copy_task) + + def restore_snapshot(self): + """Restore a snapshot""" + hdd_image = self.get_current_hdd() + snapshot_dir = self.get_snapshot_dir() + + # Check if snapshots exist + snapshots = list(snapshot_dir.glob("*.img")) + if not snapshots: + messagebox.showerror("Error", "No snapshots found.") + return + + # Create snapshot selection dialog + select_dialog = tk.Toplevel(self.root) + select_dialog.title("Select Snapshot") + select_dialog.geometry("500x300") + select_dialog.transient(self.root) + select_dialog.grab_set() + + ttk.Label(select_dialog, text="Select a snapshot to restore:").pack(pady=10) + + # Create a listbox for snapshots + listbox_frame = ttk.Frame(select_dialog) + listbox_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + scrollbar = ttk.Scrollbar(listbox_frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + snapshot_listbox = tk.Listbox(listbox_frame) + snapshot_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + snapshot_listbox.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=snapshot_listbox.yview) + + # Add snapshots to listbox + snapshot_paths = [] + for snap in snapshots: + snapshot_paths.append(snap) + snapshot_listbox.insert(tk.END, snap.name) + + selected_index = [-1] # Use list for closure + + def on_select(): + selected_index[0] = snapshot_listbox.curselection() + if selected_index[0]: + select_dialog.destroy() + + def on_cancel(): + selected_index[0] = -1 + select_dialog.destroy() + + button_frame = ttk.Frame(select_dialog) + button_frame.pack(fill=tk.X, pady=10) + ttk.Button(button_frame, text="Restore", command=on_select).pack(side=tk.LEFT, padx=20) + ttk.Button(button_frame, text="Cancel", command=on_cancel).pack(side=tk.RIGHT, padx=20) + + self.root.wait_window(select_dialog) + + # Check if user selected something + if selected_index[0] == -1 or not selected_index[0]: + return + + selected_snapshot = snapshot_paths[selected_index[0][0]] + + # Confirm restore + if not messagebox.askyesno("Confirm Restore", + "This will replace your current disk image with the selected snapshot.\n" + "All unsaved changes will be lost.\n\nContinue?"): + return + + # Show progress dialog + progress_window = tk.Toplevel(self.root) + progress_window.title("Restoring Snapshot") + progress_window.geometry("400x100") + progress_window.transient(self.root) + progress_window.grab_set() + + ttk.Label(progress_window, text="Restoring snapshot...").pack(pady=10) + progress = ttk.Progressbar(progress_window, mode="indeterminate", length=300) + progress.pack(pady=10, padx=20) + progress.start() + + def restore_task(): + try: + # Create backup of current image + backup_file = None + if hdd_image.exists(): + backup_file = hdd_image.with_suffix('.img.bak') + shutil.copy2(hdd_image, backup_file) + + # Copy snapshot to disk image location + shutil.copy2(selected_snapshot, hdd_image) + + # Remove backup if restoration was successful + if backup_file and backup_file.exists(): + backup_file.unlink() + + progress_window.destroy() + messagebox.showinfo("Snapshot Restored", "Snapshot restored successfully.") + + except Exception as e: + progress_window.destroy() + messagebox.showerror("Error", f"Failed to restore snapshot: {str(e)}") + + # Try to restore backup + if backup_file and backup_file.exists() and not hdd_image.exists(): + shutil.copy2(backup_file, hdd_image) + backup_file.unlink() + + self.root.after(100, restore_task) + + def open_settings(self): + """Open DOSBox-X settings editor""" + if not self.dosbox_conf.exists(): + self._create_default_config() + + # Create settings dialog + settings_dialog = tk.Toplevel(self.root) + settings_dialog.title("DOSBox-X Settings") + settings_dialog.geometry("600x400") + settings_dialog.transient(self.root) + settings_dialog.grab_set() + + # Read current configuration + config = configparser.ConfigParser() + config.read(self.dosbox_conf) + + # Create a notebook for different configuration sections + notebook = ttk.Notebook(settings_dialog) + notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Create frames for important config sections + sections = { + 'System': ['dosbox', 'cpu', 'dos'], + 'Graphics': ['sdl', 'render'], + 'Sound': ['mixer', 'sblaster', 'midi'], + } + + # Track all setting variables + setting_vars = {} + + for section_name, config_sections in sections.items(): + frame = ttk.Frame(notebook) + notebook.add(frame, text=section_name) + + # Add settings for each configuration section + row = 0 + for config_section in config_sections: + if config_section in config: + ttk.Label(frame, text=f"[{config_section}]", font=("", 11, "bold")).grid( + row=row, column=0, columnspan=2, sticky="w", padx=5, pady=(10, 5)) + row += 1 + + # Add each option in the section + for option in config[config_section]: + ttk.Label(frame, text=f"{option}:").grid( + row=row, column=0, sticky="w", padx=5, pady=2) + + # Create variable for this setting + var = tk.StringVar(value=config[config_section][option]) + setting_vars[(config_section, option)] = var + + entry = ttk.Entry(frame, textvariable=var, width=30) + entry.grid(row=row, column=1, sticky="w", padx=5, pady=2) + + row += 1 + + # Create buttons frame + button_frame = ttk.Frame(settings_dialog) + button_frame.pack(fill=tk.X, padx=10, pady=10) + + def save_settings(): + # Update config with values from variables + for (section, option), var in setting_vars.items(): + config[section][option] = var.get() + + # Save configuration + with open(self.dosbox_conf, 'w') as f: + config.write(f) + + settings_dialog.destroy() + messagebox.showinfo("Success", "Settings saved successfully") + + def cancel(): + settings_dialog.destroy() + + ttk.Button(button_frame, text="Save", command=save_settings).pack(side=tk.RIGHT, padx=5) + ttk.Button(button_frame, text="Cancel", command=cancel).pack(side=tk.RIGHT, padx=5) + + # Alternative: open in text editor + def open_in_editor(): + settings_dialog.destroy() + + # Check if system has a GUI text editor + editors = [ + ('xdg-open', [str(self.dosbox_conf)]), # Linux + ('notepad.exe', [str(self.dosbox_conf)]), # Windows + ('open', ['-t', str(self.dosbox_conf)]) # macOS + ] + + for editor, args in editors: + try: + subprocess.run([editor] + args) + break + except (subprocess.SubprocessError, FileNotFoundError): + continue + else: + messagebox.showerror("Error", f"Could not open editor. The config file is at:\n{self.dosbox_conf}") + + ttk.Button(button_frame, text="Open in Text Editor", command=open_in_editor).pack(side=tk.LEFT, padx=5) + +def check_requirements(): + """Check if required programs are installed""" + # Check for DOSBox-X + try: + subprocess.run(['flatpak run com.dosbox_x.DOSBox-X', '-version'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + except FileNotFoundError: + tk.messagebox.showerror("Error", "DOSBox-X is not installed or not in PATH.\n" + "Please install DOSBox-X from https://dosbox-x.com/") + return False + + return True + +def main(): + + root = tk.Tk() + root.title("Windows 9x Manager") + + # Use system theme + try: + ttk.Style().theme_use('clam') # A decent cross-platform theme + except tk.TclError: + pass # If theme not available, use default + + # Create the app + app = Win9xManager(root) + + # Add program icon if available + icon_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "assets", "icon.png") + if os.path.exists(icon_path): + img = tk.PhotoImage(file=icon_path) + root.tk.call('wm', 'iconphoto', root._w, img) + + # Start the main loop + root.mainloop() + +if __name__ == "__main__": + main()