Browse Source

feat: Initialize Windows 9x Manager package and core functionality

- Created the main package structure for Windows 9x Manager.
- Added core functionality for disk image management in `disk.py`.
- Implemented HDD image creation with a progress dialog.
- Developed the main UI manager class in `manager.py` for user interactions.
- Included utility functions for configuration management and system checks.
- Established a template for DOSBox-X configuration.
- Created snapshot management features for saving and restoring system states.
main
Matteo Benedetto 7 months ago
parent
commit
736beed38c
  1. 3
      assets/create_assets_dir.sh
  2. 0
      assets/logo.png
  3. 71
      config/dosbox.conf
  4. 335
      win98_launcher.sh
  5. 789
      win9xman.py
  6. 5
      win9xman/__init__.py
  7. 3
      win9xman/core/__init__.py
  8. 55
      win9xman/core/disk.py
  9. 3
      win9xman/ui/__init__.py
  10. 595
      win9xman/ui/manager.py
  11. 3
      win9xman/utils/__init__.py
  12. 171
      win9xman/utils/config.py
  13. 24
      win9xman/utils/system.py

3
assets/create_assets_dir.sh

@ -1,3 +0,0 @@
#!/bin/bash
# This is just a placeholder to create the assets directory
mkdir -p /home/enne2/Development/win9xman/assets

0
logo.png → assets/logo.png

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

71
config/dosbox.conf

@ -1,71 +0,0 @@
# 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

335
win98_launcher.sh

@ -1,335 +0,0 @@
#!/bin/bash
# Win98 DOSBox-X Launcher
# A GUI tool to launch Windows 98 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"
WIN98_DRIVE="$BASE_DIR/win98_drive"
ISO_DIR="$BASE_DIR/iso"
IMG_DIR="$BASE_DIR/disks"
HDD_IMAGE="$IMG_DIR/win98.img"
SNAPSHOT_DIR="$BASE_DIR/snapshots"
DEFAULT_HDD_SIZE=2000 # Default size in MB for HDD image
MIN_HDD_SIZE=500 # Minimum size in MB
MAX_HDD_SIZE=4000 # Maximum size in MB
# Make sure directories exist
mkdir -p "$WIN98_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 98 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 32 -t hd" -c "exit" > /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.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 98 if installed
start_win98() {
# 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 98 HDD image as drive C
imgmount c "$HDD_IMAGE" -t hdd -fs fatù
# Mount the Windows 98 drive as E
mount e win98_drive
boot c:
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 98 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 e win98_drive
boot 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 98 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 98 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 98 setup
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="Windows98_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
}
# Main menu function
main_menu() {
while true; do
local choice=$(zenity --list --title="Windows 98 DOSBox-X Launcher" \
--text="Select an option:" \
--column="Option" --column="Description" \
1 "Start Windows 98 (if installed)" \
2 "Mount ISO and start Windows 98" \
3 "Boot from Windows 98 ISO (for installation)" \
4 "Format C: drive" \
5 "Create snapshot of current disk image" \
6 "Restore snapshot" \
7 "Exit" \
--width=500 --height=350)
case "$choice" in
1) start_win98 ;;
2) mount_iso ;;
3) boot_iso ;;
4) format_c ;;
5) create_snapshot ;;
6) restore_snapshot ;;
7|"") exit 0 ;;
esac
done
}
# Start the main menu
main_menu

789
win9xman.py

