saving before having gemini take a crack at fixing things.
This commit is contained in:
parent
a9ac39357f
commit
aa782f3080
3 changed files with 224 additions and 12 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from textmenu import TextMenu
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
|
@ -92,19 +93,23 @@ ANSI_ESCAPE_RE = re.compile(
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# ===================== UTILITY FUNCTIONS =================
|
# ===================== UTILITY FUNCTIONS =================
|
||||||
# def apply_backspaces(s):
|
def find_pk3_subfolders(base_path):
|
||||||
# # Ensure we are working with str
|
"""
|
||||||
# if isinstance(s, bytes):
|
Returns a list of subfolder names under base_path that contain
|
||||||
# s = s.decode(errors="ignore")
|
at least one .pk3 file.
|
||||||
|
"""
|
||||||
|
base = Path(base_path)
|
||||||
|
matches = []
|
||||||
|
|
||||||
# buf = []
|
if not base.is_dir():
|
||||||
# for c in s:
|
return matches
|
||||||
# if c == "\x08":
|
|
||||||
# if buf:
|
for entry in base.iterdir():
|
||||||
# buf.pop()
|
if entry.is_dir():
|
||||||
# else:
|
if any(f.is_file() and f.suffix.lower() == ".pk3" for f in entry.iterdir()):
|
||||||
# buf.append(c)
|
matches.append(entry.name)
|
||||||
# return "".join(buf)
|
|
||||||
|
return matches
|
||||||
|
|
||||||
def acquire_single_instance(lockfile_base: str):
|
def acquire_single_instance(lockfile_base: str):
|
||||||
"""
|
"""
|
||||||
|
|
@ -583,6 +588,24 @@ def main():
|
||||||
# bufsize=0, # unbuffered
|
# bufsize=0, # unbuffered
|
||||||
# universal_newlines=False)
|
# universal_newlines=False)
|
||||||
|
|
||||||
|
items = ["Pee pee","Poo poo","Stinky caca peepee poopoo pants.","a","b","c","d"]
|
||||||
|
menu = TextMenu(
|
||||||
|
items,
|
||||||
|
width=30,
|
||||||
|
height=5,
|
||||||
|
title="Choose your stink",
|
||||||
|
border_fg="dark gray",
|
||||||
|
border_bg="dark red",
|
||||||
|
inside_fg="dark red",
|
||||||
|
inside_bg="black",
|
||||||
|
selected_fg="black",
|
||||||
|
selected_bg="dark red",
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
choice=menu.show()
|
||||||
|
|
||||||
|
print(f"You selected: {choice}")
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
pty_proc = subprocess.Popen(
|
pty_proc = subprocess.Popen(
|
||||||
[game_exe, "+set", "ttycon", "1"] + sys.argv[1:],
|
[game_exe, "+set", "ttycon", "1"] + sys.argv[1:],
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="RA3MP3Playback.py" />
|
<Compile Include="RA3MP3Playback.py" />
|
||||||
|
<Compile Include="textmenu.py" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
|
||||||
<!-- Uncomment the CoreCompile target to enable the Build command in
|
<!-- Uncomment the CoreCompile target to enable the Build command in
|
||||||
|
|
|
||||||
188
textmenu.py
Normal file
188
textmenu.py
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import urwid
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Detect if running in a real terminal
|
||||||
|
def is_real_terminal():
|
||||||
|
if not sys.stdout.isatty():
|
||||||
|
return False
|
||||||
|
if os.name == 'nt':
|
||||||
|
vs_vars = ["PYCHARM_HOSTED", "VSCODE_PID", "TERM_PROGRAM"]
|
||||||
|
for var in vs_vars:
|
||||||
|
if var in os.environ:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Fallback menu for non-terminal consoles with timeout note in input prompt
|
||||||
|
def fallback_menu(choices, title="Menu", timeout=None):
|
||||||
|
print(f"=== {title} ===")
|
||||||
|
for i, c in enumerate(choices, 1):
|
||||||
|
print(f"{i}. {c}")
|
||||||
|
|
||||||
|
prompt = "Enter choice number"
|
||||||
|
if timeout is not None:
|
||||||
|
prompt += f" (times out in {timeout}s)"
|
||||||
|
prompt += ": "
|
||||||
|
|
||||||
|
if timeout is None:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
sel = int(input(prompt))
|
||||||
|
if 1 <= sel <= len(choices):
|
||||||
|
return choices[sel - 1]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
print("Invalid input. Try again.")
|
||||||
|
else:
|
||||||
|
q = queue.Queue()
|
||||||
|
|
||||||
|
def get_input():
|
||||||
|
try:
|
||||||
|
val = input(prompt)
|
||||||
|
q.put(val)
|
||||||
|
except Exception:
|
||||||
|
q.put(None)
|
||||||
|
|
||||||
|
t = threading.Thread(target=get_input, daemon=True)
|
||||||
|
t.start()
|
||||||
|
start_time = time.time()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
val = q.get_nowait()
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
sel = int(val)
|
||||||
|
if 1 <= sel <= len(choices):
|
||||||
|
return choices[sel - 1]
|
||||||
|
except queue.Empty:
|
||||||
|
if time.time() - start_time > timeout:
|
||||||
|
return None
|
||||||
|
time.sleep(0.05)
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid input. Try again.")
|
||||||
|
t = threading.Thread(target=get_input, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
class TextMenu:
|
||||||
|
def __init__(self, choices, width=40, height=10, title="Menu",
|
||||||
|
border_fg="white", border_bg="dark blue",
|
||||||
|
inside_fg="white", inside_bg="black",
|
||||||
|
selected_fg="black", selected_bg="light gray",
|
||||||
|
timeout=None):
|
||||||
|
self.choices = choices
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.title = title
|
||||||
|
self.selected = None
|
||||||
|
self.timeout = timeout
|
||||||
|
self._start_time = None
|
||||||
|
self._timeout_alarm = None
|
||||||
|
|
||||||
|
self.last_click_time = 0
|
||||||
|
self.double_click_threshold = 0.5 # seconds
|
||||||
|
|
||||||
|
# Buttons with double-click support
|
||||||
|
self.menu_widgets = []
|
||||||
|
for c in choices:
|
||||||
|
btn = urwid.Button(c)
|
||||||
|
urwid.connect_signal(btn, 'click', self._double_click, c)
|
||||||
|
self.menu_widgets.append(urwid.AttrMap(btn, None, focus_map='selected'))
|
||||||
|
|
||||||
|
# Scrollable ListBox
|
||||||
|
self.list_walker = urwid.SimpleFocusListWalker(self.menu_widgets)
|
||||||
|
self.listbox = urwid.ListBox(self.list_walker)
|
||||||
|
self.listbox = urwid.AttrMap(self.listbox, 'inside')
|
||||||
|
|
||||||
|
# Fix height
|
||||||
|
self.box_adapter = urwid.BoxAdapter(self.listbox, self.height)
|
||||||
|
|
||||||
|
# LineBox with border and title
|
||||||
|
self.linebox = urwid.LineBox(self.box_adapter, title=self.title)
|
||||||
|
linebox_colored = urwid.AttrMap(self.linebox, 'border')
|
||||||
|
|
||||||
|
# Fix width and center
|
||||||
|
self.frame = urwid.Filler(
|
||||||
|
urwid.Padding(linebox_colored, align='center', width=self.width),
|
||||||
|
valign='middle'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Color palette
|
||||||
|
self.palette = [
|
||||||
|
('border', border_fg, border_bg),
|
||||||
|
('inside', inside_fg, inside_bg),
|
||||||
|
('selected', selected_fg, selected_bg),
|
||||||
|
]
|
||||||
|
|
||||||
|
# MainLoop
|
||||||
|
self.loop = urwid.MainLoop(
|
||||||
|
self.frame,
|
||||||
|
palette=self.palette,
|
||||||
|
unhandled_input=self._unhandled_input,
|
||||||
|
handle_mouse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cancel timeout and restore title
|
||||||
|
def _cancel_timeout(self):
|
||||||
|
if self._timeout_alarm is not None:
|
||||||
|
self.loop.remove_alarm(self._timeout_alarm)
|
||||||
|
self._timeout_alarm = None
|
||||||
|
self.linebox.set_title(self.title) # restore original title
|
||||||
|
|
||||||
|
# Mouse double-click
|
||||||
|
def _double_click(self, button, choice_text):
|
||||||
|
self._cancel_timeout()
|
||||||
|
now = time.time()
|
||||||
|
if now - self.last_click_time < self.double_click_threshold:
|
||||||
|
self.selected = choice_text
|
||||||
|
raise urwid.ExitMainLoop()
|
||||||
|
self.last_click_time = now
|
||||||
|
|
||||||
|
# Keyboard or mouse input
|
||||||
|
def _unhandled_input(self, key):
|
||||||
|
# Cancel timeout on any input
|
||||||
|
self._cancel_timeout()
|
||||||
|
|
||||||
|
if key == 'enter':
|
||||||
|
focus_widget, _ = self.listbox.get_focus()
|
||||||
|
if focus_widget:
|
||||||
|
self.selected = focus_widget.base_widget.get_label()
|
||||||
|
raise urwid.ExitMainLoop()
|
||||||
|
# Scroll events also count: 'up', 'down', 'page up', 'page down', 'mouse press', 'mouse drag', 'mouse wheel'
|
||||||
|
# urwid passes these to unhandled_input automatically, so we cancel timeout above
|
||||||
|
|
||||||
|
# Timeout exit
|
||||||
|
def _timeout_exit(self, loop, user_data=None):
|
||||||
|
self.selected = None
|
||||||
|
raise urwid.ExitMainLoop()
|
||||||
|
|
||||||
|
# Update countdown in title, precise timing
|
||||||
|
def _update_timer(self, loop, user_data=None):
|
||||||
|
elapsed = time.time() - self._start_time
|
||||||
|
remaining = self.timeout - elapsed
|
||||||
|
|
||||||
|
if remaining <= 0:
|
||||||
|
self.linebox.set_title(self.title)
|
||||||
|
self._timeout_exit(loop)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.linebox.set_title(f"{self.title} ({math.ceil(remaining)}s)")
|
||||||
|
|
||||||
|
# Schedule next update in 0.1s for precise timing
|
||||||
|
self._timeout_alarm = loop.set_alarm_in(0.1, self._update_timer)
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Show menu and return selected item. Returns None if timeout expires."""
|
||||||
|
if not is_real_terminal():
|
||||||
|
return fallback_menu(self.choices, title=self.title, timeout=self.timeout)
|
||||||
|
|
||||||
|
# Start countdown immediately
|
||||||
|
if self.timeout is not None:
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._update_timer(self.loop) # show countdown immediately
|
||||||
|
|
||||||
|
self.loop.run()
|
||||||
|
return self.selected
|
||||||
Loading…
Add table
Add a link
Reference in a new issue