meeting-noter 1.3.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. meeting_noter/__init__.py +1 -1
  2. meeting_noter/cli.py +103 -0
  3. meeting_noter/daemon.py +38 -0
  4. meeting_noter/gui/__init__.py +51 -0
  5. meeting_noter/gui/main_window.py +219 -0
  6. meeting_noter/gui/menubar.py +248 -0
  7. meeting_noter/gui/screens/__init__.py +17 -0
  8. meeting_noter/gui/screens/dashboard.py +262 -0
  9. meeting_noter/gui/screens/logs.py +184 -0
  10. meeting_noter/gui/screens/recordings.py +279 -0
  11. meeting_noter/gui/screens/search.py +229 -0
  12. meeting_noter/gui/screens/settings.py +232 -0
  13. meeting_noter/gui/screens/viewer.py +140 -0
  14. meeting_noter/gui/theme/__init__.py +5 -0
  15. meeting_noter/gui/theme/dark_theme.py +53 -0
  16. meeting_noter/gui/theme/styles.qss +504 -0
  17. meeting_noter/gui/utils/__init__.py +15 -0
  18. meeting_noter/gui/utils/signals.py +82 -0
  19. meeting_noter/gui/utils/workers.py +258 -0
  20. meeting_noter/gui/widgets/__init__.py +6 -0
  21. meeting_noter/gui/widgets/sidebar.py +210 -0
  22. meeting_noter/gui/widgets/status_indicator.py +108 -0
  23. meeting_noter/mic_monitor.py +29 -1
  24. meeting_noter/ui/__init__.py +5 -0
  25. meeting_noter/ui/app.py +68 -0
  26. meeting_noter/ui/screens/__init__.py +17 -0
  27. meeting_noter/ui/screens/logs.py +166 -0
  28. meeting_noter/ui/screens/main.py +346 -0
  29. meeting_noter/ui/screens/recordings.py +241 -0
  30. meeting_noter/ui/screens/search.py +191 -0
  31. meeting_noter/ui/screens/settings.py +184 -0
  32. meeting_noter/ui/screens/viewer.py +116 -0
  33. meeting_noter/ui/styles/app.tcss +257 -0
  34. meeting_noter/ui/widgets/__init__.py +1 -0
  35. {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/METADATA +4 -1
  36. meeting_noter-3.0.0.dist-info/RECORD +65 -0
  37. meeting_noter-1.3.0.dist-info/RECORD +0 -35
  38. {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/WHEEL +0 -0
  39. {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/entry_points.txt +0 -0
  40. {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,241 @@
1
+ """Recordings browser screen for Meeting Noter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from textual.app import ComposeResult
11
+ from textual.containers import Container, Horizontal
12
+ from textual.screen import Screen
13
+ from textual.widgets import Button, DataTable, Label, Static
14
+
15
+ from meeting_noter.config import get_config
16
+ from meeting_noter.output.writer import format_duration, format_size, get_audio_duration
17
+
18
+
19
+ @dataclass
20
+ class RecordingInfo:
21
+ """Information about a recording."""
22
+
23
+ filepath: Path
24
+ date: datetime
25
+ duration: Optional[float]
26
+ size: int
27
+ has_transcript: bool
28
+ is_favorite: bool
29
+
30
+
31
+ def get_recordings_list(output_dir: Path, limit: int = 50) -> list[RecordingInfo]:
32
+ """Get list of recordings with metadata."""
33
+ config = get_config()
34
+
35
+ if not output_dir.exists():
36
+ return []
37
+
38
+ mp3_files = sorted(
39
+ output_dir.glob("*.mp3"),
40
+ key=lambda p: p.stat().st_mtime,
41
+ reverse=True,
42
+ )
43
+
44
+ # Get transcripts directory from config
45
+ transcripts_dir = config.transcripts_dir
46
+
47
+ recordings = []
48
+ for mp3 in mp3_files[:limit]:
49
+ stat = mp3.stat()
50
+ transcript_name = mp3.stem + ".txt"
51
+
52
+ # Check for transcript in transcripts directory
53
+ transcript_path = transcripts_dir / transcript_name
54
+ has_transcript = transcript_path.exists()
55
+
56
+ recordings.append(
57
+ RecordingInfo(
58
+ filepath=mp3,
59
+ date=datetime.fromtimestamp(stat.st_mtime),
60
+ duration=get_audio_duration(mp3),
61
+ size=stat.st_size,
62
+ has_transcript=has_transcript,
63
+ is_favorite=config.is_favorite(transcript_name),
64
+ )
65
+ )
66
+
67
+ return recordings
68
+
69
+
70
+ class RecordingsScreen(Screen):
71
+ """Recordings browser screen."""
72
+
73
+ BINDINGS = [
74
+ ("t", "transcribe", "Transcribe"),
75
+ ("enter", "view", "View"),
76
+ ("f", "toggle_favorite", "Favorite"),
77
+ ("r", "refresh", "Refresh"),
78
+ ]
79
+
80
+ def compose(self) -> ComposeResult:
81
+ """Compose the recordings screen layout."""
82
+ yield Container(
83
+ Static("[bold]Recordings[/bold]", classes="title"),
84
+ Static("", classes="spacer"),
85
+ DataTable(id="recordings-table", cursor_type="row"),
86
+ Static("", classes="spacer"),
87
+ Horizontal(
88
+ Button("View", id="view-btn"),
89
+ Button("Transcribe", id="transcribe-btn", variant="primary"),
90
+ Button("Toggle Favorite", id="favorite-btn"),
91
+ Button("Refresh", id="refresh-btn"),
92
+ classes="button-row",
93
+ ),
94
+ Label("", id="status-label"),
95
+ id="recordings-container",
96
+ )
97
+
98
+ def on_mount(self) -> None:
99
+ """Load recordings on mount."""
100
+ self._setup_table()
101
+ self._load_recordings()
102
+
103
+ def _setup_table(self) -> None:
104
+ """Set up the recordings table."""
105
+ table = self.query_one("#recordings-table", DataTable)
106
+ table.add_columns("★", "Date", "Duration", "Size", "Transcript", "File")
107
+
108
+ def _load_recordings(self) -> None:
109
+ """Load recordings into the table."""
110
+ config = get_config()
111
+ table = self.query_one("#recordings-table", DataTable)
112
+ status = self.query_one("#status-label", Label)
113
+
114
+ table.clear()
115
+
116
+ recordings = get_recordings_list(config.recordings_dir)
117
+
118
+ if not recordings:
119
+ status.update("[yellow]No recordings found[/yellow]")
120
+ return
121
+
122
+ for rec in recordings:
123
+ star = "★" if rec.is_favorite else ""
124
+ date_str = rec.date.strftime("%Y-%m-%d %H:%M")
125
+ duration_str = format_duration(rec.duration) if rec.duration else "?"
126
+ size_str = format_size(rec.size)
127
+ transcript_str = "Yes" if rec.has_transcript else "No"
128
+
129
+ table.add_row(
130
+ star,
131
+ date_str,
132
+ duration_str,
133
+ size_str,
134
+ transcript_str,
135
+ rec.filepath.name,
136
+ key=str(rec.filepath),
137
+ )
138
+
139
+ status.update(f"[dim]{len(recordings)} recordings[/dim]")
140
+
141
+ def on_button_pressed(self, event: Button.Pressed) -> None:
142
+ """Handle button presses."""
143
+ button_id = event.button.id
144
+
145
+ if button_id == "view-btn":
146
+ self.action_view()
147
+ elif button_id == "transcribe-btn":
148
+ self.action_transcribe()
149
+ elif button_id == "favorite-btn":
150
+ self.action_toggle_favorite()
151
+ elif button_id == "refresh-btn":
152
+ self.action_refresh()
153
+
154
+ def _get_selected_filepath(self) -> Optional[Path]:
155
+ """Get the filepath of the currently selected recording."""
156
+ table = self.query_one("#recordings-table", DataTable)
157
+
158
+ if table.row_count == 0:
159
+ return None
160
+
161
+ row_key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
162
+ if row_key:
163
+ return Path(row_key.value)
164
+ return None
165
+
166
+ def action_view(self) -> None:
167
+ """View the selected transcript."""
168
+ filepath = self._get_selected_filepath()
169
+ if not filepath:
170
+ self.notify("No recording selected", severity="warning")
171
+ return
172
+
173
+ config = get_config()
174
+ transcript_name = filepath.stem + ".txt"
175
+ transcript = config.transcripts_dir / transcript_name
176
+
177
+ if not transcript.exists():
178
+ self.notify("No transcript available. Transcribe first.", severity="warning")
179
+ return
180
+
181
+ # Push the viewer screen with the transcript path
182
+ from meeting_noter.ui.screens.viewer import ViewerScreen
183
+
184
+ self.app.push_screen(ViewerScreen(transcript))
185
+
186
+ def action_transcribe(self) -> None:
187
+ """Transcribe the selected recording."""
188
+ import subprocess
189
+ import sys
190
+
191
+ filepath = self._get_selected_filepath()
192
+ if not filepath:
193
+ self.notify("No recording selected", severity="warning")
194
+ return
195
+
196
+ config = get_config()
197
+ transcript_name = filepath.stem + ".txt"
198
+ transcript = config.transcripts_dir / transcript_name
199
+
200
+ if transcript.exists():
201
+ self.notify("Transcript already exists", severity="warning")
202
+ return
203
+
204
+ # Run transcription in background
205
+ subprocess.Popen(
206
+ [
207
+ sys.executable,
208
+ "-m",
209
+ "meeting_noter.cli",
210
+ "transcribe",
211
+ str(filepath),
212
+ ],
213
+ stdout=subprocess.DEVNULL,
214
+ stderr=subprocess.DEVNULL,
215
+ )
216
+
217
+ self.notify(f"Transcribing: {filepath.name}", severity="information")
218
+
219
+ def action_toggle_favorite(self) -> None:
220
+ """Toggle favorite status for the selected recording."""
221
+ filepath = self._get_selected_filepath()
222
+ if not filepath:
223
+ self.notify("No recording selected", severity="warning")
224
+ return
225
+
226
+ config = get_config()
227
+ transcript_name = filepath.with_suffix(".txt").name
228
+
229
+ if config.is_favorite(transcript_name):
230
+ config.remove_favorite(transcript_name)
231
+ self.notify(f"Removed from favorites: {transcript_name}")
232
+ else:
233
+ config.add_favorite(transcript_name)
234
+ self.notify(f"Added to favorites: {transcript_name}")
235
+
236
+ self._load_recordings()
237
+
238
+ def action_refresh(self) -> None:
239
+ """Refresh the recordings list."""
240
+ self._load_recordings()
241
+ self.notify("Refreshed")
@@ -0,0 +1,191 @@
1
+ """Search screen for Meeting Noter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Container, Horizontal, Vertical
10
+ from textual.screen import Screen
11
+ from textual.widgets import Button, Checkbox, Input, Label, ListView, ListItem, Static
12
+
13
+ from meeting_noter.config import get_config
14
+ from meeting_noter.output.searcher import _search_file, FileSearchResult
15
+
16
+
17
+ class SearchResultItem(ListItem):
18
+ """A search result list item."""
19
+
20
+ def __init__(self, filepath: Path, match_text: str, timestamp: Optional[str] = None):
21
+ super().__init__()
22
+ self.filepath = filepath
23
+ self.match_text = match_text
24
+ self.timestamp = timestamp
25
+
26
+ def compose(self) -> ComposeResult:
27
+ """Compose the list item."""
28
+ if self.timestamp:
29
+ yield Static(f"[cyan][{self.timestamp}][/cyan] {self.match_text[:70]}...")
30
+ else:
31
+ yield Static(f"{self.match_text[:80]}...")
32
+
33
+
34
+ class SearchScreen(Screen):
35
+ """Search screen."""
36
+
37
+ BINDINGS = [
38
+ ("slash", "focus_search", "Search"),
39
+ ("enter", "view_selected", "View"),
40
+ ]
41
+
42
+ def compose(self) -> ComposeResult:
43
+ """Compose the search screen layout."""
44
+ yield Container(
45
+ Static("[bold]Search Transcripts[/bold]", classes="title"),
46
+ Static("", classes="spacer"),
47
+ Horizontal(
48
+ Input(placeholder="Enter search query...", id="search-input"),
49
+ Button("Search", id="search-btn", variant="primary"),
50
+ classes="search-row",
51
+ ),
52
+ Horizontal(
53
+ Checkbox("Case sensitive", id="case-sensitive"),
54
+ classes="options-row",
55
+ ),
56
+ Static("", classes="spacer"),
57
+ Label("", id="results-header"),
58
+ ListView(id="results-list"),
59
+ Label("", id="results-footer"),
60
+ id="search-container",
61
+ )
62
+
63
+ def on_mount(self) -> None:
64
+ """Focus search input on mount."""
65
+ self.query_one("#search-input", Input).focus()
66
+
67
+ def action_focus_search(self) -> None:
68
+ """Focus the search input."""
69
+ self.query_one("#search-input", Input).focus()
70
+
71
+ def on_button_pressed(self, event: Button.Pressed) -> None:
72
+ """Handle button presses."""
73
+ if event.button.id == "search-btn":
74
+ self._do_search()
75
+
76
+ def on_input_submitted(self, event: Input.Submitted) -> None:
77
+ """Handle Enter in search input."""
78
+ if event.input.id == "search-input":
79
+ self._do_search()
80
+
81
+ def _do_search(self) -> None:
82
+ """Perform the search."""
83
+ config = get_config()
84
+ query = self.query_one("#search-input", Input).value.strip()
85
+ case_sensitive = self.query_one("#case-sensitive", Checkbox).value
86
+
87
+ results_list = self.query_one("#results-list", ListView)
88
+ results_header = self.query_one("#results-header", Label)
89
+ results_footer = self.query_one("#results-footer", Label)
90
+
91
+ results_list.clear()
92
+
93
+ if not query:
94
+ results_header.update("[yellow]Enter a search query[/yellow]")
95
+ results_footer.update("")
96
+ return
97
+
98
+ transcripts_dir = config.transcripts_dir
99
+ if not transcripts_dir.exists():
100
+ results_header.update("[red]Transcripts directory not found[/red]")
101
+ results_footer.update("")
102
+ return
103
+
104
+ # Find all transcript files
105
+ txt_files = sorted(
106
+ transcripts_dir.glob("*.txt"),
107
+ key=lambda p: p.stat().st_mtime,
108
+ reverse=True,
109
+ )
110
+
111
+ if not txt_files:
112
+ results_header.update("[yellow]No transcripts found[/yellow]")
113
+ results_footer.update("")
114
+ return
115
+
116
+ # Search all files
117
+ results: list[FileSearchResult] = []
118
+ for txt_file in txt_files:
119
+ result = _search_file(txt_file, query, case_sensitive)
120
+ if result:
121
+ results.append(result)
122
+
123
+ if not results:
124
+ results_header.update(f"[yellow]No results for '{query}'[/yellow]")
125
+ results_footer.update(f"[dim]Searched {len(txt_files)} transcripts[/dim]")
126
+ return
127
+
128
+ # Sort by match count
129
+ results.sort(key=lambda r: r.match_count, reverse=True)
130
+
131
+ # Count totals
132
+ total_matches = sum(r.match_count for r in results)
133
+ total_files = len(results)
134
+
135
+ results_header.update(
136
+ f"[green]Found {total_matches} matches in {total_files} transcripts[/green]"
137
+ )
138
+
139
+ # Add results to list (limit for performance)
140
+ max_items = 50
141
+ items_added = 0
142
+
143
+ for result in results:
144
+ if items_added >= max_items:
145
+ break
146
+
147
+ # Add file header
148
+ results_list.append(
149
+ ListItem(
150
+ Static(f"[bold green]{result.filepath.name}[/bold green] ({result.match_count} matches)")
151
+ )
152
+ )
153
+
154
+ # Add matches (limit per file)
155
+ for match in result.matches[:5]:
156
+ results_list.append(
157
+ SearchResultItem(
158
+ result.filepath,
159
+ match.line.strip(),
160
+ match.timestamp,
161
+ )
162
+ )
163
+ items_added += 1
164
+ if items_added >= max_items:
165
+ break
166
+
167
+ if total_matches > max_items:
168
+ results_footer.update(
169
+ f"[dim]Showing {items_added} of {total_matches} matches. "
170
+ f"Searched {len(txt_files)} transcripts.[/dim]"
171
+ )
172
+ else:
173
+ results_footer.update(f"[dim]Searched {len(txt_files)} transcripts[/dim]")
174
+
175
+ def on_list_view_selected(self, event: ListView.Selected) -> None:
176
+ """Handle list item selection."""
177
+ item = event.item
178
+ if isinstance(item, SearchResultItem):
179
+ from meeting_noter.ui.screens.viewer import ViewerScreen
180
+
181
+ self.app.push_screen(ViewerScreen(item.filepath))
182
+
183
+ def action_view_selected(self) -> None:
184
+ """View the selected result's transcript."""
185
+ results_list = self.query_one("#results-list", ListView)
186
+ if results_list.highlighted_child:
187
+ item = results_list.highlighted_child
188
+ if isinstance(item, SearchResultItem):
189
+ from meeting_noter.ui.screens.viewer import ViewerScreen
190
+
191
+ self.app.push_screen(ViewerScreen(item.filepath))
@@ -0,0 +1,184 @@
1
+ """Settings screen for Meeting Noter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Container, Horizontal, Vertical
7
+ from textual.screen import Screen
8
+ from textual.widgets import Button, Input, Label, Select, Static, Switch
9
+
10
+ from meeting_noter.config import get_config
11
+
12
+
13
+ WHISPER_MODELS = [
14
+ ("tiny.en (fastest)", "tiny.en"),
15
+ ("base.en (balanced)", "base.en"),
16
+ ("small.en (better)", "small.en"),
17
+ ("medium.en (high quality)", "medium.en"),
18
+ ("large-v3 (best, multilingual)", "large-v3"),
19
+ ]
20
+
21
+
22
+ class SettingsScreen(Screen):
23
+ """Settings configuration screen."""
24
+
25
+ BINDINGS = [
26
+ ("s", "save", "Save"),
27
+ ("escape", "go_back", "Back"),
28
+ ]
29
+
30
+ def compose(self) -> ComposeResult:
31
+ """Compose the settings screen layout."""
32
+ config = get_config()
33
+
34
+ yield Container(
35
+ Static("[bold]Settings[/bold]", classes="title"),
36
+ Static("", classes="spacer"),
37
+ Vertical(
38
+ # Directories
39
+ Static("[dim]Directories[/dim]", classes="section-header"),
40
+ Horizontal(
41
+ Label("Recordings: ", classes="setting-label"),
42
+ Input(
43
+ str(config.recordings_dir),
44
+ id="recordings-dir",
45
+ classes="setting-input",
46
+ ),
47
+ classes="setting-row",
48
+ ),
49
+ Horizontal(
50
+ Label("Transcripts: ", classes="setting-label"),
51
+ Input(
52
+ str(config.transcripts_dir),
53
+ id="transcripts-dir",
54
+ classes="setting-input",
55
+ ),
56
+ classes="setting-row",
57
+ ),
58
+ Static("", classes="spacer"),
59
+ # Whisper model
60
+ Static("[dim]Transcription[/dim]", classes="section-header"),
61
+ Horizontal(
62
+ Label("Whisper model: ", classes="setting-label"),
63
+ Select(
64
+ WHISPER_MODELS,
65
+ value=config.whisper_model,
66
+ id="whisper-model",
67
+ ),
68
+ classes="setting-row",
69
+ ),
70
+ Static("", classes="spacer"),
71
+ # Toggles
72
+ Static("[dim]Options[/dim]", classes="section-header"),
73
+ Horizontal(
74
+ Label("Auto-transcribe: ", classes="setting-label"),
75
+ Switch(value=config.auto_transcribe, id="auto-transcribe"),
76
+ classes="setting-row",
77
+ ),
78
+ Horizontal(
79
+ Label("Capture system audio: ", classes="setting-label"),
80
+ Switch(value=config.capture_system_audio, id="capture-system-audio"),
81
+ classes="setting-row",
82
+ ),
83
+ Horizontal(
84
+ Label("Auto-update: ", classes="setting-label"),
85
+ Switch(value=config.auto_update, id="auto-update"),
86
+ classes="setting-row",
87
+ ),
88
+ Static("", classes="spacer"),
89
+ # Silence timeout
90
+ Horizontal(
91
+ Label("Silence timeout (min): ", classes="setting-label"),
92
+ Input(
93
+ str(config.silence_timeout),
94
+ id="silence-timeout",
95
+ type="integer",
96
+ ),
97
+ classes="setting-row",
98
+ ),
99
+ classes="settings-form",
100
+ ),
101
+ Static("", classes="spacer"),
102
+ Horizontal(
103
+ Button("Save", id="save-btn", variant="success"),
104
+ Button("Reset", id="reset-btn"),
105
+ Button("Back", id="back-btn"),
106
+ classes="button-row",
107
+ ),
108
+ Label("", id="status-label"),
109
+ id="settings-container",
110
+ )
111
+
112
+ def on_button_pressed(self, event: Button.Pressed) -> None:
113
+ """Handle button presses."""
114
+ button_id = event.button.id
115
+
116
+ if button_id == "save-btn":
117
+ self.action_save()
118
+ elif button_id == "reset-btn":
119
+ self._reset_to_current()
120
+ elif button_id == "back-btn":
121
+ self.action_go_back()
122
+
123
+ def action_save(self) -> None:
124
+ """Save settings."""
125
+ from pathlib import Path
126
+
127
+ config = get_config()
128
+
129
+ try:
130
+ # Get values from inputs
131
+ recordings_dir = self.query_one("#recordings-dir", Input).value
132
+ transcripts_dir = self.query_one("#transcripts-dir", Input).value
133
+ whisper_model = self.query_one("#whisper-model", Select).value
134
+ auto_transcribe = self.query_one("#auto-transcribe", Switch).value
135
+ capture_system_audio = self.query_one("#capture-system-audio", Switch).value
136
+ auto_update = self.query_one("#auto-update", Switch).value
137
+ silence_timeout = self.query_one("#silence-timeout", Input).value
138
+
139
+ # Validate and set
140
+ recordings_path = Path(recordings_dir).expanduser()
141
+ transcripts_path = Path(transcripts_dir).expanduser()
142
+
143
+ # Create directories if they don't exist
144
+ recordings_path.mkdir(parents=True, exist_ok=True)
145
+ transcripts_path.mkdir(parents=True, exist_ok=True)
146
+
147
+ # Update config
148
+ config.recordings_dir = recordings_path
149
+ config.transcripts_dir = transcripts_path
150
+ config.whisper_model = whisper_model
151
+ config.auto_transcribe = auto_transcribe
152
+ config.capture_system_audio = capture_system_audio
153
+ config.auto_update = auto_update
154
+ config.silence_timeout = int(silence_timeout) if silence_timeout else 5
155
+
156
+ config.save()
157
+
158
+ self.notify("Settings saved", severity="information")
159
+ self.query_one("#status-label", Label).update(
160
+ "[green]Settings saved successfully[/green]"
161
+ )
162
+
163
+ except Exception as e:
164
+ self.notify(f"Error saving settings: {e}", severity="error")
165
+ self.query_one("#status-label", Label).update(f"[red]Error: {e}[/red]")
166
+
167
+ def _reset_to_current(self) -> None:
168
+ """Reset form to current config values."""
169
+ config = get_config()
170
+ config.load() # Reload from disk
171
+
172
+ self.query_one("#recordings-dir", Input).value = str(config.recordings_dir)
173
+ self.query_one("#transcripts-dir", Input).value = str(config.transcripts_dir)
174
+ self.query_one("#whisper-model", Select).value = config.whisper_model
175
+ self.query_one("#auto-transcribe", Switch).value = config.auto_transcribe
176
+ self.query_one("#capture-system-audio", Switch).value = config.capture_system_audio
177
+ self.query_one("#auto-update", Switch).value = config.auto_update
178
+ self.query_one("#silence-timeout", Input).value = str(config.silence_timeout)
179
+
180
+ self.notify("Reset to saved values")
181
+
182
+ def action_go_back(self) -> None:
183
+ """Go back to the previous screen."""
184
+ self.app.pop_screen()