Coverage for nexusLIMS/tui/common/base_app.py: 100%

74 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2026-03-24 05:23 +0000

1""" 

2Base application class for NexusLIMS TUI applications. 

3 

4Provides common functionality including theme switching, database session 

5management, help screens, and error notifications. 

6""" 

7 

8import logging 

9from pathlib import Path 

10from typing import ClassVar 

11 

12from sqlmodel import Session 

13from textual.app import App 

14from textual.screen import Screen 

15from textual.widgets import Footer, Header, Markdown 

16 

17from nexusLIMS.db.engine import get_engine 

18 

19_logger = logging.getLogger(__name__) 

20 

21 

22class HelpScreen(Screen): 

23 """Help screen showing keybindings and usage information.""" 

24 

25 BINDINGS: ClassVar = [ 

26 ("escape", "dismiss", "Close Help"), 

27 ("q", "dismiss", "Close Help"), 

28 ] 

29 

30 def __init__( 

31 self, 

32 app_name: str, 

33 keybindings: list[tuple[str, str]], 

34 description: str = "", 

35 **kwargs, 

36 ): 

37 """ 

38 Initialize help screen. 

39 

40 Parameters 

41 ---------- 

42 app_name : str 

43 Name of the application 

44 keybindings : list[tuple[str, str]] 

45 List of (key, description) tuples 

46 description : str 

47 Optional prose blurb shown between the title and keybindings. 

48 **kwargs 

49 Additional arguments passed to Screen 

50 """ 

51 super().__init__(**kwargs) 

52 self.app_name = app_name 

53 self.keybindings = keybindings 

54 self.description = description 

55 

56 def compose(self): 

57 """Compose help screen layout.""" 

58 yield Header() 

59 

60 help_text = f"# {self.app_name}\n\n" 

61 if self.description: 

62 help_text += f"{self.description}\n\n" 

63 help_text += "## Keybindings\n\n" 

64 

65 for key, description in self.keybindings: 

66 help_text += f"- **{key}**: {description}\n" 

67 

68 help_text += "\n## Theme Switching\n\n" 

69 help_text += "- **Ctrl+T**: Toggle between dark and light themes\n" 

70 help_text += "\n## Navigation\n\n" 

71 help_text += "- **Arrow keys**: Navigate menus and forms\n" 

72 help_text += "- **Tab**: Move between form fields\n" 

73 help_text += "- **Enter**: Select/Submit\n" 

74 help_text += "- **Escape**: Cancel/Go back\n" 

75 

76 yield Markdown(help_text, id="help-content") 

77 yield Footer() 

78 

79 def action_dismiss(self): 

80 """Dismiss the help screen.""" 

81 self.app.pop_screen() 

82 

83 

84class BaseNexusApp(App): 

85 """ 

86 Base application class for NexusLIMS TUI apps. 

87 

88 Provides: 

89 - Theme switching (dark/light) using Textual's built-in themes 

90 - Database session management 

91 - Help screen 

92 - Error notifications 

93 - Common keybindings 

94 

95 Subclasses should: 

96 - Define their own screens 

97 - Override get_app_name() to return app name 

98 - Override get_keybindings() to add app-specific bindings 

99 """ 

100 

101 CSS_PATH: ClassVar = [Path(__file__).parent.parent / "styles" / "base_app.tcss"] 

102 

103 BINDINGS: ClassVar = [ 

104 ("ctrl+t", "toggle_theme", "Toggle Theme"), 

105 ("ctrl+q", "quit", "Quit"), 

106 ("?", "help", "Help"), 

107 ] 

108 

109 def __init__(self, **kwargs): 

110 """Initialize the base application with dark mode by default.""" 

111 # Will be set in on_mount 

112 self.db_session: Session | None = None 

113 

114 super().__init__(**kwargs) 

115 

116 def on_mount(self) -> None: 

117 """Set up database connection.""" 

118 # Create database session (if not already set by subclass) 

119 if self.db_session is None: 

120 try: 

121 self.db_session = Session(get_engine()) 

122 _logger.debug("Database session created") 

123 except Exception as e: 

124 _logger.exception("Failed to create database session") 

125 self.show_error(f"Database connection failed: {e}") 

126 

127 def on_unmount(self) -> None: 

128 """Clean up database connection.""" 

129 if self.db_session: 

130 self.db_session.close() 

131 _logger.debug("Database session closed") 

132 

133 def action_toggle_theme(self) -> None: 

134 """Toggle between dark and light themes using Textual's built-in system.""" 

135 # Use Textual's built-in action to toggle between textual-dark and textual-light 

136 self.action_toggle_dark() 

137 

138 # Show notification 

139 theme_name = "light" if self.current_theme.name == "textual-light" else "dark" 

140 self.notify(f"Switched to {theme_name} mode", timeout=2) 

141 _logger.debug("Theme toggled to: %s", self.current_theme.name) 

142 

143 def action_help(self) -> None: 

144 """Show help screen.""" 

145 help_screen = HelpScreen( 

146 app_name=self.get_app_name(), 

147 keybindings=self.get_keybindings(), 

148 ) 

149 self.push_screen(help_screen) 

150 

151 def get_app_name(self) -> str: 

152 """ 

153 Get application name for help screen. 

154 

155 Returns 

156 ------- 

157 str 

158 Application name (override in subclass) 

159 """ 

160 return "NexusLIMS TUI" 

161 

162 def get_keybindings(self) -> list[tuple[str, str]]: 

163 """ 

164 Get app-specific keybindings for help screen. 

165 

166 Returns 

167 ------- 

168 list[tuple[str, str]] 

169 List of (key, description) tuples (override in subclass) 

170 """ 

171 return [ 

172 ("ctrl+q", "Quit application"), 

173 ("ctrl+t", "Toggle theme (dark/light)"), 

174 ("?", "Show this help"), 

175 ] 

176 

177 def show_error(self, message: str) -> None: 

178 """ 

179 Display an error notification. 

180 

181 Parameters 

182 ---------- 

183 message : str 

184 Error message to display 

185 """ 

186 # Use Textual's notify system 

187 self.notify(message, severity="error", timeout=5) 

188 _logger.error(message) 

189 

190 def show_success(self, message: str) -> None: 

191 """ 

192 Display a success notification. 

193 

194 Parameters 

195 ---------- 

196 message : str 

197 Success message to display 

198 """ 

199 self.notify(message, severity="information", timeout=3) 

200 _logger.info(message) 

201 

202 def show_warning(self, message: str) -> None: 

203 """ 

204 Display a warning notification. 

205 

206 Parameters 

207 ---------- 

208 message : str 

209 Warning message to display 

210 """ 

211 self.notify(message, severity="warning", timeout=4) 

212 _logger.warning(message)