mirror of https://github.com/Enne2/win9xman
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
814 lines
29 KiB
814 lines
29 KiB
#!/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("<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 |
|
|
|
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()
|
|
|