sandeshlauncher 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sandesh Bhandari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: sandeshlauncher
3
+ Version: 1.0.0
4
+ Summary: Minecraft Launcher with auto Java, Fabric support, and profile management
5
+ Author: Sandesh Bhandari
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: customtkinter>=5.2.0
13
+ Requires-Dist: minecraft-launcher-lib>=1.11.0
14
+ Dynamic: author
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: license-file
19
+ Dynamic: requires-dist
20
+ Dynamic: requires-python
21
+ Dynamic: summary
22
+
23
+ A modern Minecraft launcher with offline support, profile management, auto Java installation, and Fabric mod loader support.
@@ -0,0 +1,18 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Sandesh Bhandari
3
+
4
+ """Sandesh Minecraft Launcher - A modern Minecraft launcher"""
5
+
6
+ from .launcher import run, launch_game, load_configs, build_ui
7
+
8
+ __version__ = "1.0.0"
9
+ __author__ = "Sandesh Bhandari"
10
+ __copyright__ = "Copyright (c) 2026 Sandesh Bhandari"
11
+ __license__ = "MIT"
12
+
13
+ def main():
14
+ """Entry point for console script"""
15
+ run()
16
+
17
+ if __name__ == "__main__":
18
+ run()
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sandesh Launcher – Final Edition
4
+ Offline only. Profiles, version isolation, auto Java, auto Fabric.
5
+ Modern UI, no Microsoft login.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import threading
11
+ import subprocess
12
+ import shutil
13
+ import re
14
+ import uuid
15
+ from datetime import datetime
16
+
17
+ import customtkinter as ctk
18
+ from tkinter import messagebox
19
+ import minecraft_launcher_lib
20
+ from minecraft_launcher_lib.utils import get_installed_versions
21
+ from minecraft_launcher_lib import fabric
22
+ import jdk
23
+
24
+ # ==================== CONFIG ====================
25
+ DEFAULT_BASE_DIR = os.path.expanduser("~/sandeshlauncher")
26
+ LAUNCHER_FILES_DIR = os.path.join(DEFAULT_BASE_DIR, "launcher_files")
27
+ VERSIONS_DIR = os.path.join(DEFAULT_BASE_DIR, "versions")
28
+ CONFIG_FILE = os.path.join(DEFAULT_BASE_DIR, "launcher_config.json")
29
+ JAVA_DIR = os.path.join(LAUNCHER_FILES_DIR, "java")
30
+
31
+ os.makedirs(LAUNCHER_FILES_DIR, exist_ok=True)
32
+ os.makedirs(VERSIONS_DIR, exist_ok=True)
33
+ os.makedirs(JAVA_DIR, exist_ok=True)
34
+
35
+ ctk.set_appearance_mode("dark")
36
+ ctk.set_default_color_theme("blue")
37
+
38
+ # ==================== GLOBALS ====================
39
+ config = {}
40
+ profiles = {}
41
+ java_path = None
42
+ app = None
43
+ log_textbox = None
44
+ status_label = None
45
+ profile_combobox = None
46
+
47
+ # ==================== HELPERS ====================
48
+ def update_log(message):
49
+ if log_textbox:
50
+ log_textbox.configure(state="normal")
51
+ timestamp = datetime.now().strftime("%H:%M:%S")
52
+ log_textbox.insert("end", f"[{timestamp}] {message}\n")
53
+ log_textbox.see("end")
54
+ log_textbox.configure(state="disabled")
55
+ app.update_idletasks()
56
+
57
+ def set_status(text, color="#4CAF50"):
58
+ if status_label:
59
+ status_label.configure(text=text, text_color=color)
60
+
61
+ # ---------- Java ----------
62
+ def get_java_executable():
63
+ global java_path
64
+ if java_path and os.path.exists(java_path):
65
+ return java_path
66
+ java_cmd = shutil.which("java")
67
+ if java_cmd:
68
+ try:
69
+ output = subprocess.check_output([java_cmd, "-version"], stderr=subprocess.STDOUT, text=True)
70
+ if "21" in output or "openjdk version \"21" in output:
71
+ update_log(f"Found Java 21 at: {java_cmd}")
72
+ java_path = java_cmd
73
+ return java_path
74
+ except:
75
+ pass
76
+ update_log("Java 21 not found. Installing OpenJDK 21...")
77
+ set_status("Installing Java...", "#FF9800")
78
+ try:
79
+ installed_path = jdk.install('21', path=JAVA_DIR, jre=False)
80
+ java_path = os.path.join(installed_path, "bin", "java")
81
+ if os.name == 'nt':
82
+ java_path += ".exe"
83
+ if os.path.exists(java_path):
84
+ update_log(f"Java 21 installed at: {java_path}")
85
+ return java_path
86
+ raise Exception("Installation path not found")
87
+ except Exception as e:
88
+ update_log(f"Failed to auto-install Java: {e}")
89
+ messagebox.showerror("Java Error", "Install Java 21 manually.")
90
+ return None
91
+
92
+ # ---------- Minecraft / Fabric ----------
93
+ def is_valid_minecraft_version(version):
94
+ return bool(re.match(r'^\d+(\.\d+){1,2}$', version))
95
+
96
+ def get_installed_version_ids(version_dir):
97
+ try:
98
+ return [v["id"] for v in get_installed_versions(version_dir)]
99
+ except:
100
+ return []
101
+
102
+ def is_version_installed(version_id, version_dir):
103
+ return version_id in get_installed_version_ids(version_dir)
104
+
105
+ def get_installed_fabric_version(mc_version, version_dir):
106
+ for vid in get_installed_version_ids(version_dir):
107
+ if vid.startswith("fabric-loader") and mc_version in vid:
108
+ return vid
109
+ return None
110
+
111
+ def delete_corrupted_library(lib_path):
112
+ if os.path.exists(lib_path):
113
+ try:
114
+ os.remove(lib_path)
115
+ update_log(f"Deleted {lib_path}")
116
+ except Exception as e:
117
+ update_log(f"Delete failed: {e}")
118
+
119
+ def repair_libraries(version_dir):
120
+ libs_dir = os.path.join(version_dir, "libraries")
121
+ bad = os.path.join(libs_dir, "org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux.jar")
122
+ if os.path.exists(bad):
123
+ delete_corrupted_library(bad)
124
+ update_log("Removed corrupted lwjgl-jemalloc")
125
+
126
+ def ensure_minecraft_installed(version, version_dir, retry=True):
127
+ if not is_valid_minecraft_version(version):
128
+ return False
129
+ try:
130
+ if is_version_installed(version, version_dir):
131
+ return True
132
+ update_log(f"Installing Vanilla {version}...")
133
+ set_status(f"Installing {version}...", "#FF9800")
134
+ minecraft_launcher_lib.install.install_minecraft_version(version, version_dir)
135
+ update_log(f"✓ Vanilla {version} installed")
136
+ return True
137
+ except Exception as e:
138
+ error_msg = str(e)
139
+ if "wrong Checksum" in error_msg and retry:
140
+ update_log("Checksum error, cleaning...")
141
+ match = re.search(r"'(/[^']+\.jar)'", error_msg)
142
+ if match:
143
+ delete_corrupted_library(match.group(1))
144
+ else:
145
+ repair_libraries(version_dir)
146
+ return ensure_minecraft_installed(version, version_dir, retry=False)
147
+ set_status("✗ Vanilla install failed", "#F44336")
148
+ return False
149
+
150
+ def ensure_fabric_installed(mc_version, version_dir):
151
+ if not ensure_minecraft_installed(mc_version, version_dir):
152
+ return None
153
+ existing = get_installed_fabric_version(mc_version, version_dir)
154
+ if existing:
155
+ update_log(f"✓ Fabric already: {existing}")
156
+ return existing
157
+ update_log(f"Installing Fabric for {mc_version}...")
158
+ set_status("Installing Fabric...", "#FF9800")
159
+ try:
160
+ latest = fabric.get_latest_loader_version()
161
+ fabric_id = fabric.install_fabric(mc_version, version_dir, loader_version=latest)
162
+ if not fabric_id:
163
+ raise Exception("install_fabric returned None")
164
+ if not is_version_installed(fabric_id, version_dir):
165
+ found = get_installed_fabric_version(mc_version, version_dir)
166
+ if found:
167
+ fabric_id = found
168
+ else:
169
+ raise Exception("Fabric not found after install")
170
+ update_log(f"✓ Fabric {fabric_id} installed")
171
+ return fabric_id
172
+ except Exception as e:
173
+ set_status("✗ Fabric install failed", "#F44336")
174
+ update_log(f"Fabric error: {e}")
175
+ return None
176
+
177
+ # ---------- Profile & Config ----------
178
+ def load_configs():
179
+ global config, profiles
180
+ if os.path.exists(CONFIG_FILE):
181
+ try:
182
+ with open(CONFIG_FILE, 'r') as f:
183
+ config = json.load(f)
184
+ except:
185
+ config = get_default_config()
186
+ else:
187
+ config = get_default_config()
188
+ profiles = config.get("profiles", {})
189
+ if not profiles:
190
+ profiles["Default"] = get_default_profile()
191
+ config["profiles"] = profiles
192
+ save_configs()
193
+
194
+ def get_default_config():
195
+ return {"base_dir": DEFAULT_BASE_DIR, "profiles": {}}
196
+
197
+ def get_default_profile():
198
+ return {
199
+ "username": "Player",
200
+ "version": "1.21.1",
201
+ "loader": "vanilla",
202
+ "ram": "2048",
203
+ "width": "854",
204
+ "height": "480",
205
+ "fullscreen": False
206
+ }
207
+
208
+ def save_configs():
209
+ with open(CONFIG_FILE, 'w') as f:
210
+ json.dump(config, f, indent=4)
211
+ update_log("Settings saved")
212
+
213
+ def refresh_profile_list():
214
+ if profile_combobox:
215
+ profile_combobox.configure(values=list(profiles.keys()))
216
+ if profiles:
217
+ profile_combobox.set(list(profiles.keys())[0])
218
+
219
+ # ---------- Profile dialogs ----------
220
+ def add_profile():
221
+ def save_new():
222
+ name = profile_name_entry.get().strip()
223
+ if not name or name in profiles:
224
+ messagebox.showerror("Error", "Invalid or duplicate name")
225
+ return
226
+ profiles[name] = {
227
+ "username": username_entry.get().strip() or "Player",
228
+ "version": version_entry.get().strip() or "1.21.1",
229
+ "loader": loader_var.get(),
230
+ "ram": ram_entry.get().strip() or "2048",
231
+ "width": width_entry.get().strip() or "854",
232
+ "height": height_entry.get().strip() or "480",
233
+ "fullscreen": fullscreen_var.get()
234
+ }
235
+ save_configs()
236
+ refresh_profile_list()
237
+ new_window.destroy()
238
+ update_log(f"Profile '{name}' added")
239
+ new_window = ctk.CTkToplevel(app)
240
+ new_window.title("Add Profile")
241
+ new_window.geometry("450x550")
242
+ new_window.update_idletasks()
243
+ new_window.grab_set()
244
+ main = ctk.CTkFrame(new_window, fg_color="transparent")
245
+ main.pack(fill="both", expand=True, padx=20, pady=20)
246
+ ctk.CTkLabel(main, text="Create Profile", font=("Arial", 18, "bold")).pack(pady=(0,15))
247
+ ctk.CTkLabel(main, text="Profile Name").pack(anchor="w")
248
+ profile_name_entry = ctk.CTkEntry(main, width=350)
249
+ profile_name_entry.pack(pady=(0,10), fill="x")
250
+ ctk.CTkLabel(main, text="Username").pack(anchor="w")
251
+ username_entry = ctk.CTkEntry(main, width=350)
252
+ username_entry.pack(pady=(0,10), fill="x")
253
+ ctk.CTkLabel(main, text="Minecraft Version").pack(anchor="w")
254
+ version_entry = ctk.CTkEntry(main, width=350, placeholder_text="1.21.1")
255
+ version_entry.pack(pady=(0,10), fill="x")
256
+ ctk.CTkLabel(main, text="Mod Loader").pack(anchor="w")
257
+ loader_var = ctk.StringVar(value="vanilla")
258
+ ctk.CTkOptionMenu(main, values=["vanilla", "fabric"], variable=loader_var).pack(pady=(0,10), fill="x")
259
+ ctk.CTkLabel(main, text="RAM (MB)").pack(anchor="w")
260
+ ram_entry = ctk.CTkEntry(main, width=350, placeholder_text="2048")
261
+ ram_entry.pack(pady=(0,10), fill="x")
262
+ size_frame = ctk.CTkFrame(main, fg_color="transparent")
263
+ size_frame.pack(fill="x", pady=(0,10))
264
+ ctk.CTkLabel(size_frame, text="Width:").pack(side="left", padx=(0,5))
265
+ width_entry = ctk.CTkEntry(size_frame, width=80, placeholder_text="854")
266
+ width_entry.pack(side="left", padx=(0,15))
267
+ ctk.CTkLabel(size_frame, text="Height:").pack(side="left", padx=(0,5))
268
+ height_entry = ctk.CTkEntry(size_frame, width=80, placeholder_text="480")
269
+ height_entry.pack(side="left")
270
+ fullscreen_var = ctk.BooleanVar(value=False)
271
+ ctk.CTkCheckBox(main, text="Fullscreen", variable=fullscreen_var).pack(anchor="w", pady=(0,15))
272
+ btn_frame = ctk.CTkFrame(main, fg_color="transparent")
273
+ btn_frame.pack(fill="x")
274
+ ctk.CTkButton(btn_frame, text="Cancel", command=new_window.destroy, fg_color="#555").pack(side="right", padx=5)
275
+ ctk.CTkButton(btn_frame, text="Create", command=save_new, fg_color="#4CAF50").pack(side="right", padx=5)
276
+
277
+ def edit_profile():
278
+ selected = profile_combobox.get()
279
+ if not selected:
280
+ return
281
+ p = profiles[selected]
282
+ def save_edits():
283
+ profiles[selected] = {
284
+ "username": username_entry.get().strip() or "Player",
285
+ "version": version_entry.get().strip() or "1.21.1",
286
+ "loader": loader_var.get(),
287
+ "ram": ram_entry.get().strip() or "2048",
288
+ "width": width_entry.get().strip() or "854",
289
+ "height": height_entry.get().strip() or "480",
290
+ "fullscreen": fullscreen_var.get()
291
+ }
292
+ save_configs()
293
+ edit_window.destroy()
294
+ update_log(f"Profile '{selected}' updated")
295
+ edit_window = ctk.CTkToplevel(app)
296
+ edit_window.title(f"Edit {selected}")
297
+ edit_window.geometry("450x550")
298
+ edit_window.update_idletasks()
299
+ edit_window.grab_set()
300
+ main = ctk.CTkFrame(edit_window, fg_color="transparent")
301
+ main.pack(fill="both", expand=True, padx=20, pady=20)
302
+ ctk.CTkLabel(main, text=f"Editing {selected}", font=("Arial", 18, "bold")).pack(pady=(0,15))
303
+ ctk.CTkLabel(main, text="Username").pack(anchor="w")
304
+ username_entry = ctk.CTkEntry(main, width=350)
305
+ username_entry.insert(0, p.get("username", "Player"))
306
+ username_entry.pack(pady=(0,10), fill="x")
307
+ ctk.CTkLabel(main, text="Minecraft Version").pack(anchor="w")
308
+ version_entry = ctk.CTkEntry(main, width=350)
309
+ version_entry.insert(0, p.get("version", "1.21.1"))
310
+ version_entry.pack(pady=(0,10), fill="x")
311
+ loader_var = ctk.StringVar(value=p.get("loader", "vanilla"))
312
+ ctk.CTkOptionMenu(main, values=["vanilla", "fabric"], variable=loader_var).pack(pady=(0,10), fill="x")
313
+ ctk.CTkLabel(main, text="RAM (MB)").pack(anchor="w")
314
+ ram_entry = ctk.CTkEntry(main, width=350)
315
+ ram_entry.insert(0, p.get("ram", "2048"))
316
+ ram_entry.pack(pady=(0,10), fill="x")
317
+ size_frame = ctk.CTkFrame(main, fg_color="transparent")
318
+ size_frame.pack(fill="x", pady=(0,10))
319
+ ctk.CTkLabel(size_frame, text="Width:").pack(side="left", padx=(0,5))
320
+ width_entry = ctk.CTkEntry(size_frame, width=80)
321
+ width_entry.insert(0, p.get("width", "854"))
322
+ width_entry.pack(side="left", padx=(0,15))
323
+ ctk.CTkLabel(size_frame, text="Height:").pack(side="left", padx=(0,5))
324
+ height_entry = ctk.CTkEntry(size_frame, width=80)
325
+ height_entry.insert(0, p.get("height", "480"))
326
+ height_entry.pack(side="left")
327
+ fullscreen_var = ctk.BooleanVar(value=p.get("fullscreen", False))
328
+ ctk.CTkCheckBox(main, text="Fullscreen", variable=fullscreen_var).pack(anchor="w", pady=(0,15))
329
+ btn_frame = ctk.CTkFrame(main, fg_color="transparent")
330
+ btn_frame.pack(fill="x")
331
+ ctk.CTkButton(btn_frame, text="Cancel", command=edit_window.destroy, fg_color="#555").pack(side="right", padx=5)
332
+ ctk.CTkButton(btn_frame, text="Save", command=save_edits, fg_color="#2196F3").pack(side="right", padx=5)
333
+
334
+ def delete_profile():
335
+ sel = profile_combobox.get()
336
+ if sel and messagebox.askyesno("Confirm", f"Delete '{sel}'?"):
337
+ del profiles[sel]
338
+ save_configs()
339
+ refresh_profile_list()
340
+ update_log(f"Profile '{sel}' deleted")
341
+
342
+ # ---------- Launch ----------
343
+ def launch_game():
344
+ profile_name = profile_combobox.get()
345
+ if not profile_name:
346
+ messagebox.showerror("Error", "No profile")
347
+ return
348
+ profile = profiles.get(profile_name)
349
+ if not profile:
350
+ return
351
+
352
+ username = profile.get("username", "Player")
353
+ mc_version = profile.get("version", "1.21.1")
354
+ loader = profile.get("loader", "vanilla")
355
+ ram = profile.get("ram", "2048")
356
+ width = profile.get("width", "854")
357
+ height = profile.get("height", "480")
358
+ fullscreen = profile.get("fullscreen", False)
359
+
360
+ # Instance folder
361
+ loader_str = "Fabric" if loader == "fabric" else "Vanilla"
362
+ inst_name = f"{profile_name}-{mc_version}-{loader_str}".replace(" ", "_")
363
+ game_dir = os.path.join(VERSIONS_DIR, inst_name)
364
+ os.makedirs(game_dir, exist_ok=True)
365
+ for folder in ["mods", "config", "saves", "resourcepacks", "shaderpacks", "tmp"]:
366
+ os.makedirs(os.path.join(game_dir, folder), exist_ok=True)
367
+
368
+ java_exe = get_java_executable()
369
+ if not java_exe:
370
+ return
371
+
372
+ try:
373
+ ram_mb = int(ram)
374
+ if ram_mb < 512:
375
+ ram_mb = 512
376
+ except:
377
+ ram_mb = 2048
378
+
379
+ # Offline auth
380
+ auth_data = {
381
+ "username": username,
382
+ "uuid": str(uuid.uuid4()),
383
+ "token": "0"
384
+ }
385
+ update_log(f"Offline mode: {username}")
386
+
387
+ # Install required version
388
+ actual_version = mc_version
389
+ if loader == "fabric":
390
+ fabric_ver = ensure_fabric_installed(mc_version, game_dir)
391
+ if not fabric_ver:
392
+ set_status("✗ Fabric setup failed", "#F44336")
393
+ return
394
+ actual_version = fabric_ver
395
+ else:
396
+ if not ensure_minecraft_installed(mc_version, game_dir):
397
+ return
398
+
399
+ # Launch
400
+ try:
401
+ set_status("🚀 Launching...", "#FF9800")
402
+ update_log(f"Launching {actual_version} as {username}")
403
+ jvm_args = [f"-Xmx{ram_mb}M", f"-Xms{max(512, ram_mb // 2)}M",
404
+ f"-Djava.io.tmpdir={os.path.join(game_dir, 'tmp')}"]
405
+ game_args = []
406
+ if width and height:
407
+ game_args.extend(["--width", str(width), "--height", str(height)])
408
+ if fullscreen:
409
+ game_args.append("--fullscreen")
410
+ options = {
411
+ "username": auth_data["username"],
412
+ "uuid": auth_data["uuid"],
413
+ "token": auth_data["token"],
414
+ "jvmArguments": jvm_args,
415
+ "gameArguments": game_args,
416
+ "executable": java_exe
417
+ }
418
+ command = minecraft_launcher_lib.command.get_minecraft_command(actual_version, game_dir, options)
419
+ process = subprocess.Popen(command)
420
+ set_status(f"✓ Game started (PID: {process.pid})", "#4CAF50")
421
+ update_log("✓ Minecraft launched")
422
+ except Exception as e:
423
+ set_status("✗ Launch failed", "#F44336")
424
+ update_log(f"Error: {e}")
425
+
426
+ # ---------- UI ----------
427
+ def build_ui():
428
+ global app, log_textbox, status_label, profile_combobox
429
+ app = ctk.CTk()
430
+ app.title("Sandesh Launcher")
431
+ app.geometry("1000x700")
432
+ app.minsize(800, 600)
433
+
434
+ app.grid_rowconfigure(0, weight=1)
435
+ app.grid_columnconfigure(0, weight=0)
436
+ app.grid_columnconfigure(1, weight=1)
437
+
438
+ # Sidebar
439
+ sidebar = ctk.CTkFrame(app, width=260, corner_radius=0, fg_color="#1e1e1e")
440
+ sidebar.grid(row=0, column=0, sticky="nsew")
441
+ sidebar.grid_propagate(False)
442
+
443
+ ctk.CTkLabel(sidebar, text="⛏️", font=("Arial", 48)).pack(pady=(30,0))
444
+ ctk.CTkLabel(sidebar, text="SANDESH LAUNCHER", font=("Arial", 18, "bold")).pack()
445
+ ctk.CTkFrame(sidebar, height=2, fg_color="#333").pack(fill="x", padx=20, pady=10)
446
+
447
+ # Profiles
448
+ profile_frame = ctk.CTkFrame(sidebar, fg_color="transparent")
449
+ profile_frame.pack(fill="x", padx=20, pady=10)
450
+ ctk.CTkLabel(profile_frame, text="PROFILES", font=("Arial", 12, "bold"), text_color="#aaa").pack(anchor="w")
451
+ profile_combobox = ctk.CTkComboBox(profile_frame, values=list(profiles.keys()), state="readonly", height=35)
452
+ profile_combobox.pack(fill="x", pady=(5,10))
453
+ if profiles:
454
+ profile_combobox.set(list(profiles.keys())[0])
455
+
456
+ btn_frame = ctk.CTkFrame(profile_frame, fg_color="transparent")
457
+ btn_frame.pack(fill="x")
458
+ ctk.CTkButton(btn_frame, text="➕ Add", command=add_profile, fg_color="#4CAF50", height=32).pack(side="left", padx=(0,5), fill="x", expand=True)
459
+ ctk.CTkButton(btn_frame, text="✏️ Edit", command=edit_profile, fg_color="#2196F3", height=32).pack(side="left", padx=5, fill="x", expand=True)
460
+ ctk.CTkButton(btn_frame, text="🗑️ Delete", command=delete_profile, fg_color="#F44336", height=32).pack(side="left", padx=(5,0), fill="x", expand=True)
461
+
462
+ ctk.CTkFrame(sidebar, height=2, fg_color="#333").pack(fill="x", padx=20, pady=20)
463
+
464
+ info_text = """📁 Each profile's game data is isolated.
465
+
466
+ ~/sandeshlauncher/versions/ProfileName-X.X.X-Vanilla/
467
+ ├── mods/
468
+ ├── saves/
469
+ ├── config/
470
+ └── ...
471
+
472
+ ⚙️ Java auto-installed.
473
+ 🔧 Fabric auto-installed."""
474
+ ctk.CTkLabel(sidebar, text=info_text, font=("Arial", 11), text_color="#888", justify="left", wraplength=220).pack(padx=20, pady=10)
475
+
476
+ # Main content
477
+ main = ctk.CTkFrame(app, fg_color="transparent")
478
+ main.grid(row=0, column=1, sticky="nsew", padx=15, pady=15)
479
+ main.grid_rowconfigure(1, weight=1)
480
+ main.grid_columnconfigure(0, weight=1)
481
+
482
+ status_card = ctk.CTkFrame(main, fg_color="#1e1e1e", corner_radius=12)
483
+ status_card.grid(row=0, column=0, sticky="ew", pady=(0,15))
484
+ status_label = ctk.CTkLabel(status_card, text="✓ Ready", font=("Arial", 14, "bold"), text_color="#4CAF50")
485
+ status_label.pack(pady=15)
486
+
487
+ log_frame = ctk.CTkFrame(main, fg_color="#1e1e1e", corner_radius=12)
488
+ log_frame.grid(row=1, column=0, sticky="nsew")
489
+ log_frame.grid_rowconfigure(1, weight=1)
490
+ log_frame.grid_columnconfigure(0, weight=1)
491
+ ctk.CTkLabel(log_frame, text="CONSOLE", font=("Arial", 12, "bold"), text_color="#aaa").pack(anchor="w", padx=15, pady=(10,0))
492
+ log_textbox = ctk.CTkTextbox(log_frame, font=("Consolas", 11), fg_color="#0d0d0d", corner_radius=8)
493
+ log_textbox.pack(fill="both", expand=True, padx=15, pady=(5,15))
494
+
495
+ action_frame = ctk.CTkFrame(main, fg_color="transparent")
496
+ action_frame.grid(row=2, column=0, sticky="ew", pady=(15,0))
497
+ action_frame.grid_columnconfigure((0,1,2,3), weight=1)
498
+
499
+ ctk.CTkButton(action_frame, text="▶ PLAY NOW", command=lambda: threading.Thread(target=launch_game, daemon=True).start(),
500
+ fg_color="#2196F3", font=("Arial", 16, "bold"), height=50, corner_radius=12).grid(row=0, column=0, columnspan=4, sticky="ew", padx=5, pady=5)
501
+
502
+ def open_mods():
503
+ sel = profile_combobox.get()
504
+ if sel and sel in profiles:
505
+ p = profiles[sel]
506
+ loader_str = "Fabric" if p.get("loader") == "fabric" else "Vanilla"
507
+ inst = f"{sel}-{p.get('version', '1.21.1')}-{loader_str}".replace(" ", "_")
508
+ mods_path = os.path.join(VERSIONS_DIR, inst, "mods")
509
+ else:
510
+ mods_path = os.path.join(VERSIONS_DIR, "default", "mods")
511
+ os.makedirs(mods_path, exist_ok=True)
512
+ if os.name == 'nt':
513
+ os.startfile(mods_path)
514
+ else:
515
+ subprocess.Popen(['xdg-open', mods_path])
516
+
517
+ ctk.CTkButton(action_frame, text="📁 Mods", command=open_mods, fg_color="#4CAF50", height=40).grid(row=1, column=0, sticky="ew", padx=5, pady=5)
518
+ ctk.CTkButton(action_frame, text="📂 Files", command=lambda: subprocess.Popen(['xdg-open', VERSIONS_DIR] if os.name != 'nt' else ['start', VERSIONS_DIR], shell=True),
519
+ fg_color="#FF9800", height=40).grid(row=1, column=1, sticky="ew", padx=5, pady=5)
520
+ ctk.CTkButton(action_frame, text="🔧 Repair", command=lambda: repair_libraries(VERSIONS_DIR), fg_color="#F44336", height=40).grid(row=1, column=2, sticky="ew", padx=5, pady=5)
521
+ ctk.CTkButton(action_frame, text="🗑️ Clear", command=lambda: log_textbox.delete("1.0", "end"), fg_color="#555", height=40).grid(row=1, column=3, sticky="ew", padx=5, pady=5)
522
+
523
+ app.after(500, lambda: update_log(f"✓ Ready – {len(profiles)} profile(s)"))
524
+ app.mainloop()
525
+
526
+ if __name__ == "__main__":
527
+ load_configs()
528
+ build_ui()
529
+
530
+ def run():
531
+ """Main entry point for the launcher"""
532
+ load_configs()
533
+ build_ui()
534
+
535
+ # Keep the existing main block
536
+ if __name__ == "__main__":
537
+ load_configs()
538
+ build_ui()
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: sandeshlauncher
3
+ Version: 1.0.0
4
+ Summary: Minecraft Launcher with auto Java, Fabric support, and profile management
5
+ Author: Sandesh Bhandari
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: customtkinter>=5.2.0
13
+ Requires-Dist: minecraft-launcher-lib>=1.11.0
14
+ Dynamic: author
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: license-file
19
+ Dynamic: requires-dist
20
+ Dynamic: requires-python
21
+ Dynamic: summary
22
+
23
+ A modern Minecraft launcher with offline support, profile management, auto Java installation, and Fabric mod loader support.
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ setup.py
3
+ sandeshlauncher/__init__.py
4
+ sandeshlauncher/launcher.py
5
+ sandeshlauncher.egg-info/PKG-INFO
6
+ sandeshlauncher.egg-info/SOURCES.txt
7
+ sandeshlauncher.egg-info/dependency_links.txt
8
+ sandeshlauncher.egg-info/entry_points.txt
9
+ sandeshlauncher.egg-info/requires.txt
10
+ sandeshlauncher.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sandeshlauncher = sandeshlauncher.launcher:run
@@ -0,0 +1,2 @@
1
+ customtkinter>=5.2.0
2
+ minecraft-launcher-lib>=1.11.0
@@ -0,0 +1 @@
1
+ sandeshlauncher
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="sandeshlauncher",
5
+ version="1.0.0",
6
+ author="Sandesh Bhandari",
7
+ description="Minecraft Launcher with auto Java, Fabric support, and profile management",
8
+ long_description="A modern Minecraft launcher with offline support, profile management, auto Java installation, and Fabric mod loader support.",
9
+ long_description_content_type="text/markdown",
10
+ packages=find_packages(),
11
+ install_requires=[
12
+ "customtkinter>=5.2.0",
13
+ "minecraft-launcher-lib>=1.11.0",
14
+ ],
15
+ python_requires=">=3.7",
16
+ classifiers=[
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ ],
21
+ entry_points={
22
+ "console_scripts": [
23
+ "sandeshlauncher=sandeshlauncher.launcher:run",
24
+ ],
25
+ },
26
+ )