From: sbkelley Date: Sun, 30 Nov 2025 19:42:04 +0000 (-0500) Subject: Continue restructuring X-Git-Url: https://skyeroc.xyz/gitweb/?a=commitdiff_plain;h=9253d4cef3a2ad21a0dc3c3ac714049992a937ac;p=obs-ctl Continue restructuring --- diff --git a/src/obs_ctl/__init__.py b/src/obs_ctl/__init__.py index 440cccd..a4326e9 100644 --- a/src/obs_ctl/__init__.py +++ b/src/obs_ctl/__init__.py @@ -11,10 +11,11 @@ from textual.widgets import Footer, Header, Label, Placeholder, TabbedContent, T from textual.css.query import NoMatches from . import log, data -from .widget import OBSSettingsPane +from .interface import OBSSettingsPane -from .widget.element.bool_control import BoolControl -from .widget.element.text_readout import TextReadout +from .interface.element.bool_control import BoolControl +from .interface.element.text_readout import TextReadout +from .interface import RecordControls logger = log.getLogger(__name__) obs_req_client = obs.ReqClient() @@ -70,10 +71,11 @@ class OBSCtlApp(App): with ScrollableContainer(id="content_container"): with Vertical(id="blocks_v_container", classes="blocks_containers"): with Horizontal(id="blocks_h_container1", classes="blocks_h_containers"): - with Grid(id="controls_container", classes="blocks gridblocks"): - yield BoolControl(False, label="Recording", id="record_control", classes="controls") - yield BoolControl(False, label="Paused", true_label="Pause", false_label="Unpause", swap_red=True, id="pause_control", classes="controls") - yield BoolControl(False, label="Streaming", id="stream_control", classes="controls") + yield RecordControls(id="record_controls") + # with Grid(id="controls_container", classes="blocks gridblocks"): + # yield BoolControl(False, label="Recording", id="record_control", classes="controls") + # yield BoolControl(False, label="Paused", true_label="Pause", false_label="Unpause", swap_red=True, id="pause_control", classes="controls") + # yield BoolControl(False, label="Streaming", id="stream_control", classes="controls") with TabbedContent(id="settings_container", classes="blocks"): with TabPane("OBS Settings", id="settings-tab"): yield OBSSettingsPane(id="settings-content") @@ -93,9 +95,8 @@ class OBSCtlApp(App): yield TextReadout("Encdng frames dropped", "-/- (-.-%)", id="encoding_lag", classes="stats") - @on(BoolControl.SetFalse, "#stream_control") + @on(RecordControls.StreamStopped, "#record_controls",) def stream_off(self, message): - logger.debug(f"Stop message rcv'd from {message.control}") try: obs_req_client.stop_stream() except obs_error.OBSSDKRequestError as err: @@ -104,9 +105,9 @@ class OBSCtlApp(App): else: raise - @on(BoolControl.SetTrue, "#stream_control") + + @on(RecordControls.StreamStarted, "#record_controls") def stream_on(self, message): - logger.debug("Stream Started message rcv'd") try: obs_req_client.start_stream() except obs_error.OBSSDKRequestError as err: @@ -115,20 +116,9 @@ class OBSCtlApp(App): else: raise - @on(BoolControl.SetFalse, "#record_control") - def record_off(self, message): - logger.debug("Record Stopped message rcv'd") - try: - obs_req_client.stop_record() - except obs_error.OBSSDKRequestError as err: - if err.code == 501: - logger.debug("Record already stopped.") - else: - raise - @on(BoolControl.SetTrue, "#record_control") + @on(RecordControls.RecordStarted, "#record_controls") def record_on(self, message): - logger.debug("Record Started message rcv'd") try: obs_req_client.start_record() except obs_error.OBSSDKRequestError as err: @@ -138,7 +128,19 @@ class OBSCtlApp(App): raise - @on(BoolControl.SetTrue, "#pause_control") + @on(RecordControls.RecordStopped, "#record_controls") + def record_off(self, message): + logger.debug("Record Stopped message rcv'd") + try: + obs_req_client.stop_record() + except obs_error.OBSSDKRequestError as err: + if err.code == 501: + logger.debug("Record already stopped.") + else: + raise + + + @on(RecordControls.RecordPaused, "#record_controls") def pause_on(self, message): logger.debug("Record Paused message rcv'd") try: @@ -150,7 +152,7 @@ class OBSCtlApp(App): raise - @on(BoolControl.SetFalse, "#pause_control") + @on(RecordControls.RecordUnpaused, "#pause_control") def pause_off(self, message): logger.debug("Record Unpaused message rcv'd") try: diff --git a/src/obs_ctl/data/layout.tcss b/src/obs_ctl/data/layout.tcss index ef7d44e..e0dbb18 100644 --- a/src/obs_ctl/data/layout.tcss +++ b/src/obs_ctl/data/layout.tcss @@ -35,6 +35,9 @@ min-height:8; } +RecordControls { +} + BoolControl { background:$surface; border:blank; diff --git a/src/obs_ctl/interface/__init__.py b/src/obs_ctl/interface/__init__.py new file mode 100644 index 0000000..2015550 --- /dev/null +++ b/src/obs_ctl/interface/__init__.py @@ -0,0 +1,56 @@ +from textual.app import App, ComposeResult +from textual.containers import Horizontal, VerticalScroll, Grid, Container +from textual.message import Message +from textual.reactive import reactive +from textual.widgets import Label, RadioButton, Button, Input +from textual.widget import Widget +from textual.css import query + +from .message import MSGBase +from .molecule import RecordControls +from .. import log + + + +logger = log.getLogger(__name__) + +class OBSSettingsPane(VerticalScroll): + DEFAULT_CSS = """ + .obssettingspane--input { + width:1fr; + } + Grid { + grid-size:2 1; + grid-columns:21 1fr; + Label { + width:1fr; + text-align:right; + padding-right:1; + } + } + """ + COMPONENT_CLASSES = { + "obssettingspane--input", + } + record_path: reactive[str] = reactive("") + class RecordPathChanged(Message): + control: "OBSSettingsPane" = None + record_path: str = None + def __init__(self, ctrl, path): + super().__init__() + self.control = ctrl + self.record_path = path + + def compose(self): + with Grid(): + yield Label("Record path:") + yield Input("path", id="input_record_path", classes="obssettingspane--input", compact=True) + + def on_input_submitted(self, event:Input.Submitted): + self.set_reactive(OBSSettingsPane.record_path, event.value) + if event.control.id == "input_record_path": + self.post_message(OBSSettingsPane.RecordPathChanged(self, event.value)) + + def watch_record_path(self, value): + input: Input = self.query_exactly_one("#input_record_path") + input.value = value diff --git a/src/obs_ctl/interface/element/__init__.py b/src/obs_ctl/interface/element/__init__.py new file mode 100644 index 0000000..99d7b11 --- /dev/null +++ b/src/obs_ctl/interface/element/__init__.py @@ -0,0 +1,2 @@ +from .bool_control import BoolControl +from .text_readout import TextReadout \ No newline at end of file diff --git a/src/obs_ctl/interface/element/bool_control.py b/src/obs_ctl/interface/element/bool_control.py new file mode 100644 index 0000000..a6b2d71 --- /dev/null +++ b/src/obs_ctl/interface/element/bool_control.py @@ -0,0 +1,167 @@ +from textual import on +from textual.app import ComposeResult +from textual.containers import Container +from textual.css import query +from textual.message import Message +from textual.reactive import reactive +from textual.widgets import Button, RadioButton + +from ...log import getLogger + +logger = getLogger(__name__) + +class BoolControl(Container): + """BoolControl manages a boolean that belongs to something else. + + In other words, it can display true or false and this can be set + on it directly, but its buttons do nothing other than post a + message. Designed as i/o for an API""" + DEFAULT_CSS = """ + .bool_control--light { + background:$panel; + text-align:center; + align:center bottom; + width:1fr; + height:1; + } + .bool_control--true_button { + width:1fr; + height:1fr; + } + .bool_control--false_button { + width:1fr; + height:1fr; + } + """ + COMPONENT_CLASSES = { + "bool_control--light", + "bool_control--true_button", + "bool_control--false_button", + } + value: reactive[bool] = reactive(False) + label: reactive[str] = reactive("Status") + true_label: reactive[str] = reactive("Start") + false_label: reactive[str] = reactive("Stop") + swap_red: reactive[bool] = reactive(False) + compact: reactive[bool] = reactive(True) + control_layout: reactive[str] = reactive("vertical") + + class MsgBase(Message): + control: "BoolControl" = None + def __init__(self, control: "BoolControl"): + super().__init__() + self.control = control + class SetTrue(MsgBase): ... + class SetFalse(MsgBase): ... + + + def __init__( + self, + value: bool, + label: str = "Boolean", + true_label: str = "Start", + false_label: str = "Stop", + swap_red: bool = False, + compact: bool = True, + *children, + layout = "vertical", + name = None, + id = None, + classes = None, + disabled = False, + markup = True): + super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + self.set_reactive(BoolControl.value, value) + self.set_reactive(BoolControl.label, label) + self.set_reactive(BoolControl.true_label, true_label) + self.set_reactive(BoolControl.false_label, false_label) + self.set_reactive(BoolControl.swap_red, swap_red) + self.set_reactive(BoolControl.compact, compact) + self.set_reactive(BoolControl.control_layout, layout) + + def compose(self) -> ComposeResult: + with Container(classes="bool_control--light"): + yield RadioButton( + label="", + value=False, + button_first=False, + id="light", + compact=True, + disabled=True, + classes="bool_control--light", + ) + yield Button( + label="Start", + id="true", + variant="success", + compact=True, + classes="bool_control--true_button" + ) + yield Button( + label="Stop", + id="false", + variant="error", + compact=True, + classes = "bool_control--false_button" + ) + + + def _on_mount(self, event): + # super()._on_mount(event) + self.watch_value(self.value) + self.watch_label(self.label) + self.watch_true_label(self.true_label) + self.watch_false_label(self.false_label) + self.watch_swap_red(self.swap_red) + self.watch_compact(self.compact) + self.watch_control_layout(self.control_layout) + + + @on(Button.Pressed, "#false") + def false_pressed(self, event: Button.Pressed): + self.post_message(self.SetFalse(self)) + + + @on(Button.Pressed, "#true") + def true_pressed(self): + self.post_message(self.SetTrue(self)) + + + def watch_value(self, value: bool) -> None: + light: RadioButton = self.query_exactly_one("#light") + light.value = value + + + def watch_label(self, value): + light: RadioButton = self.query_exactly_one("#light") + light.label = value + + + def watch_true_label(self, value): + true: Button = self.query_exactly_one("#true") + true.label = value + + + def watch_false_label(self, value): + false: Button = self.query_exactly_one("#false") + false.label = value + + + def watch_swap_red(self, value): + true: Button = self.query_exactly_one("#true") + false: Button = self.query_exactly_one("#false") + false.variant = "success" if value else "error" + true.variant = "error" if value else "success" + + + def watch_compact(self, value): + light: RadioButton = self.query_exactly_one("#light") + true: Button = self.query_exactly_one("#true") + false: Button = self.query_exactly_one("#false") + light.compact = value + true.compact = value + false.compact = value + + + def watch_control_layout(self, value): + self.set_styles(layout=value) \ No newline at end of file diff --git a/src/obs_ctl/interface/element/text_readout.py b/src/obs_ctl/interface/element/text_readout.py new file mode 100644 index 0000000..55ede6b --- /dev/null +++ b/src/obs_ctl/interface/element/text_readout.py @@ -0,0 +1,36 @@ +from textual.containers import Container +from textual.reactive import reactive +from textual.widgets import Label + +class TextReadout(Container): + COMPONENT_CLASSES = { + "text_readout--label", + "text_readout--text", + } + text = reactive("Text") + label = reactive("Label") + readout_layout = reactive("vertical") + + def __init__(self, label, text, *children, layout = "vertical", name = None, id = None, classes = None, disabled = False, markup = True): + super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + self.set_reactive(TextReadout.label, label) + self.set_reactive(TextReadout.text, text) + self.set_reactive(TextReadout.readout_layout, layout) + + + def compose(self): + yield Container(Label(self.label, id="label"), classes="text_readout--label") + yield Container(Label(self.text, id="text"), classes="text_readout--text") + + def watch_text(self, value: str): + text: Label = self.query_exactly_one("#text") + text.update(value) + + + def watch_label(self, value): + label: Label = self.query_exactly_one("#label") + label.update(value) + + + def watch_readout_layout(self, value): + self.set_styles(layout=value) \ No newline at end of file diff --git a/src/obs_ctl/interface/message.py b/src/obs_ctl/interface/message.py new file mode 100644 index 0000000..650e06b --- /dev/null +++ b/src/obs_ctl/interface/message.py @@ -0,0 +1,8 @@ +from textual.message import Message +from textual.widget import Widget + +class MSGBase(Message): + control: Widget = None + def __init__(self, control): + self.control = control + super().__init__() \ No newline at end of file diff --git a/src/obs_ctl/interface/molecule/__init__.py b/src/obs_ctl/interface/molecule/__init__.py new file mode 100644 index 0000000..ace480d --- /dev/null +++ b/src/obs_ctl/interface/molecule/__init__.py @@ -0,0 +1 @@ +from .record_controls import RecordControls \ No newline at end of file diff --git a/src/obs_ctl/interface/molecule/record_controls.py b/src/obs_ctl/interface/molecule/record_controls.py new file mode 100644 index 0000000..18e1450 --- /dev/null +++ b/src/obs_ctl/interface/molecule/record_controls.py @@ -0,0 +1,72 @@ +from textual.containers import ScrollableContainer, Grid +from textual.message import Message +from textual.reactive import reactive + +from .. import MSGBase +from ..element import BoolControl +from ...log import getLogger + +logger=getLogger(__name__) +class RecordControls(ScrollableContainer): + DEFAULT_CSS = """ + Grid { + grid-size:2; + grid-rows:5; + height:10; + } + """ + + recording: reactive[bool] = False + paused: reactive[bool] = False + streaming: reactive[bool] = False + + class RecordStarted(MSGBase): ... + class RecordStopped(MSGBase): ... + class RecordPaused(MSGBase): ... + class RecordUnpaused(MSGBase): ... + class StreamStarted(MSGBase): ... + class StreamStopped(MSGBase): ... + + def compose(self): + with Grid(): + yield BoolControl(False, label="Record", id="record_control") + yield BoolControl( + False, + label="Pause", + true_label="Pause", + false_label="Unpause", + swap_red=True, + id="pause_control" + ) + yield BoolControl(False, label="Stream", id="stream_control") + + def on_bool_control_set_true(self, message: BoolControl.SetTrue): + match message.control.id: + case "record_control": + self.post_message(RecordControls.RecordStarted(self)) + case "pause_control": + self.post_message(RecordControls.RecordPaused(self)) + case "stream_control": + self.post_message(RecordControls.StreamStarted(self)) + + + def on_bool_control_set_false(self, message: BoolControl.SetFalse): + match message.control.id: + case "record_control": + self.post_message(RecordControls.RecordStopped(self)) + case "pause_control": + self.post_message(RecordControls.RecordUnpaused(self)) + case "stream_control": + self.post_message(RecordControls.StreamStopped(self)) + + def watch_recording(self, value): + control: BoolControl = self.query_exactly_one("#record_control") + control.value = value + + def watch_paused(self, value): + control: BoolControl = self.query_exactly_one("#pause_control") + control.value = value + + def watch_streaming(self, value): + control: BoolControl = self.query_exactly_one("#stream_control") + control.value = value \ No newline at end of file diff --git a/src/obs_ctl/widget/__init__.py b/src/obs_ctl/widget/__init__.py deleted file mode 100644 index 6d1f0e0..0000000 --- a/src/obs_ctl/widget/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from textual.app import App, ComposeResult -from textual.containers import Horizontal, VerticalScroll, Grid, Container -from textual.message import Message -from textual.reactive import reactive -from textual.widgets import Label, RadioButton, Button, Input -from textual.widget import Widget -from textual.css import query - - -from .. import log - -logger = log.getLogger(__name__) - -class OBSSettingsPane(VerticalScroll): - DEFAULT_CSS = """ - .obssettingspane--input { - width:1fr; - } - Grid { - grid-size:2 1; - grid-columns:21 1fr; - Label { - width:1fr; - text-align:right; - padding-right:1; - } - } - """ - COMPONENT_CLASSES = { - "obssettingspane--input", - } - record_path: reactive[str] = reactive("") - class RecordPathChanged(Message): - control: "OBSSettingsPane" = None - record_path: str = None - def __init__(self, ctrl, path): - super().__init__() - self.control = ctrl - self.record_path = path - - def compose(self): - with Grid(): - yield Label("Record path:") - yield Input("path", id="input_record_path", classes="obssettingspane--input", compact=True) - - def on_input_submitted(self, event:Input.Submitted): - self.set_reactive(OBSSettingsPane.record_path, event.value) - if event.control.id == "input_record_path": - self.post_message(OBSSettingsPane.RecordPathChanged(self, event.value)) - - def watch_record_path(self, value): - input: Input = self.query_exactly_one("#input_record_path") - input.value = value diff --git a/src/obs_ctl/widget/element/bool_control.py b/src/obs_ctl/widget/element/bool_control.py deleted file mode 100644 index a6b2d71..0000000 --- a/src/obs_ctl/widget/element/bool_control.py +++ /dev/null @@ -1,167 +0,0 @@ -from textual import on -from textual.app import ComposeResult -from textual.containers import Container -from textual.css import query -from textual.message import Message -from textual.reactive import reactive -from textual.widgets import Button, RadioButton - -from ...log import getLogger - -logger = getLogger(__name__) - -class BoolControl(Container): - """BoolControl manages a boolean that belongs to something else. - - In other words, it can display true or false and this can be set - on it directly, but its buttons do nothing other than post a - message. Designed as i/o for an API""" - DEFAULT_CSS = """ - .bool_control--light { - background:$panel; - text-align:center; - align:center bottom; - width:1fr; - height:1; - } - .bool_control--true_button { - width:1fr; - height:1fr; - } - .bool_control--false_button { - width:1fr; - height:1fr; - } - """ - COMPONENT_CLASSES = { - "bool_control--light", - "bool_control--true_button", - "bool_control--false_button", - } - value: reactive[bool] = reactive(False) - label: reactive[str] = reactive("Status") - true_label: reactive[str] = reactive("Start") - false_label: reactive[str] = reactive("Stop") - swap_red: reactive[bool] = reactive(False) - compact: reactive[bool] = reactive(True) - control_layout: reactive[str] = reactive("vertical") - - class MsgBase(Message): - control: "BoolControl" = None - def __init__(self, control: "BoolControl"): - super().__init__() - self.control = control - class SetTrue(MsgBase): ... - class SetFalse(MsgBase): ... - - - def __init__( - self, - value: bool, - label: str = "Boolean", - true_label: str = "Start", - false_label: str = "Stop", - swap_red: bool = False, - compact: bool = True, - *children, - layout = "vertical", - name = None, - id = None, - classes = None, - disabled = False, - markup = True): - super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) - self.set_reactive(BoolControl.value, value) - self.set_reactive(BoolControl.label, label) - self.set_reactive(BoolControl.true_label, true_label) - self.set_reactive(BoolControl.false_label, false_label) - self.set_reactive(BoolControl.swap_red, swap_red) - self.set_reactive(BoolControl.compact, compact) - self.set_reactive(BoolControl.control_layout, layout) - - def compose(self) -> ComposeResult: - with Container(classes="bool_control--light"): - yield RadioButton( - label="", - value=False, - button_first=False, - id="light", - compact=True, - disabled=True, - classes="bool_control--light", - ) - yield Button( - label="Start", - id="true", - variant="success", - compact=True, - classes="bool_control--true_button" - ) - yield Button( - label="Stop", - id="false", - variant="error", - compact=True, - classes = "bool_control--false_button" - ) - - - def _on_mount(self, event): - # super()._on_mount(event) - self.watch_value(self.value) - self.watch_label(self.label) - self.watch_true_label(self.true_label) - self.watch_false_label(self.false_label) - self.watch_swap_red(self.swap_red) - self.watch_compact(self.compact) - self.watch_control_layout(self.control_layout) - - - @on(Button.Pressed, "#false") - def false_pressed(self, event: Button.Pressed): - self.post_message(self.SetFalse(self)) - - - @on(Button.Pressed, "#true") - def true_pressed(self): - self.post_message(self.SetTrue(self)) - - - def watch_value(self, value: bool) -> None: - light: RadioButton = self.query_exactly_one("#light") - light.value = value - - - def watch_label(self, value): - light: RadioButton = self.query_exactly_one("#light") - light.label = value - - - def watch_true_label(self, value): - true: Button = self.query_exactly_one("#true") - true.label = value - - - def watch_false_label(self, value): - false: Button = self.query_exactly_one("#false") - false.label = value - - - def watch_swap_red(self, value): - true: Button = self.query_exactly_one("#true") - false: Button = self.query_exactly_one("#false") - false.variant = "success" if value else "error" - true.variant = "error" if value else "success" - - - def watch_compact(self, value): - light: RadioButton = self.query_exactly_one("#light") - true: Button = self.query_exactly_one("#true") - false: Button = self.query_exactly_one("#false") - light.compact = value - true.compact = value - false.compact = value - - - def watch_control_layout(self, value): - self.set_styles(layout=value) \ No newline at end of file diff --git a/src/obs_ctl/widget/element/text_readout.py b/src/obs_ctl/widget/element/text_readout.py deleted file mode 100644 index 55ede6b..0000000 --- a/src/obs_ctl/widget/element/text_readout.py +++ /dev/null @@ -1,36 +0,0 @@ -from textual.containers import Container -from textual.reactive import reactive -from textual.widgets import Label - -class TextReadout(Container): - COMPONENT_CLASSES = { - "text_readout--label", - "text_readout--text", - } - text = reactive("Text") - label = reactive("Label") - readout_layout = reactive("vertical") - - def __init__(self, label, text, *children, layout = "vertical", name = None, id = None, classes = None, disabled = False, markup = True): - super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) - self.set_reactive(TextReadout.label, label) - self.set_reactive(TextReadout.text, text) - self.set_reactive(TextReadout.readout_layout, layout) - - - def compose(self): - yield Container(Label(self.label, id="label"), classes="text_readout--label") - yield Container(Label(self.text, id="text"), classes="text_readout--text") - - def watch_text(self, value: str): - text: Label = self.query_exactly_one("#text") - text.update(value) - - - def watch_label(self, value): - label: Label = self.query_exactly_one("#label") - label.update(value) - - - def watch_readout_layout(self, value): - self.set_styles(layout=value) \ No newline at end of file