aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rwxr-xr-xqemu-tui.py51
2 files changed, 27 insertions, 26 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..720d52e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+__pycache__/
+CLAUDE.md
diff --git a/qemu-tui.py b/qemu-tui.py
index 3cd5d4e..c929938 100755
--- a/qemu-tui.py
+++ b/qemu-tui.py
@@ -61,6 +61,7 @@ class VMConfig:
memory: int = 1024
cpus: int = 2
disk: str = ""
+ disk_fmt: str = "qcow2"
cdrom: str = ""
arch: str = "x86_64"
network: str = "user"
@@ -189,7 +190,7 @@ class VMManager:
vm = self.vms.get(name)
if not vm:
return "Not found"
- if vm.status == "running":
+ if vm.status in ("running", "paused"):
return "Stop VM first"
del self.vms[name]
self._save()
@@ -199,7 +200,7 @@ class VMManager:
vm = self.vms.get(name)
if not vm:
return "Not found"
- if vm.status == "running":
+ if vm.status in ("running", "paused"):
return "Stop VM first"
vm.config = cfg
self._save()
@@ -221,7 +222,7 @@ class VMManager:
cmd += ["-drive", f"if=pflash,format=raw,readonly=on,file={ovmf}"]
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:
cmd += ["-cdrom", cfg.cdrom]
@@ -285,8 +286,8 @@ class VMManager:
vm = self.vms.get(name)
if not vm:
return "Not found"
- if vm.status == "running":
- return "Already running"
+ if vm.status in ("running", "paused"):
+ return "Already running or paused"
cmd, sock = self.build_cmd(vm.config)
try:
Path(sock).unlink(missing_ok=True)
@@ -610,7 +611,6 @@ class VMManager:
# strip ANSI escape sequences (e.g. cursor movement, colour codes)
import re as _re
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()
return text or "(ok)"
except Exception as e:
@@ -627,6 +627,7 @@ class VMManager:
vm = self.vms.get(name)
if vm:
vm.status = "paused"
+ self._save_runtime()
return ""
def monitor_resume(self, name: str) -> str:
@@ -636,6 +637,7 @@ class VMManager:
vm = self.vms.get(name)
if vm:
vm.status = "running"
+ self._save_runtime()
return ""
def monitor_reset(self, name: str) -> str:
@@ -713,18 +715,13 @@ class VMManager:
# probe with qemu-img info
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"
cfg = VMConfig(
- name = vm_name,
- disk = disk_path,
- extra_args = f"-drive file={disk_path},format={fmt},if=virtio" if fmt != "qcow2" else "",
+ name = vm_name,
+ disk = disk_path,
+ 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._save()
@@ -1066,6 +1063,7 @@ def vm_form_modal(stdscr, cfg: Optional[VMConfig] = None) -> Optional[VMConfig]:
memory = int(values["memory"] or 1024),
cpus = int(values["cpus"] or 2),
disk = values["disk"].strip(),
+ disk_fmt = cfg.disk_fmt,
cdrom = values["cdrom"].strip(),
arch = values["arch"],
network = values["network"],
@@ -1382,7 +1380,7 @@ def disk_mgmt_modal(stdscr, mgr: VMManager, vm_state) -> str:
else:
if not cfg.disk:
cfg.disk = path_raw
- mgr.update(cfg.name, cfg)
+ mgr._save()
msg = f"Created {path_raw} ({gb} GiB)"
info = _load_info()
@@ -1664,7 +1662,8 @@ def portfwd_modal(stdscr, mgr: VMManager, vm_state) -> str:
def _save():
cfg.portfwds = [dict(r) for r in rules]
- mgr.update(cfg.name, cfg)
+ err = mgr.update(cfg.name, cfg)
+ return err
while True:
win.erase()
@@ -1734,8 +1733,10 @@ def portfwd_modal(stdscr, mgr: VMManager, vm_state) -> str:
if _is_esc(ch):
_flush_esc(win)
- _save()
+ err = _save()
_close_modal(win, stdscr)
+ if err:
+ return f"Error: {err}"
return f"Saved {len(rules)} rule(s)." if rules else "No rules."
elif ch == curses.KEY_UP:
@@ -2214,7 +2215,7 @@ class TUI:
# tabs: 0=Info 1=Command 2=Console 3=Disk 4=Snapshots 5=Monitor
self.tab = 0
self.log_off = 0
- self.msg = "Ready | n=new Tab=switch tab q=quit"
+ self.msg = "Ready"
self.msg_ok = True
self.last_poll = 0.0
@@ -2254,7 +2255,8 @@ class TUI:
("s","start"), ("k","stop"), ("F","force kill"),
("g","ACPI"), ("z","pause"), ("~","monitor ~"),
("d","disk"), ("p","snaps"), ("f","portfwd"),
- ("x","eject"), ("q","quit"),
+ ("x","eject"),
+ ("Tab","switch tab"), ("q","quit"),
]
hy = sh - len(hints) - 2
for key, act in hints:
@@ -2379,7 +2381,7 @@ class TUI:
elif base < y0 + h and cfg.network == "user":
try:
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:
pass
@@ -2445,7 +2447,7 @@ class TUI:
try:
scr.addstr(row, x0, "No disk configured.", curses.color_pair(6))
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))
except curses.error:
pass
@@ -2456,7 +2458,7 @@ class TUI:
if not p.exists():
addrow("Status", "FILE NOT FOUND", curses.color_pair(5))
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))
except curses.error:
pass
@@ -2481,7 +2483,7 @@ class TUI:
try:
scr.addstr(row, x0, "-" * min(w-1, 50), curses.color_pair(6))
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))
except curses.error:
pass
@@ -2615,9 +2617,6 @@ class TUI:
if ch in (ord("q"), ord("Q")):
return False
- if _is_esc(ch):
- return False
-
elif ch == curses.KEY_DOWN:
self.sel = clamp(self.sel + 1, 0, max(0, len(names)-1))
self.log_off = 0