@ -3,791 +3,22 @@
Windows 9x Manager - A GUI tool to launch Windows 95/98 in DOSBox-X Windows 9x Manager - A GUI tool to launch Windows 95/98 in DOSBox-X
""" """
import tkinter as tk
from tkinter import ttk, messagebox
import os import os
import sys 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("<Motion>", 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 # Aggiungi la directory corrente al path per importare i moduli locali
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def on_select(): from win9xman.ui.manager import Win9xManager
selected_index[0] = snapshot_listbox.curselection() from win9xman.utils.system import check_requirements
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(): def main():
"""Main entry point for the application"""
# Check if required dependencies are installed
if not check_requirements():
return
root = tk.Tk() root = tk.Tk()
root.title("Windows 9x Manager") root.title("Windows 9x Manager")

5
win9xman/__init__.py

@ -0,0 +1,5 @@
"""
Windows 9x Manager Python Package
"""
__version__ = "1.0.0"

3
win9xman/core/__init__.py

@ -0,0 +1,3 @@
"""
Core functionality for Windows 9x Manager
"""

55
win9xman/core/disk.py

@ -0,0 +1,55 @@
"""
Disk image management functions
"""
import subprocess
import tkinter as tk
from tkinter import ttk, messagebox
def create_hdd_image(root, hdd_image, hdd_size):
"""Create a new HDD image file
Args:
root: The Tkinter root window
hdd_image: Path to the HDD image file to create
hdd_size: Size of the HDD in MB
Returns:
bool: True if the image was created successfully, False otherwise
"""
# 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(root)
progress_window.title("Creating HDD Image")
progress_window.geometry("400x100")
progress_window.transient(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()
result = [False] # Use list for result
# 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)
result[0] = True
progress_window.destroy()
messagebox.showinfo("Success", f"Disk image of {hdd_size}MB created successfully.")
except subprocess.CalledProcessError:
progress_window.destroy()
messagebox.showerror("Error", "Failed to create HDD image. Please check your permissions.")
root.after(100, create_task)
root.wait_window(progress_window)
return result[0]

3
win9xman/ui/__init__.py

@ -0,0 +1,3 @@
"""
UI Components for Windows 9x Manager
"""

595
win9xman/ui/manager.py

@ -0,0 +1,595 @@
"""
Main UI manager class for Windows 9x Manager
"""
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter.simpledialog import askstring
from datetime import datetime
from pathlib import Path
import configparser
import subprocess
import shutil
from win9xman.core.disk import create_hdd_image
from win9xman.utils.config import create_temp_config, generate_config_from_template, create_default_template
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.dirname(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():
create_default_template(self.templates_dir)
# Create config from template
template_vars = {
'memsize': '64',
'cycles': 'max 80% limit 33000',
'machine': 'svga_s3',
'windowresolution': '1024x768',
'output': 'opengl'
}
generate_config_from_template(self.templates_dir, 'dosbox_template.conf', self.dosbox_conf, template_vars)
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("<Motion>", 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()
# Call the core function to create the HDD image
return create_hdd_image(self.root, hdd_image, hdd_size)
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 = create_temp_config(self.dosbox_conf, autoexec, self.templates_dir, self.base_dir)
# 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 = create_temp_config(self.dosbox_conf, autoexec, self.templates_dir, self.base_dir)
# 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 = create_temp_config(self.dosbox_conf, autoexec, self.templates_dir, self.base_dir)
# 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)

3
win9xman/utils/__init__.py

@ -0,0 +1,3 @@
"""
Utility functions for Windows 9x Manager
"""

171
win9xman/utils/config.py

@ -0,0 +1,171 @@
"""
Configuration utilities for Win9xManager
"""
import configparser
import string
from pathlib import Path
def create_default_template(templates_dir):
"""Create the default DOSBox-X template file"""
template_path = templates_dir / "dosbox_template.conf"
# Make sure templates directory exists
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(templates_dir, template_name, output_path, variables):
"""Generate a configuration file from a template with variable substitution
Args:
templates_dir: Directory containing templates
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 = 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_temp_config(dosbox_conf, autoexec_content, templates_dir, base_dir):
"""Create a temporary DOSBox-X configuration file with custom autoexec section"""
# Make sure the base config exists
if not dosbox_conf.exists():
# Create default config
config_dir = base_dir / "config"
config_dir.mkdir(exist_ok=True)
# Check if template exists
template_path = templates_dir / "dosbox_template.conf"
if not template_path.exists():
create_default_template(templates_dir)
# Create config from template
template_vars = {
'memsize': '64',
'cycles': 'max 80% limit 33000',
'machine': 'svga_s3',
'windowresolution': '1024x768',
'output': 'opengl'
}
generate_config_from_template(templates_dir, 'dosbox_template.conf', dosbox_conf, template_vars)
# Create a temp config based on current settings
temp_conf = base_dir / "temp_dosbox.conf"
# Read current config to preserve settings
current_config = {}
config = configparser.ConfigParser()
config.read(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
generate_config_from_template(templates_dir, '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

24
win9xman/utils/system.py

@ -0,0 +1,24 @@
"""
System-related utility functions for Win9xManager
"""
import subprocess
from tkinter import messagebox
def check_requirements():
"""Check if required programs are installed"""
# Check for DOSBox-X
try:
subprocess.run(['dosbox-x', '-version'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
return True
except FileNotFoundError:
# Try with flatpak
try:
subprocess.run(['flatpak', 'run', 'com.dosbox_x.DOSBox-X', '-version'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
return True
except FileNotFoundError:
messagebox.showerror("Error", "DOSBox-X is not installed or not in PATH.\n"
"Please install DOSBox-X from https://dosbox-x.com/")
return False
Loading…
Cancel
Save