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

1from abc import ABC, abstractmethod 

2from typing import List, Dict, Any, Optional, Callable, TypeVar, Generic 

3from dataclasses import dataclass 

4from enum import Enum, auto 

5 

6 

7T = TypeVar("T") 

8 

9 

10class ActionKind(Enum): 

11 VALUE = auto() 

12 BACK = auto() 

13 ERROR = auto() 

14 PROCEED = auto() 

15 TERMINATE = auto() 

16 

17 

18@dataclass 

19class ActionResult(Generic[T]): 

20 kind: ActionKind 

21 payload: Optional[T] = None 

22 message: Optional[str] = None 

23 

24 @classmethod 

25 def value(cls, payload: T) -> "ActionResult[T]": 

26 return cls(ActionKind.VALUE, payload=payload) 

27 

28 

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) 

36 

37 

38 @classmethod 

39 def terminate(cls) -> "ActionResult[None]": 

40 return cls(ActionKind.TERMINATE) 

41 

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) 

48 

49 

50class UIInterface(ABC): 

51 """Abstract interface for the UI/View layer to facilitate testing and 

52 dependency injection. 

53 """ 

54 

55 @property 

56 @abstractmethod 

57 def keyboard_reader(self) -> Callable: 

58 """Callable that returns KeyboardToken on each call.""" 

59 pass 

60 

61 @abstractmethod 

62 def input_center(self, prompt_symbol: str) -> ActionResult[str]: 

63 pass 

64 

65 @abstractmethod 

66 def draw_header(self): 

67 pass 

68 

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. 

72  

73 Args: 

74 file_data: List of dicts with 'name' and 'size' keys 

75  

76 Returns: 

77 List of selected file indices 

78 """ 

79 pass 

80 

81 @abstractmethod 

82 def get_path_input(self) -> ActionResult[str]: 

83 pass 

84 

85 @abstractmethod 

86 def select_output_format(self) -> ActionResult: 

87 pass 

88 

89 @abstractmethod 

90 def select_merge_mode(self) -> ActionResult: 

91 pass 

92 

93 @abstractmethod 

94 def prompt_merged_filename(self) -> ActionResult[str]: 

95 pass 

96 

97 @abstractmethod 

98 def get_progress_bar(self): 

99 pass 

100 

101 @abstractmethod 

102 def ask_again(self) -> ActionResult[bool]: 

103 pass 

104 

105 @abstractmethod 

106 def show_error(self, message: str): 

107 pass 

108 

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. 

122  

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