wrkmon 1.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.
@@ -0,0 +1,259 @@
1
+ """Playlists view container for wrkmon."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Vertical
5
+ from textual.widgets import Static, ListView, ListItem, Label, Input
6
+ from textual.binding import Binding
7
+ from textual import on
8
+
9
+ from wrkmon.core.youtube import SearchResult
10
+ from wrkmon.data.models import Playlist, Track
11
+ from wrkmon.ui.messages import TrackSelected, TrackQueued, StatusMessage
12
+ from wrkmon.utils.stealth import get_stealth
13
+
14
+
15
+ class PlaylistItem(ListItem):
16
+ """List item for a playlist."""
17
+
18
+ def __init__(self, playlist: Playlist, index: int, **kwargs) -> None:
19
+ super().__init__(**kwargs)
20
+ self.playlist = playlist
21
+ self.index = index
22
+
23
+ def compose(self) -> ComposeResult:
24
+ count = self.playlist.track_count
25
+ duration = self.playlist.total_duration_str
26
+ text = f"{self.index:>2} {self.playlist.name:<42} {count:>4} tracks {duration:>8}"
27
+ yield Label(text)
28
+
29
+
30
+ class TrackItem(ListItem):
31
+ """List item for a track in a playlist."""
32
+
33
+ def __init__(self, track: Track, index: int, **kwargs) -> None:
34
+ super().__init__(**kwargs)
35
+ self.track = track
36
+ self.index = index
37
+ self._stealth = get_stealth()
38
+
39
+ def compose(self) -> ComposeResult:
40
+ process_name = self._stealth.get_fake_process_name(self.track.title)[:40]
41
+ duration = self.track.duration_str
42
+ text = f" {self.index:>2} {process_name:<40} {duration:>8}"
43
+ yield Label(text)
44
+
45
+
46
+ class PlaylistsView(Vertical):
47
+ """Playlists view - manage playlists and their tracks."""
48
+
49
+ BINDINGS = [
50
+ Binding("a", "queue_selected", "Add to Queue", show=True),
51
+ Binding("n", "new_playlist", "New Playlist", show=True),
52
+ Binding("d", "delete_item", "Delete", show=True),
53
+ Binding("p", "play_all", "Play All", show=True),
54
+ Binding("escape", "go_back", "Back", show=True),
55
+ ]
56
+
57
+ def __init__(self, **kwargs) -> None:
58
+ super().__init__(**kwargs)
59
+ self.playlists: list[Playlist] = []
60
+ self.current_playlist: Playlist | None = None
61
+ self.viewing_tracks = False
62
+
63
+ def compose(self) -> ComposeResult:
64
+ yield Static("PLAYLISTS", id="view-title")
65
+
66
+ yield Static(
67
+ " # Name Tracks Duration",
68
+ id="list-header",
69
+ )
70
+
71
+ yield ListView(id="playlist-list")
72
+
73
+ yield Input(
74
+ placeholder="Enter playlist name...",
75
+ id="new-playlist-input",
76
+ )
77
+
78
+ yield Static("Loading playlists...", id="status-bar")
79
+
80
+ def on_mount(self) -> None:
81
+ """Load playlists on mount."""
82
+ self.query_one("#new-playlist-input", Input).display = False
83
+ self.load_playlists()
84
+
85
+ def load_playlists(self) -> None:
86
+ """Load all playlists."""
87
+ try:
88
+ db = self.app.database
89
+ self.playlists = db.get_all_playlists()
90
+ self._display_playlists()
91
+ except Exception as e:
92
+ self._update_status(f"Failed to load: {e}")
93
+
94
+ def _display_playlists(self) -> None:
95
+ """Display playlist list."""
96
+ self.viewing_tracks = False
97
+ self.current_playlist = None
98
+
99
+ self.query_one("#view-title", Static).update("PLAYLISTS")
100
+ self.query_one("#list-header", Static).update(
101
+ " # Name Tracks Duration"
102
+ )
103
+
104
+ list_view = self.query_one("#playlist-list", ListView)
105
+ list_view.clear()
106
+
107
+ if not self.playlists:
108
+ self._update_status("No playlists. Press 'n' to create one.")
109
+ return
110
+
111
+ for i, playlist in enumerate(self.playlists, 1):
112
+ list_view.append(PlaylistItem(playlist, i))
113
+
114
+ self._update_status(f"{len(self.playlists)} playlists")
115
+
116
+ def _display_tracks(self, playlist: Playlist) -> None:
117
+ """Display tracks in a playlist."""
118
+ self.viewing_tracks = True
119
+ self.current_playlist = playlist
120
+
121
+ # Load full playlist with tracks
122
+ try:
123
+ full_playlist = self.app.database.get_playlist(playlist.id)
124
+ if full_playlist:
125
+ self.current_playlist = full_playlist
126
+ except Exception:
127
+ pass
128
+
129
+ self.query_one("#view-title", Static).update(f"PLAYLIST: {playlist.name}")
130
+ self.query_one("#list-header", Static).update(
131
+ " # Process Duration"
132
+ )
133
+
134
+ list_view = self.query_one("#playlist-list", ListView)
135
+ list_view.clear()
136
+
137
+ if not self.current_playlist.tracks:
138
+ self._update_status("Playlist empty")
139
+ return
140
+
141
+ for i, track in enumerate(self.current_playlist.tracks, 1):
142
+ list_view.append(TrackItem(track, i))
143
+
144
+ self._update_status(
145
+ f"{len(self.current_playlist.tracks)} tracks - "
146
+ f"{self.current_playlist.total_duration_str}"
147
+ )
148
+
149
+ def _update_status(self, message: str) -> None:
150
+ """Update status bar."""
151
+ try:
152
+ self.query_one("#status-bar", Static).update(message)
153
+ except Exception:
154
+ pass
155
+
156
+ def _track_to_result(self, track: Track) -> SearchResult:
157
+ """Convert a Track to SearchResult."""
158
+ return SearchResult(
159
+ video_id=track.video_id,
160
+ title=track.title,
161
+ channel=track.channel,
162
+ duration=track.duration,
163
+ view_count=0,
164
+ )
165
+
166
+ def action_go_back(self) -> None:
167
+ """Go back to playlist list."""
168
+ if self.viewing_tracks:
169
+ self._display_playlists()
170
+
171
+ @on(ListView.Selected, "#playlist-list")
172
+ def handle_item_selected(self, event: ListView.Selected) -> None:
173
+ """Handle Enter key - open playlist or play track."""
174
+ item = event.item
175
+
176
+ if isinstance(item, PlaylistItem):
177
+ self._display_tracks(item.playlist)
178
+ elif isinstance(item, TrackItem):
179
+ result = self._track_to_result(item.track)
180
+ self.post_message(TrackSelected(result))
181
+ self._update_status(f"Playing: {item.track.title[:40]}...")
182
+
183
+ def action_new_playlist(self) -> None:
184
+ """Create a new playlist."""
185
+ if self.viewing_tracks:
186
+ return
187
+
188
+ input_widget = self.query_one("#new-playlist-input", Input)
189
+ input_widget.display = True
190
+ input_widget.focus()
191
+
192
+ @on(Input.Submitted, "#new-playlist-input")
193
+ def handle_new_playlist(self, event: Input.Submitted) -> None:
194
+ """Handle new playlist name submission."""
195
+ name = event.value.strip()
196
+ input_widget = self.query_one("#new-playlist-input", Input)
197
+ input_widget.display = False
198
+ input_widget.value = ""
199
+
200
+ if not name:
201
+ return
202
+
203
+ try:
204
+ db = self.app.database
205
+ playlist = db.create_playlist(name)
206
+ self.playlists.append(playlist)
207
+ self._display_playlists()
208
+ self.post_message(StatusMessage(f"Created: {name}", "success"))
209
+ except Exception as e:
210
+ self.post_message(StatusMessage(f"Error: {e}", "error"))
211
+
212
+ def action_delete_item(self) -> None:
213
+ """Delete selected item."""
214
+ list_view = self.query_one("#playlist-list", ListView)
215
+ if list_view.highlighted_child is None:
216
+ return
217
+
218
+ item = list_view.highlighted_child
219
+ db = self.app.database
220
+
221
+ try:
222
+ if isinstance(item, PlaylistItem):
223
+ if db.delete_playlist(item.playlist.id):
224
+ self.load_playlists()
225
+ self.post_message(StatusMessage(f"Deleted: {item.playlist.name}", "info"))
226
+ elif isinstance(item, TrackItem) and self.current_playlist:
227
+ if db.remove_track_from_playlist(self.current_playlist.id, item.track.id):
228
+ self._display_tracks(self.current_playlist)
229
+ self.post_message(StatusMessage("Track removed", "info"))
230
+ except Exception as e:
231
+ self.post_message(StatusMessage(f"Error: {e}", "error"))
232
+
233
+ async def action_play_all(self) -> None:
234
+ """Play all tracks in current playlist."""
235
+ if not self.current_playlist or not self.current_playlist.tracks:
236
+ return
237
+
238
+ # Add all to queue
239
+ for track in self.current_playlist.tracks:
240
+ result = self._track_to_result(track)
241
+ self.app.add_to_queue(result)
242
+
243
+ # Play first
244
+ first = self.current_playlist.tracks[0]
245
+ result = self._track_to_result(first)
246
+ self.post_message(TrackSelected(result))
247
+ self._update_status("Playing playlist")
248
+
249
+ def action_queue_selected(self) -> None:
250
+ """Add selected track to queue."""
251
+ list_view = self.query_one("#playlist-list", ListView)
252
+ if list_view.highlighted_child is None:
253
+ return
254
+
255
+ item = list_view.highlighted_child
256
+ if isinstance(item, TrackItem):
257
+ result = self._track_to_result(item.track)
258
+ self.post_message(TrackQueued(result))
259
+ self._update_status(f"Queued: {item.track.title[:40]}...")
@@ -0,0 +1,191 @@
1
+ """Queue view container for wrkmon."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Vertical, Horizontal
5
+ from textual.widgets import Static, ListView, ProgressBar
6
+ from textual.binding import Binding
7
+ from textual.reactive import reactive
8
+
9
+ from wrkmon.ui.widgets.result_item import QueueItem
10
+ from wrkmon.ui.messages import StatusMessage
11
+ from wrkmon.utils.stealth import get_stealth
12
+
13
+
14
+ class QueueView(Vertical):
15
+ """Queue/Player view - shows current playback and queue."""
16
+
17
+ BINDINGS = [
18
+ Binding("space", "toggle_pause", "Play/Pause", show=True),
19
+ Binding("n", "next_track", "Next", show=True),
20
+ Binding("p", "prev_track", "Previous", show=True),
21
+ Binding("s", "toggle_shuffle", "Shuffle", show=True),
22
+ Binding("r", "toggle_repeat", "Repeat", show=True),
23
+ Binding("c", "clear_queue", "Clear Queue", show=True),
24
+ Binding("delete", "remove_selected", "Remove", show=False),
25
+ ]
26
+
27
+ # Reactive state
28
+ shuffle_enabled = reactive(False)
29
+ repeat_mode = reactive("none")
30
+
31
+ def __init__(self, **kwargs) -> None:
32
+ super().__init__(**kwargs)
33
+ self._stealth = get_stealth()
34
+
35
+ def compose(self) -> ComposeResult:
36
+ yield Static("QUEUE", id="view-title")
37
+
38
+ # Now playing section
39
+ with Vertical(id="now-playing-section"):
40
+ yield Static("NOW RUNNING", id="section-header")
41
+ yield Static("No process running", id="current-track")
42
+
43
+ with Horizontal(id="playback-progress"):
44
+ yield Static("--:--", id="pos-time")
45
+ yield ProgressBar(total=100, show_percentage=False, id="track-progress")
46
+ yield Static("--:--", id="dur-time")
47
+
48
+ # Mode indicators
49
+ with Horizontal(id="mode-indicators"):
50
+ yield Static("", id="shuffle-indicator")
51
+ yield Static("", id="repeat-indicator")
52
+
53
+ # Queue list
54
+ yield Static(
55
+ " # Process Duration Status",
56
+ id="list-header",
57
+ )
58
+ yield ListView(id="queue-list")
59
+ yield Static("Queue empty", id="status-bar")
60
+
61
+ def on_mount(self) -> None:
62
+ """Initialize the view."""
63
+ self.refresh_queue()
64
+
65
+ def refresh_queue(self) -> None:
66
+ """Refresh the queue display."""
67
+ try:
68
+ queue = self.app.queue
69
+ list_view = self.query_one("#queue-list", ListView)
70
+ list_view.clear()
71
+
72
+ items = queue.to_list()
73
+ if not items:
74
+ self._update_status("Queue empty")
75
+ return
76
+
77
+ for i, item in enumerate(items):
78
+ is_current = i == queue.current_index
79
+ list_view.append(
80
+ QueueItem(
81
+ title=item.title,
82
+ duration=item.duration,
83
+ index=i + 1,
84
+ is_current=is_current,
85
+ )
86
+ )
87
+
88
+ self._update_status(f"{len(items)} in queue")
89
+ self._update_mode_indicators()
90
+ except Exception:
91
+ pass
92
+
93
+ def update_now_playing(self, title: str, position: float, duration: float) -> None:
94
+ """Update the now playing display."""
95
+ try:
96
+ process_name = self._stealth.get_fake_process_name(title)
97
+ self.query_one("#current-track", Static).update(process_name)
98
+ self.query_one("#pos-time", Static).update(self._stealth.format_duration(position))
99
+ self.query_one("#dur-time", Static).update(self._stealth.format_duration(duration))
100
+
101
+ if duration > 0:
102
+ progress = (position / duration) * 100
103
+ self.query_one("#track-progress", ProgressBar).update(progress=progress)
104
+ except Exception:
105
+ pass
106
+
107
+ def _update_status(self, message: str) -> None:
108
+ """Update status bar."""
109
+ try:
110
+ self.query_one("#status-bar", Static).update(message)
111
+ except Exception:
112
+ pass
113
+
114
+ def _update_mode_indicators(self) -> None:
115
+ """Update shuffle/repeat indicators."""
116
+ try:
117
+ queue = self.app.queue
118
+
119
+ shuffle_text = "[SHUFFLE]" if queue.shuffle_mode else ""
120
+ self.query_one("#shuffle-indicator", Static).update(shuffle_text)
121
+
122
+ repeat_text = ""
123
+ if queue.repeat_mode == "one":
124
+ repeat_text = "[REPEAT ONE]"
125
+ elif queue.repeat_mode == "all":
126
+ repeat_text = "[REPEAT ALL]"
127
+ self.query_one("#repeat-indicator", Static).update(repeat_text)
128
+ except Exception:
129
+ pass
130
+
131
+ async def action_toggle_pause(self) -> None:
132
+ """Toggle playback."""
133
+ try:
134
+ await self.app.toggle_pause()
135
+ except Exception:
136
+ pass
137
+
138
+ async def action_next_track(self) -> None:
139
+ """Play next track."""
140
+ try:
141
+ await self.app.play_next()
142
+ self.refresh_queue()
143
+ except Exception:
144
+ pass
145
+
146
+ async def action_prev_track(self) -> None:
147
+ """Play previous track."""
148
+ try:
149
+ await self.app.play_previous()
150
+ self.refresh_queue()
151
+ except Exception:
152
+ pass
153
+
154
+ def action_toggle_shuffle(self) -> None:
155
+ """Toggle shuffle mode."""
156
+ try:
157
+ is_shuffle = self.app.queue.toggle_shuffle()
158
+ self._update_mode_indicators()
159
+ status = "Shuffle ON" if is_shuffle else "Shuffle OFF"
160
+ self.post_message(StatusMessage(status, "info"))
161
+ except Exception:
162
+ pass
163
+
164
+ def action_toggle_repeat(self) -> None:
165
+ """Cycle repeat mode."""
166
+ try:
167
+ mode = self.app.queue.cycle_repeat()
168
+ self._update_mode_indicators()
169
+ mode_names = {"none": "OFF", "one": "ONE", "all": "ALL"}
170
+ self.post_message(StatusMessage(f"Repeat: {mode_names[mode]}", "info"))
171
+ except Exception:
172
+ pass
173
+
174
+ def action_clear_queue(self) -> None:
175
+ """Clear the queue."""
176
+ try:
177
+ self.app.queue.clear()
178
+ self.refresh_queue()
179
+ self.post_message(StatusMessage("Queue cleared", "info"))
180
+ except Exception:
181
+ pass
182
+
183
+ def action_remove_selected(self) -> None:
184
+ """Remove selected item from queue."""
185
+ try:
186
+ list_view = self.query_one("#queue-list", ListView)
187
+ if list_view.index is not None:
188
+ self.app.queue.remove(list_view.index)
189
+ self.refresh_queue()
190
+ except Exception:
191
+ pass
@@ -0,0 +1,150 @@
1
+ """Search view container for wrkmon."""
2
+
3
+ import logging
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Vertical
6
+ from textual.widgets import Static, Input, ListView
7
+ from textual.binding import Binding
8
+ from textual.events import Key
9
+ from textual import on
10
+
11
+ from wrkmon.core.youtube import SearchResult
12
+ from wrkmon.ui.messages import TrackSelected, TrackQueued, StatusMessage
13
+ from wrkmon.ui.widgets.result_item import ResultItem
14
+
15
+ logger = logging.getLogger("wrkmon.search")
16
+
17
+
18
+ class SearchView(Vertical):
19
+ """Main search view - search YouTube and display results."""
20
+
21
+ BINDINGS = [
22
+ Binding("a", "queue_selected", "Add to Queue", show=True),
23
+ Binding("escape", "clear_search", "Clear", show=True),
24
+ Binding("/", "focus_search", "Search", show=True),
25
+ ]
26
+
27
+ def __init__(self, **kwargs) -> None:
28
+ super().__init__(**kwargs)
29
+ self.results: list[SearchResult] = []
30
+ self._is_searching = False
31
+
32
+ def compose(self) -> ComposeResult:
33
+ yield Static("SEARCH", id="view-title")
34
+
35
+ with Vertical(id="search-container"):
36
+ yield Input(
37
+ placeholder="Search processes...",
38
+ id="search-input",
39
+ )
40
+
41
+ yield Static(
42
+ " # Process PID Duration Status",
43
+ id="list-header",
44
+ )
45
+
46
+ yield ListView(id="results-list")
47
+ yield Static("Type to search", id="status-bar")
48
+
49
+ def on_mount(self) -> None:
50
+ """Focus the search input on mount."""
51
+ self.query_one("#search-input", Input).focus()
52
+
53
+ @on(Input.Submitted, "#search-input")
54
+ async def handle_search(self, event: Input.Submitted) -> None:
55
+ """Execute search when Enter is pressed."""
56
+ query = event.value.strip()
57
+ if not query or self._is_searching:
58
+ return
59
+
60
+ self._is_searching = True
61
+ self._update_status("Searching...")
62
+
63
+ try:
64
+ # Access the app's YouTube client
65
+ youtube = self.app.youtube
66
+ self.results = await youtube.search(query, max_results=15)
67
+ self._display_results()
68
+ except Exception as e:
69
+ self._update_status(f"Search failed: {e}")
70
+ self.post_message(StatusMessage(f"Search error: {e}", "error"))
71
+ finally:
72
+ self._is_searching = False
73
+
74
+ def _display_results(self) -> None:
75
+ """Display search results in the list."""
76
+ list_view = self.query_one("#results-list", ListView)
77
+ list_view.clear()
78
+
79
+ if not self.results:
80
+ self._update_status("No results found")
81
+ return
82
+
83
+ for i, result in enumerate(self.results, 1):
84
+ list_view.append(ResultItem(result, i))
85
+
86
+ self._update_status(f"Found {len(self.results)} | Enter=Play A=Queue /=Search ↑↓=Nav")
87
+ list_view.focus()
88
+
89
+ def _update_status(self, message: str) -> None:
90
+ """Update the status bar."""
91
+ try:
92
+ self.query_one("#status-bar", Static).update(message)
93
+ except Exception:
94
+ pass
95
+
96
+ def _get_selected(self) -> SearchResult | None:
97
+ """Get the currently selected result."""
98
+ list_view = self.query_one("#results-list", ListView)
99
+ if list_view.highlighted_child is None:
100
+ return None
101
+
102
+ item = list_view.highlighted_child
103
+ if isinstance(item, ResultItem):
104
+ return item.result
105
+ return None
106
+
107
+ def action_clear_search(self) -> None:
108
+ """Clear the search input."""
109
+ search_input = self.query_one("#search-input", Input)
110
+ if search_input.value:
111
+ search_input.value = ""
112
+ search_input.focus()
113
+
114
+ @on(ListView.Selected, "#results-list")
115
+ def handle_result_selected(self, event: ListView.Selected) -> None:
116
+ """Handle Enter key on a result - play it."""
117
+ if isinstance(event.item, ResultItem):
118
+ result = event.item.result
119
+ self.post_message(TrackSelected(result))
120
+ self._update_status(f"Playing: {result.title[:40]}...")
121
+
122
+ def action_queue_selected(self) -> None:
123
+ """Add selected track to queue."""
124
+ logger.info("=== 'a' PRESSED: action_queue_selected ===")
125
+ result = self._get_selected()
126
+ logger.info(f" Selected result: {result}")
127
+ if result:
128
+ logger.info(f" Posting TrackQueued for: {result.title}")
129
+ self.post_message(TrackQueued(result))
130
+ self._update_status(f"Queued: {result.title[:40]}...")
131
+ else:
132
+ logger.warning(" No result selected!")
133
+
134
+ def focus_input(self) -> None:
135
+ """Focus the search input (called from parent)."""
136
+ self.query_one("#search-input", Input).focus()
137
+
138
+ def action_focus_search(self) -> None:
139
+ """Focus the search input (/ key)."""
140
+ self.focus_input()
141
+
142
+ def on_key(self, event: Key) -> None:
143
+ """Handle key events - up at top of list goes to search."""
144
+ if event.key == "up":
145
+ list_view = self.query_one("#results-list", ListView)
146
+ # If list is focused and at top (index 0), go to search input
147
+ if list_view.has_focus and list_view.index == 0:
148
+ self.focus_input()
149
+ event.prevent_default()
150
+ event.stop()
@@ -0,0 +1,7 @@
1
+ """Reusable widgets for wrkmon TUI."""
2
+
3
+ from wrkmon.ui.widgets.header import HeaderBar
4
+ from wrkmon.ui.widgets.player_bar import PlayerBar
5
+ from wrkmon.ui.widgets.result_item import ResultItem
6
+
7
+ __all__ = ["HeaderBar", "PlayerBar", "ResultItem"]
@@ -0,0 +1,59 @@
1
+ """Header bar widget for wrkmon."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Horizontal
5
+ from textual.reactive import reactive
6
+ from textual.widgets import Static
7
+
8
+ from wrkmon.utils.stealth import get_stealth
9
+
10
+
11
+ class HeaderBar(Static):
12
+ """Application header with title and fake system stats."""
13
+
14
+ cpu = reactive(23)
15
+ mem = reactive(45)
16
+
17
+ def __init__(self, **kwargs) -> None:
18
+ super().__init__(**kwargs)
19
+ self._stealth = get_stealth()
20
+
21
+ def compose(self) -> ComposeResult:
22
+ with Horizontal(id="header-inner"):
23
+ yield Static("WRKMON", id="app-title")
24
+ yield Static("", id="current-view")
25
+ yield Static(self._format_stats(), id="sys-stats")
26
+
27
+ def _format_stats(self) -> str:
28
+ return f"CPU:{self.cpu:>3}% MEM:{self.mem:>3}%"
29
+
30
+ def on_mount(self) -> None:
31
+ """Start periodic stats updates."""
32
+ self.set_interval(3.0, self._update_stats)
33
+
34
+ def _update_stats(self) -> None:
35
+ """Update fake system stats."""
36
+ self.cpu = self._stealth.get_fake_cpu()
37
+ self.mem = self._stealth.get_fake_memory()
38
+
39
+ def watch_cpu(self) -> None:
40
+ """React to CPU changes."""
41
+ self._refresh_stats()
42
+
43
+ def watch_mem(self) -> None:
44
+ """React to memory changes."""
45
+ self._refresh_stats()
46
+
47
+ def _refresh_stats(self) -> None:
48
+ """Update the stats display."""
49
+ try:
50
+ self.query_one("#sys-stats", Static).update(self._format_stats())
51
+ except Exception:
52
+ pass
53
+
54
+ def set_view_name(self, name: str) -> None:
55
+ """Update the current view indicator."""
56
+ try:
57
+ self.query_one("#current-view", Static).update(f"/{name.upper()}")
58
+ except Exception:
59
+ pass