feat: interactive terminal controls - #7
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an interactive terminal UI layer to Focus (profile picker + live status line + single-key transport controls) and refactors session/audio output to support pause/resume and real-time volume changes, while keeping non-interactive CLI usage intact.
Changes:
- Introduces terminal transport controls (
PlaybackState,KeyboardController) and a liveStatusLine. - Adds a bare
focusinteractive launcher (profile picker) and routes bothfocus startand launcher through a sharedlaunch_session(). - Extends
AudioOutput/MockAudioOutputwith pause/resume and gain control; adds UI-focused tests and README updates.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_ui.py | Adds tests for transport state handling, status line rendering, terminal readers, and launcher gating. |
| src/focus/ui/transport.py | Implements shared playback state, keyboard controller, and status line rendering. |
| src/focus/ui/launcher.py | Implements interactive terminal profile picker shown on bare focus invocation. |
| src/focus/ui/init.py | Adds UI package module docstring. |
| src/focus/cli.py | Adds interactive bare invocation behavior, shared session launcher, and session loop reconnection for pause/next-take. |
| src/focus/audio/output.py | Adds volume control and pause/resume behavior to real and mock audio outputs. |
| README.md | Documents interactive launcher and playback controls. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+102
to
+110
| def _on_readable(self) -> None: | ||
| try: | ||
| data = os.read(self._fd, 6) | ||
| except (OSError, BlockingIOError): | ||
| return | ||
| if data: | ||
| self.state.handle_key(data) | ||
| if self._on_change is not None: | ||
| self._on_change() |
Comment on lines
+16
to
+33
| def _getch(fd: int | None = None) -> bytes: | ||
| """Read one keypress (or escape sequence) in cbreak mode. | ||
|
|
||
| Reads the raw fd directly (not a buffered reader): under cbreak with VMIN=1 | ||
| a single ``os.read`` returns a whole escape burst (e.g. ``\\x1b[A`` for an | ||
| arrow key) in one call, so arrow keys are not mistaken for a bare Escape. | ||
| """ | ||
| import termios | ||
| import tty | ||
|
|
||
| fd = sys.stdin.fileno() if fd is None else fd | ||
| old = termios.tcgetattr(fd) | ||
| try: | ||
| tty.setcbreak(fd) | ||
| return os.read(fd, 6) | ||
| finally: | ||
| termios.tcsetattr(fd, termios.TCSADRAIN, old) | ||
|
|
Comment on lines
+66
to
+75
| while True: | ||
| lines = _menu_lines(profiles, selected) | ||
| if prev_lines: | ||
| # Return to the top of the previous block and clear everything | ||
| # below it, so nothing from the prior frame can ghost through. | ||
| out.write(f"\x1b[{prev_lines}A") | ||
| out.write("\x1b[J") | ||
| out.write("\n".join(lines) + "\n") | ||
| out.flush() | ||
| prev_lines = len(lines) |
Comment on lines
+259
to
+265
| def pause(self) -> None: | ||
| """Pause playback by tearing down the stream and dropping the buffer. | ||
|
|
||
| The object stays alive; the next ``write()`` calls re-fill the buffer and | ||
| ``_maybe_start_stream`` recreates the stream once enough is buffered (so | ||
| resume is click-free, just like initial start). Call ``resume()`` first. | ||
| """ |
Comment on lines
+334
to
+338
| def pause(self) -> None: | ||
| self._paused = True | ||
|
|
||
| def resume(self) -> None: | ||
| self._paused = False |
Comment on lines
+16
to
+24
| from focus.audio.output import AudioOutput, MockAudioOutput | ||
| from focus.cli import main | ||
| from focus.ui import launcher | ||
| from focus.ui.transport import KeyboardController, PlaybackState, StatusLine, _format_time | ||
|
|
||
| # The pty-backed tests exercise the raw terminal readers; pty is Unix-only. | ||
| requires_pty = pytest.mark.skipif( | ||
| not hasattr(os, "openpty"), reason="pty is unavailable on this platform" | ||
| ) |
Comment on lines
+173
to
+175
| class TestAudioOutputControls: | ||
| def test_set_volume_clamps(self): | ||
| out = AudioOutput() |
Comment on lines
+194
to
+196
| ``_getch`` flushes pending input when it enters cbreak mode (so stray | ||
| keystrokes typed before a prompt are dropped), so the test must write the | ||
| key only *after* the reader is blocked in ``read`` — hence the helper thread. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Interactive terminal controls and a live status line to the Focus music generator, allowing users to control playback (pause/resume, next take, volume, help, quit) directly from the terminal. It introduces a new interactive launcher (
focuswith no arguments) for easy profile selection, improves session management, and refactors audio output to support pausing and volume adjustment. These changes enhance usability for terminal users while maintaining compatibility with scriptable and non-interactive use cases.Interactive Terminal Controls and UI:
focuswith no arguments now opens a profile picker in the terminal.Audio Output Enhancements:
AudioOutputto support pausing/resuming playback, dropping/retaining buffers as needed, and adjusting output gain (volume) in real time.Session Management and Refactoring:
Documentation Updates:
README.mdto document the new interactive launcher and terminal playback controls.