This commit is contained in:
2026-06-15 23:44:02 +08:00
parent 6651c16aaa
commit b3b3b11d0a
2 changed files with 27 additions and 26 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
CLAUDE.md
+25 -26
View File
@@ -61,6 +61,7 @@ class VMConfig:
memory: int = 1024 memory: int = 1024
cpus: int = 2 cpus: int = 2
disk: str = "" disk: str = ""
disk_fmt: str = "qcow2"
cdrom: str = "" cdrom: str = ""
arch: str = "x86_64" arch: str = "x86_64"
network: str = "user" network: str = "user"
@@ -189,7 +190,7 @@ class VMManager:
vm = self.vms.get(name) vm = self.vms.get(name)
if not vm: if not vm:
return "Not found" return "Not found"
if vm.status == "running": if vm.status in ("running", "paused"):
return "Stop VM first" return "Stop VM first"
del self.vms[name] del self.vms[name]
self._save() self._save()
@@ -199,7 +200,7 @@ class VMManager:
vm = self.vms.get(name) vm = self.vms.get(name)
if not vm: if not vm:
return "Not found" return "Not found"
if vm.status == "running": if vm.status in ("running", "paused"):
return "Stop VM first" return "Stop VM first"
vm.config = cfg vm.config = cfg
self._save() self._save()
@@ -221,7 +222,7 @@ class VMManager:
cmd += ["-drive", f"if=pflash,format=raw,readonly=on,file={ovmf}"] cmd += ["-drive", f"if=pflash,format=raw,readonly=on,file={ovmf}"]
if cfg.disk: if cfg.disk:
cmd += ["-drive", f"file={cfg.disk},format=qcow2,if=virtio"] cmd += ["-drive", f"file={cfg.disk},format={cfg.disk_fmt},if=virtio"]
if cfg.cdrom: if cfg.cdrom:
cmd += ["-cdrom", cfg.cdrom] cmd += ["-cdrom", cfg.cdrom]
@@ -285,8 +286,8 @@ class VMManager:
vm = self.vms.get(name) vm = self.vms.get(name)
if not vm: if not vm:
return "Not found" return "Not found"
if vm.status == "running": if vm.status in ("running", "paused"):
return "Already running" return "Already running or paused"
cmd, sock = self.build_cmd(vm.config) cmd, sock = self.build_cmd(vm.config)
try: try:
Path(sock).unlink(missing_ok=True) Path(sock).unlink(missing_ok=True)
@@ -610,7 +611,6 @@ class VMManager:
# strip ANSI escape sequences (e.g. cursor movement, colour codes) # strip ANSI escape sequences (e.g. cursor movement, colour codes)
import re as _re import re as _re
text = _re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', text) text = _re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', text)
text = _re.sub(r'\x1b\[[0-9;]*m', '', text)
text = text.replace("(qemu)", "").strip() text = text.replace("(qemu)", "").strip()
return text or "(ok)" return text or "(ok)"
except Exception as e: except Exception as e:
@@ -627,6 +627,7 @@ class VMManager:
vm = self.vms.get(name) vm = self.vms.get(name)
if vm: if vm:
vm.status = "paused" vm.status = "paused"
self._save_runtime()
return "" return ""
def monitor_resume(self, name: str) -> str: def monitor_resume(self, name: str) -> str:
@@ -636,6 +637,7 @@ class VMManager:
vm = self.vms.get(name) vm = self.vms.get(name)
if vm: if vm:
vm.status = "running" vm.status = "running"
self._save_runtime()
return "" return ""
def monitor_reset(self, name: str) -> str: def monitor_reset(self, name: str) -> str:
@@ -713,18 +715,13 @@ class VMManager:
# probe with qemu-img info # probe with qemu-img info
info = self.disk_info(disk_path) info = self.disk_info(disk_path)
# build config — use detected format in extra_args if not qcow2
fmt = info.get("format", "qcow2") if "error" not in info else "qcow2" fmt = info.get("format", "qcow2") if "error" not in info else "qcow2"
cfg = VMConfig( cfg = VMConfig(
name = vm_name, name = vm_name,
disk = disk_path, disk = disk_path,
extra_args = f"-drive file={disk_path},format={fmt},if=virtio" if fmt != "qcow2" else "", disk_fmt = fmt,
) )
# if format is not qcow2, use raw drive and clear the standard disk field
if fmt != "qcow2":
cfg.disk = ""
cfg.extra_args = f"-drive file={disk_path},format={fmt},if=virtio"
self.vms[vm_name] = VMState(config=cfg) self.vms[vm_name] = VMState(config=cfg)
self._save() self._save()
@@ -1066,6 +1063,7 @@ def vm_form_modal(stdscr, cfg: Optional[VMConfig] = None) -> Optional[VMConfig]:
memory = int(values["memory"] or 1024), memory = int(values["memory"] or 1024),
cpus = int(values["cpus"] or 2), cpus = int(values["cpus"] or 2),
disk = values["disk"].strip(), disk = values["disk"].strip(),
disk_fmt = cfg.disk_fmt,
cdrom = values["cdrom"].strip(), cdrom = values["cdrom"].strip(),
arch = values["arch"], arch = values["arch"],
network = values["network"], network = values["network"],
@@ -1382,7 +1380,7 @@ def disk_mgmt_modal(stdscr, mgr: VMManager, vm_state) -> str:
else: else:
if not cfg.disk: if not cfg.disk:
cfg.disk = path_raw cfg.disk = path_raw
mgr.update(cfg.name, cfg) mgr._save()
msg = f"Created {path_raw} ({gb} GiB)" msg = f"Created {path_raw} ({gb} GiB)"
info = _load_info() info = _load_info()
@@ -1664,7 +1662,8 @@ def portfwd_modal(stdscr, mgr: VMManager, vm_state) -> str:
def _save(): def _save():
cfg.portfwds = [dict(r) for r in rules] cfg.portfwds = [dict(r) for r in rules]
mgr.update(cfg.name, cfg) err = mgr.update(cfg.name, cfg)
return err
while True: while True:
win.erase() win.erase()
@@ -1734,8 +1733,10 @@ def portfwd_modal(stdscr, mgr: VMManager, vm_state) -> str:
if _is_esc(ch): if _is_esc(ch):
_flush_esc(win) _flush_esc(win)
_save() err = _save()
_close_modal(win, stdscr) _close_modal(win, stdscr)
if err:
return f"Error: {err}"
return f"Saved {len(rules)} rule(s)." if rules else "No rules." return f"Saved {len(rules)} rule(s)." if rules else "No rules."
elif ch == curses.KEY_UP: elif ch == curses.KEY_UP:
@@ -2214,7 +2215,7 @@ class TUI:
# tabs: 0=Info 1=Command 2=Console 3=Disk 4=Snapshots 5=Monitor # tabs: 0=Info 1=Command 2=Console 3=Disk 4=Snapshots 5=Monitor
self.tab = 0 self.tab = 0
self.log_off = 0 self.log_off = 0
self.msg = "Ready | n=new Tab=switch tab q=quit" self.msg = "Ready"
self.msg_ok = True self.msg_ok = True
self.last_poll = 0.0 self.last_poll = 0.0
@@ -2254,7 +2255,8 @@ class TUI:
("s","start"), ("k","stop"), ("F","force kill"), ("s","start"), ("k","stop"), ("F","force kill"),
("g","ACPI"), ("z","pause"), ("~","monitor ~"), ("g","ACPI"), ("z","pause"), ("~","monitor ~"),
("d","disk"), ("p","snaps"), ("f","portfwd"), ("d","disk"), ("p","snaps"), ("f","portfwd"),
("x","eject"), ("q","quit"), ("x","eject"),
("Tab","switch tab"), ("q","quit"),
] ]
hy = sh - len(hints) - 2 hy = sh - len(hints) - 2
for key, act in hints: for key, act in hints:
@@ -2379,7 +2381,7 @@ class TUI:
elif base < y0 + h and cfg.network == "user": elif base < y0 + h and cfg.network == "user":
try: try:
scr.addstr(base, x0, f"{'Port Fwds':<16}", curses.color_pair(6)) scr.addstr(base, x0, f"{'Port Fwds':<16}", curses.color_pair(6))
scr.addstr(base, x0 + 16, "(none — press F to add)", curses.color_pair(6)) scr.addstr(base, x0 + 16, "(none — press f to add)", curses.color_pair(6))
except curses.error: except curses.error:
pass pass
@@ -2445,7 +2447,7 @@ class TUI:
try: try:
scr.addstr(row, x0, "No disk configured.", curses.color_pair(6)) scr.addstr(row, x0, "No disk configured.", curses.color_pair(6))
row += 1 row += 1
scr.addstr(row, x0, "Press 'm' to open Disk Management and create one.", scr.addstr(row, x0, "Press 'd' to open Disk Management and create one.",
curses.color_pair(8)) curses.color_pair(8))
except curses.error: except curses.error:
pass pass
@@ -2456,7 +2458,7 @@ class TUI:
if not p.exists(): if not p.exists():
addrow("Status", "FILE NOT FOUND", curses.color_pair(5)) addrow("Status", "FILE NOT FOUND", curses.color_pair(5))
try: try:
scr.addstr(row+1, x0, "Press 'm' > Create to make the disk image.", scr.addstr(row+1, x0, "Press 'd' > Create to make the disk image.",
curses.color_pair(8)) curses.color_pair(8))
except curses.error: except curses.error:
pass pass
@@ -2481,7 +2483,7 @@ class TUI:
try: try:
scr.addstr(row, x0, "-" * min(w-1, 50), curses.color_pair(6)) scr.addstr(row, x0, "-" * min(w-1, 50), curses.color_pair(6))
row += 1 row += 1
scr.addstr(row, x0, "Press 'm' to manage: create / resize / convert / delete", scr.addstr(row, x0, "Press 'd' to manage: create / resize / convert / delete",
curses.color_pair(8)) curses.color_pair(8))
except curses.error: except curses.error:
pass pass
@@ -2615,9 +2617,6 @@ class TUI:
if ch in (ord("q"), ord("Q")): if ch in (ord("q"), ord("Q")):
return False return False
if _is_esc(ch):
return False
elif ch == curses.KEY_DOWN: elif ch == curses.KEY_DOWN:
self.sel = clamp(self.sel + 1, 0, max(0, len(names)-1)) self.sel = clamp(self.sel + 1, 0, max(0, len(names)-1))
self.log_off = 0 self.log_off = 0