Coverage for view / interface.py: 100.00%
32 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-07 00:07 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-07 00:07 +0000
1from abc import ABC, abstractmethod
2from typing import List, Dict, Any, Optional, Callable, TypeVar, Generic
3from dataclasses import dataclass
4from enum import Enum, auto
7T = TypeVar("T")
10class ActionKind(Enum):
11 VALUE = auto()
12 BACK = auto()
13 ERROR = auto()
14 PROCEED = auto()
15 TERMINATE = auto()
18@dataclass
19class ActionResult(Generic[T]):
20 kind: ActionKind
21 payload: Optional[T] = None
22 message: Optional[str] = None
24 @classmethod
25 def value(cls, payload: T) -> "ActionResult[T]":
26 return cls(ActionKind.VALUE, payload=payload)
29 @classmethod
30 def proceed(cls) -> "ActionResult[None]":
31 """Convenience representing a VALUE result that signals the
32 controller run-loop to continue. This is equivalent to
33 `ActionResult.value(True)`.
34 """
35 return cls(ActionKind.PROCEED)
38 @classmethod
39 def terminate(cls) -> "ActionResult[None]":
40 return cls(ActionKind.TERMINATE)
42 @classmethod
43 def back(cls) -> "ActionResult[None]":
44 return cls(ActionKind.BACK)
45 @classmethod
46 def error(cls, message: str) -> "ActionResult[None]":
47 return cls(ActionKind.ERROR, message=message)
50class UIInterface(ABC):
51 """Abstract interface for the UI/View layer to facilitate testing and
52 dependency injection.
53 """
55 @property
56 @abstractmethod
57 def keyboard_reader(self) -> Callable:
58 """Callable that returns KeyboardToken on each call."""
59 pass
61 @abstractmethod
62 def input_center(self, prompt_symbol: str) -> ActionResult[str]:
63 pass
65 @abstractmethod
66 def draw_header(self):
67 pass
69 @abstractmethod
70 def select_files(self, file_data: List[Dict[str, Any]]) -> List[int]:
71 """Display file selector and return indices of selected files.
73 Args:
74 file_data: List of dicts with 'name' and 'size' keys
76 Returns:
77 List of selected file indices
78 """
79 pass
81 @abstractmethod
82 def get_path_input(self) -> ActionResult[str]:
83 pass
85 @abstractmethod
86 def select_output_format(self) -> ActionResult:
87 pass
89 @abstractmethod
90 def select_merge_mode(self) -> ActionResult:
91 pass
93 @abstractmethod
94 def prompt_merged_filename(self) -> ActionResult[str]:
95 pass
97 @abstractmethod
98 def get_progress_bar(self):
99 pass
101 @abstractmethod
102 def ask_again(self) -> ActionResult[bool]:
103 pass
105 @abstractmethod
106 def show_error(self, message: str):
107 pass
109 @abstractmethod
110 def show_conversion_summary(
111 self,
112 total_files: int,
113 output_count: int,
114 merge_mode: str,
115 merged_filename: Optional[str],
116 total_runtime: float,
117 total_input_size_formatted: str,
118 total_output_size_formatted: str,
119 single_output_filename: Optional[str] = None
120 ):
121 """Display comprehensive conversion summary and completion message.
123 Args:
124 total_files: Number of input files processed
125 output_count: Number of output files/pages/chapters created
126 merge_mode: One of "no_merge", "merge", or "per_page"
127 merged_filename: Name of merged output file (if merge_mode == "merge")
128 total_runtime: Total conversion time in seconds
129 total_input_size_formatted: Formatted total size of input files (e.g., "2.5MB")
130 """
131 pass