Update selector.py

This commit is contained in:
CodeName393
2026-02-26 15:16:34 +09:00
committed by GitHub
parent 65e6ae88d0
commit 00b4f2cdd1

View File

@@ -8,7 +8,8 @@ from rich.text import Text
from unshackle.core.console import console from unshackle.core.console import console
IS_WINDOWS = sys.platform == "win32" IS_WINDOWS = sys.platform == "win32"
if IS_WINDOWS: import msvcrt if IS_WINDOWS:
import msvcrt
class Selector: class Selector:
""" """
@@ -44,7 +45,7 @@ class Selector:
self.page_size = page_size self.page_size = page_size
self.minimal_count = minimal_count self.minimal_count = minimal_count
self.dependencies = dependencies or {} self.dependencies = dependencies or {}
# Parent-Child mapping for quick lookup # Parent-Child mapping for quick lookup
self.child_to_parent = {} self.child_to_parent = {}
for parent, children in self.dependencies.items(): for parent, children in self.dependencies.items():
@@ -83,7 +84,7 @@ class Selector:
Constructs and returns the renderable object (Table + Info) for the current state. Constructs and returns the renderable object (Table + Info) for the current state.
""" """
visible_indices = self.get_visible_indices() visible_indices = self.get_visible_indices()
# Adjust scroll offset to ensure cursor is visible # Adjust scroll offset to ensure cursor is visible
if self.cursor_index not in visible_indices: if self.cursor_index not in visible_indices:
# Fallback if cursor got hidden (should be handled in move, but safety check) # Fallback if cursor got hidden (should be handled in move, but safety check)
@@ -98,13 +99,13 @@ class Selector:
# Calculate logical page start/end based on VISIBLE items # Calculate logical page start/end based on VISIBLE items
start_idx = self.scroll_offset start_idx = self.scroll_offset
end_idx = start_idx + self.page_size end_idx = start_idx + self.page_size
# Dynamic scroll adjustment # Dynamic scroll adjustment
if cursor_visual_pos < start_idx: if cursor_visual_pos < start_idx:
self.scroll_offset = cursor_visual_pos self.scroll_offset = cursor_visual_pos
elif cursor_visual_pos >= end_idx: elif cursor_visual_pos >= end_idx:
self.scroll_offset = cursor_visual_pos - self.page_size + 1 self.scroll_offset = cursor_visual_pos - self.page_size + 1
# Re-calc render range # Re-calc render range
render_indices = visible_indices[self.scroll_offset : self.scroll_offset + self.page_size] render_indices = visible_indices[self.scroll_offset : self.scroll_offset + self.page_size]
@@ -133,7 +134,8 @@ class Selector:
total_visible = len(visible_indices) total_visible = len(visible_indices)
total_pages = (total_visible + self.page_size - 1) // self.page_size total_pages = (total_visible + self.page_size - 1) // self.page_size
if total_pages == 0: total_pages = 1 if total_pages == 0:
total_pages = 1
current_page = (self.scroll_offset // self.page_size) + 1 current_page = (self.scroll_offset // self.page_size) + 1
if self.dependencies: if self.dependencies:
@@ -174,16 +176,16 @@ class Selector:
return return
total_visible = len(visible_indices) total_visible = len(visible_indices)
# Calculate current logical page # Calculate current logical page
current_page = self.scroll_offset // self.page_size current_page = self.scroll_offset // self.page_size
total_pages = (total_visible + self.page_size - 1) // self.page_size total_pages = (total_visible + self.page_size - 1) // self.page_size
new_page = current_page + delta new_page = current_page + delta
if 0 <= new_page < total_pages: if 0 <= new_page < total_pages:
self.scroll_offset = new_page * self.page_size self.scroll_offset = new_page * self.page_size
# Move cursor to top of new page # Move cursor to top of new page
try: try:
# Calculate what visual index corresponds to the start of the new page # Calculate what visual index corresponds to the start of the new page
@@ -210,24 +212,16 @@ class Selector:
self.selected_indices.update(target_indices) self.selected_indices.update(target_indices)
else: else:
self.selected_indices.difference_update(target_indices) self.selected_indices.difference_update(target_indices)
def toggle_expand(self, expand: bool = None): def toggle_expand(self):
""" """
Expands or collapses the current header. Expands or collapses the current header.
Args:
expand: True to expand, False to collapse, None to toggle.
""" """
if self.cursor_index in self.dependencies: if self.cursor_index in self.dependencies:
if expand is None: if self.cursor_index in self.expanded_headers:
if self.cursor_index in self.expanded_headers: self.expanded_headers.remove(self.cursor_index)
self.expanded_headers.remove(self.cursor_index)
else:
self.expanded_headers.add(self.cursor_index)
elif expand:
self.expanded_headers.add(self.cursor_index)
else: else:
if self.cursor_index in self.expanded_headers: self.expanded_headers.add(self.cursor_index)
self.expanded_headers.remove(self.cursor_index)
def toggle_expand_all(self): def toggle_expand_all(self):
""" """
@@ -265,10 +259,14 @@ class Selector:
if key == b"\xe0" or key == b"\x00": if key == b"\xe0" or key == b"\x00":
try: try:
key = msvcrt.getch() key = msvcrt.getch()
if key == b"H": return "UP" # Arrow Up if key == b"H": # Arrow Up
if key == b"P": return "DOWN" # Arrow Down return "UP"
if key == b"K": return "LEFT" # Arrow Left if key == b"P": # Arrow Down
if key == b"M": return "RIGHT" # Arrow Right return "DOWN"
if key == b"K": # Arrow Left
return "LEFT"
if key == b"M": # Arrow Right
return "RIGHT"
except Exception: except Exception:
pass pass
@@ -277,16 +275,26 @@ class Selector:
except Exception: except Exception:
return None return None
if char in ("\r", "\n"): return "ENTER" if char in ("\r", "\n"):
if char == " ": return "SPACE" return "ENTER"
if char in ("q", "Q"): return "QUIT" if char == " ":
if char in ("a", "A"): return "ALL" return "SPACE"
if char == "e": return "EXPAND" if char in ("q", "Q"):
if char == "E": return "EXPAND_ALL" return "QUIT"
if char in ("w", "W", "k", "K"): return "UP" if char in ("a", "A"):
if char in ("s", "S", "j", "J"): return "DOWN" return "ALL"
if char in ("h", "H"): return "LEFT" if char == "e":
if char in ("d", "D", "l", "L"): return "RIGHT" return "EXPAND"
if char == "E":
return "EXPAND_ALL"
if char in ("w", "W", "k", "K"):
return "UP"
if char in ("s", "S", "j", "J"):
return "DOWN"
if char in ("h", "H"):
return "LEFT"
if char in ("d", "D", "l", "L"):
return "RIGHT"
return None return None
def get_input_unix(self): def get_input_unix(self):
@@ -297,7 +305,7 @@ class Selector:
char = click.getchar() char = click.getchar()
# Ctrl+C # Ctrl+C
if char == "\x03": return "CANCEL" if char == "\x03": return "CANCEL"
# ANSI Escape Sequences for Arrow Keys # ANSI Escape Sequences for Arrow Keys
mapping = { mapping = {
"\x1b[A": "UP", # Escape + [ + A "\x1b[A": "UP", # Escape + [ + A
@@ -306,31 +314,45 @@ class Selector:
"\x1b[D": "LEFT", # Escape + [ + D "\x1b[D": "LEFT", # Escape + [ + D
} }
if char in mapping: return mapping[char] if char in mapping: return mapping[char]
# Handling manual Escape sequences # Handling manual Escape sequences
if char == "\x1b": # ESC if char == "\x1b": # ESC
try: try:
next1 = click.getchar() next1 = click.getchar()
if next1 in ("[", "O"): # Sequence indicators if next1 in ("[", "O"): # Sequence indicators
next2 = click.getchar() next2 = click.getchar()
if next2 == "A": return "UP" # Arrow Up if next2 == "A": # Arrow Up
if next2 == "B": return "DOWN" # Arrow Down return "UP"
if next2 == "C": return "RIGHT" # Arrow Right if next2 == "B": # Arrow Down
if next2 == "D": return "LEFT" # Arrow Left return "DOWN"
if next2 == "C": # Arrow Right
return "RIGHT"
if next2 == "D": # Arrow Left
return "LEFT"
return "CANCEL" return "CANCEL"
except Exception: except Exception:
return "CANCEL" return "CANCEL"
if char in ("\r", "\n"): return "ENTER" if char in ("\r", "\n"):
if char == " ": return "SPACE" return "ENTER"
if char in ("q", "Q"): return "QUIT" if char == " ":
if char in ("a", "A"): return "ALL" return "SPACE"
if char == "e": return "EXPAND" if char in ("q", "Q"):
if char == "E": return "EXPAND_ALL" return "QUIT"
if char in ("w", "W", "k", "K"): return "UP" if char in ("a", "A"):
if char in ("s", "S", "j", "J"): return "DOWN" return "ALL"
if char in ("h", "H"): return "LEFT" if char == "e":
if char in ("d", "D", "l", "L"): return "RIGHT" return "EXPAND"
if char == "E":
return "EXPAND_ALL"
if char in ("w", "W", "k", "K"):
return "UP"
if char in ("s", "S", "j", "J"):
return "DOWN"
if char in ("h", "H"):
return "LEFT"
if char in ("d", "D", "l", "L"):
return "RIGHT"
return None return None
def run(self) -> list[int]: def run(self) -> list[int]:
@@ -359,7 +381,7 @@ class Selector:
elif action == "RIGHT": elif action == "RIGHT":
self.change_page(1) self.change_page(1)
elif action == "EXPAND": elif action == "EXPAND":
self.toggle_expand(expand=None) self.toggle_expand()
elif action == "EXPAND_ALL": elif action == "EXPAND_ALL":
self.toggle_expand_all() self.toggle_expand_all()
elif action == "SPACE": elif action == "SPACE":
@@ -374,7 +396,6 @@ class Selector:
except KeyboardInterrupt: except KeyboardInterrupt:
return [] return []
def select_multiple( def select_multiple(
options: list[str], options: list[str],
minimal_count: int = 1, minimal_count: int = 1,