Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/toad/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from toad.widgets.comms_transfer import Transfers
import asyncio
import signal
import traceback
import gc
import sys
import json
Expand Down Expand Up @@ -294,7 +293,7 @@ def _handle_exception(self, error: Exception) -> bool:
"""The mounted application owns escaped handler and worker errors."""
if not self.screen_stack or not self.screen.is_mounted:
return super()._handle_exception(error)
self.show_error("".join(traceback.format_exception(error)), type(error).__name__)
self.show_error(error, type(error).__name__)
return True

async def on_load(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/toad/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def persist_terminal_failure(error: Exception) -> None:
from toad import paths

try:
path = paths.save_traceback(error, "Terminal_crash")
path = paths.save_error(error, "Terminal_crash")
except OSError as log_error:
click.echo(f"Unable to save terminal traceback: {log_error}", err=True)
else:
Expand Down
21 changes: 18 additions & 3 deletions src/toad/forms/editors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from python_introspect.validation import validate_annotation_value
from textual import containers
from textual.app import ComposeResult
from textual.message import Message
from textual.reactive import var
from textual.validation import Function
from textual.geometry import Spacing
from textual.css.scalar import Scalar
Expand All @@ -38,15 +40,23 @@
class ValueEditor(DeclaredViews, EditorWidget):
"""A control that shows one field's value and sends the user's edits to the state.

The value lives in the ObjectState: the control is painted from it and
keeps nothing else. The editor capability binds the field before the native
The value lives in the ObjectState: the control is painted from it. Only
transient validation feedback lives here. The editor binds the field before the native
control is initialized: `class BoolEditor(ValueEditor, Checkbox)`.
"""

states_default: ClassVar[bool] = True
"""Whether the field's help should end with its default value."""

field: DeclaredField
feedback: var[str] = var("", init=False)

class Feedback(Message):
"""Transient validation text for the field's own view, never a crash log."""

def __init__(self, text: str) -> None:
super().__init__()
self.text = text

def __init__(self, field: DeclaredField) -> None:
self.field = field
Expand Down Expand Up @@ -81,11 +91,16 @@ def send(self, value: object) -> None:
self.field.edit(value)
except (TypeError, ValueError) as error:
self.refuse(error)
else:
self.feedback = ""

def refuse(self, error: Exception) -> None:
self.app.show_error(error, self.field.label)
self.feedback = str(error)
self.paint()

def watch_feedback(self, text: str) -> None:
self.post_message(self.Feedback(text))


class BoolEditor(ValueEditor, FormCheckbox):
shows = (bool,)
Expand Down
19 changes: 17 additions & 2 deletions src/toad/forms/form_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
from formbridge.forms.parameter_form_service import ParameterFormService
from formbridge.forms.parameter_info_types import DirectDataclassInfo, ParameterInfo
from objectstate import DottedFieldPath, ObjectState
from textual import containers, events, lazy
from textual import containers, events, lazy, on
from textual.app import ComposeResult
from textual.content import Content
from textual.widgets import Static
from textual.css._style_properties import Edges
from textual.geometry import Spacing
from textual.layouts.stream import StreamLayout
from rich.style import Style
from ui_theme.scheme import BackgroundColour, ForegroundColour, MutedColour, PrimaryColour, SecondaryColour
from ui_theme.scheme import BackgroundColour, ErrorColour, ForegroundColour, MutedColour, PrimaryColour, SecondaryColour

from toad.forms.declared_views import DeclaredField, DeclaredViews
from toad.forms.editors import ValueEditor
Expand All @@ -37,6 +37,12 @@ def paint_foreground(self):
return self.app.colours.colour(MutedColour)


class FieldRefusal(FieldHelp):
@property
def paint_foreground(self):
return self.app.colours.colour(ErrorColour)


class GroupTitle(WidgetView, Static):
padding = Spacing(0, 0, 0, 1)

Expand Down Expand Up @@ -93,6 +99,8 @@ def __init__(self, field: DeclaredField,
super().__init__(field, included)
self.editor = ValueEditor.declared_for(field)
self.title = FieldTitle(field.label)
self.refusal = FieldRefusal("", markup=False)
self.refusal.display = False
self.search_text = " ".join(
DeclaredField(field.state, step).label
for step in field.state.parameter_structure.ancestors(field.path)
Expand All @@ -102,6 +110,13 @@ def compose(self) -> ComposeResult:
yield self.title
yield FieldHelp(self.description())
yield self.editor
yield self.refusal

@on(ValueEditor.Feedback)
def show_feedback(self, message: ValueEditor.Feedback) -> None:
message.stop()
self.refusal.update(message.text)
self.refusal.display = bool(message.text)

def description(self) -> Content:
help = Content.from_markup(self.field.help)
Expand Down
6 changes: 3 additions & 3 deletions src/toad/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ def get_project_data(project_path: Checkout) -> Path:
return project_data_path


def save_traceback(error: BaseException, prefix: str) -> Path:
"""Write ``error``'s traceback to a new file in the log directory."""
def save_error(error: BaseException, prefix: str) -> Path:
"""Persist the exception's traceback and chained causes."""
import traceback
from toad.acp.agent import generate_datetime_filename

path = get_log() / generate_datetime_filename(prefix, ".txt")
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, "w") as stream:
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
traceback.print_exception(error, file=stream)
return path

Expand Down
Loading
Loading