Skip to content

feat: interactive terminal controls - #7

Merged
cubny merged 6 commits into
mainfrom
feat/improve-tui
Jun 20, 2026
Merged

feat: interactive terminal controls #7
cubny merged 6 commits into
mainfrom
feat/improve-tui

Conversation

@cubny

@cubny cubny commented Jun 18, 2026

Copy link
Copy Markdown
Owner

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 (focus with 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:

  • Added interactive playback controls (pause/resume, next take, volume up/down, help, quit) when running in a terminal, with a live status line reflecting playback state.
  • Implemented a new interactive launcher: running focus with no arguments now opens a profile picker in the terminal.

Audio Output Enhancements:

  • Refactored AudioOutput to support pausing/resuming playback, dropping/retaining buffers as needed, and adjusting output gain (volume) in real time.

Session Management and Refactoring:

  • Refactored session management to support reconnecting to the generator (for next take or pause/resume) and to apply interactive controls cleanly, including proper teardown and resource management.
  • Extracted and unified session launching logic for use by both the CLI and the interactive launcher.

Documentation Updates:

  • Updated README.md to document the new interactive launcher and terminal playback controls.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 live StatusLine.
  • Adds a bare focus interactive launcher (profile picker) and routes both focus start and launcher through a shared launch_session().
  • Extends AudioOutput/MockAudioOutput with 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 thread src/focus/ui/transport.py
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 thread src/focus/ui/launcher.py
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 thread src/focus/ui/launcher.py
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 thread src/focus/ui/launcher.py
Comment thread src/focus/cli.py Outdated
Comment thread src/focus/audio/output.py
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 thread src/focus/audio/output.py
Comment on lines +334 to +338
def pause(self) -> None:
self._paused = True

def resume(self) -> None:
self._paused = False
Comment thread tests/test_ui.py Outdated
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 thread tests/test_ui.py
Comment on lines +173 to +175
class TestAudioOutputControls:
def test_set_volume_clamps(self):
out = AudioOutput()
Comment thread tests/test_ui.py Outdated
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.
Copilot finished work on behalf of cubny June 18, 2026 19:52
@cubny
cubny merged commit 73ffd61 into main Jun 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants