The screen, minimal: no status column, no counters, five keys, short words

The owner's brief: people do not want everything full. The list shows a
score, a company, a role, a place, and one sign at most in front: a dot
for selected, a tick for sent; what else happened to a job is in its
details, on Enter, which now also opens a tried job's page and proof.
The heading says what is looked for and nothing else; the counters
"reading", "scoring 3/25" and "N selected" are gone, the scores filling
the list are the progress. Footer: Select, Apply, More, Settings, Quit;
Enter, O and S still work, unshown. The log is four lines and its
sentences a third of their length; every question is one bold line with
its explanation muted under it. Settings put what you change first and
the four models last. The selection mark, double-encoded since it was
committed, is a real dot again.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Federico EliaandClaude Fable 5.1 committed 2026-11-07 08:52:42 +01:00
1 parent f0a41a3a94
commit 479c7aaf17
2 files changed
+81 -112

No files matched your search

+66 -97
View File
@@ -25,23 +25,15 @@ from aihawk.store import SUBMITTED, Sent, Stored, said, sent_word
# Below this width there is no room for the location beside the company and the role.
WIDE_ENOUGH = 100
SEARCH_SETTINGS = {"titles", "places", "exclude", "max_age_days"}
RESUME_QUESTION = "Where is your resume? PDF, .docx or text."
MAIL_QUESTION = "The email you apply with. Job sites send their codes there, and AIHawk reads only those."
RESUME_QUESTION = "Where is your resume?"
MAIL_QUESTION = "The email you apply with."
MAIL_HINT = "Job sites send their codes there, and AIHawk reads only those · Enter · Esc for later"
KEY_QUESTION = "Your OrcaRouter key."
KEY_HINT = f"Get one at {KEY_URL} · it stays on this computer · Enter saves it"
# What the mail plugins make the model say when the mailbox shows its sign-in page: the application is then run again after a sign-in.
MAIL_NOT_SIGNED_IN = "The mailbox is not signed in."
MAIL_NAMES = {"gmail": "Gmail", "outlook": "Outlook"}
RESUME_HINT = "Enter reads it · Esc for later · Ctrl+C quits"
STATUS_TEXT = {
"new": "",
"scored": "",
"shortlisted": "",
"passed_over": "passed over",
"submitted": "sent",
"unconfirmed": "not confirmed",
"refused": "refused",
"blocked": "blocked",
"failed": "failed",
}
RESUME_HINT = "PDF, .docx or text · Enter reads it · Esc for later"
class TextPrompt(ModalScreen[str | None]):
@@ -49,7 +41,7 @@ class TextPrompt(ModalScreen[str | None]):
BINDINGS: ClassVar = [Binding("escape", "skip", "Skip")]
def __init__(self, question: str, *, placeholder: str = "", password: bool = False, hint: str = "Enter answers · Esc goes back · Ctrl+C quits", value: str | None = None) -> None:
def __init__(self, question: str, *, placeholder: str = "", password: bool = False, hint: str = "Enter · Esc goes back", value: str | None = None) -> None:
super().__init__()
# With a value to change, Enter on an empty line clears it, and only Esc leaves it as it is.
self.value = value
@@ -86,7 +78,7 @@ class SendBatch(ModalScreen[bool]):
lines = [f"{index}. {item.job.company}: {item.job.title}" for index, item in enumerate(self.items, 1)]
with Vertical(id="dialog"):
yield Static(f"Send {count} application{'' if count == 1 else 's'} now?", id="question", markup=False)
yield Static("In your name, in the browser window. Enter sends; Esc cancels; Ctrl+C quits.", classes="hint")
yield Static("In your name, in the browser window · Enter sends · Esc cancels", classes="hint")
with VerticalScroll(id="summary"):
yield Static("\n".join(lines), markup=False)
yield Footer()
@@ -117,7 +109,7 @@ class SentList(ModalScreen[None]):
with Vertical(id="wide"):
yield Static("Applications", id="question", markup=False)
yield DataTable(id="sent", cursor_type="row")
yield Static("Enter opens a job in full · Esc goes back", classes="hint")
yield Static("Enter opens one · Esc goes back", classes="hint")
yield Footer()
def on_mount(self) -> None:
@@ -192,6 +184,8 @@ class JobDetails(ModalScreen[None]):
lines += [f"Score {report.get('score', 0):.1f}/5 · posting looks {report.get('legitimacy', '?')}", answers, "", report.get("summary", "")]
if report.get("gaps"):
lines += ["", "Gaps:", *(f" - {gap}" for gap in report["gaps"])]
if not job.description.strip():
lines += ["", "Scored from the title alone: the posting had no text to read."]
if self.item.note:
lines += ["", "Last attempt:", self.item.note]
with Vertical(id="dialog"):
@@ -250,14 +244,15 @@ class SignIn(ModalScreen[None]):
BINDINGS: ClassVar = [Binding("enter,escape", "done", "Done")]
def __init__(self, text: str = "The browser window is open. Sign in there to the job site you need, then come back.") -> None:
def __init__(self, text: str = "The browser is open. Sign in there to the job site you need.", hint: str = "Enter here when you are done · the window closes") -> None:
super().__init__()
self.text = text
self.hint = hint
def compose(self) -> ComposeResult:
with Vertical(id="dialog"):
yield Static(self.text, id="question", markup=False)
yield Static("Press Enter here when you are done. The window closes then. Ctrl+C quits.", classes="hint")
yield Static(self.hint, classes="hint")
def action_done(self) -> None:
self.dismiss(None)
@@ -270,7 +265,7 @@ class AIHawk(App[None]):
CSS = """
#heading { height: 1; padding: 0 1; background: $primary; color: $text; text-style: bold; }
#jobs { height: 1fr; }
#log { height: 7; border-top: solid $primary; padding: 0 1; scrollbar-size-vertical: 1; }
#log { height: 4; border-top: solid $primary; padding: 0 1; scrollbar-size-vertical: 1; }
ModalScreen { align: center middle; background: $background 60%; }
#dialog { width: 80%; max-width: 100; height: auto; max-height: 85%; padding: 1 2; background: $surface; border: thick $primary; }
#wide { width: 95%; height: auto; max-height: 85%; padding: 1 2; background: $surface; border: thick $primary; }
@@ -281,10 +276,10 @@ class AIHawk(App[None]):
"""
BINDINGS: ClassVar = [
Binding("space", "toggle", "Select"),
Binding("enter", "details", "Details"),
Binding("enter", "details", show=False),
Binding("a", "apply", "Apply"),
Binding("s", "sent", "Sent"),
Binding("o", "open_link", "Open"),
Binding("s", "sent", show=False),
Binding("o", "open_link", show=False),
Binding("m", "more", "More"),
Binding("e", "settings", "Settings"),
Binding("r", "new_resume", show=False),
@@ -330,9 +325,9 @@ class AIHawk(App[None]):
table.clear(columns=True)
self.with_location = width >= WIDE_ENOUGH
# A column's padding is 2, and the scrollbar takes 2.
left = width - 2 - 2 * (6 if self.with_location else 5) - 2 - 5 - 14
left = width - 2 - 2 * (5 if self.with_location else 4) - 2 - 5
words = [("Company", left // 4), ("Role", left // 2), ("Location", left // 4)] if self.with_location else [("Company", left // 3), ("Role", left - left // 3)]
for label, size in (("", 2), ("Score", 5), *words, ("Status", 14)):
for label, size in (("", 2), ("Score", 5), *words):
table.add_column(label, key=label or "mark", width=size)
self.refresh_jobs(current)
@@ -353,27 +348,22 @@ class AIHawk(App[None]):
if self.session.needs_key():
self.start()
def show_heading(self, progress: str = "") -> None:
def show_heading(self) -> None:
# A flow that ends while the app quits finds its screen gone: there is no heading left to write to.
if not self.is_running:
return
config = self.session.config
wanted = config.filters.titles() or "no resume yet"
place = ", ".join(config.filters.locations[:3])
chosen = f"{len(self.selected)} selected" if self.selected else ""
left = " · ".join(part for part in (config.name, wanted, place, chosen) if part)
self.query_one("#heading", Static).update(f"AIHawk {left}" + (f" [{progress}]" if progress else ""))
self.query_one("#heading", Static).update(" · ".join(part for part in ("AIHawk", wanted, place) if part))
def row(self, item: Stored) -> tuple[Text | str, ...]:
mark = "●" if item.job.id in self.selected else ""
# One sign, or none: selected, or sent. What else happened to a job is in its details, on Enter.
mark = "●" if item.job.id in self.selected else "✓" if item.status in SUBMITTED else ""
score = f"{item.score:.1f}" if item.score is not None else "-"
status = STATUS_TEXT.get(item.status, item.status)
# A score from the title and the place alone: the person should know the posting's text was not read.
if item.score is not None and not item.job.description.strip():
status = ", ".join(part for part in (status, "title only") if part)
# Text, because a string cell is read as markup, and a posting's words are a source's: "[/x] Engineer" would stop the screen.
place = (Text(item.job.location),) if self.with_location else ()
return (Text(mark, style="bold green"), score, Text(item.job.company), Text(item.job.title), *place, status)
return (Text(mark, style="bold green"), score, Text(item.job.company), Text(item.job.title), *place)
def refresh_jobs(self, current: str | None = None) -> None:
"""The list, best first. The cursor stays on the job it was on."""
@@ -403,11 +393,9 @@ class AIHawk(App[None]):
session = self.session
try:
if session.needs_key():
key = await self.push_screen_wait(
TextPrompt(f"Your OrcaRouter key. Get one at {KEY_URL} ; it stays on this computer.", placeholder="sk-orca-...", password=True, hint="Enter saves it · Esc stops here · Ctrl+C quits")
)
key = await self.push_screen_wait(TextPrompt(KEY_QUESTION, placeholder="sk-orca-...", password=True, hint=KEY_HINT))
if not key:
self.say("AIHawk needs a model key to read your resume and judge jobs. Quit and start again when you have one.")
self.say("Without a key AIHawk cannot read. Press e when you have one.")
return
session.save_key(key)
if self.resume or session.needs_resume():
@@ -415,22 +403,19 @@ class AIHawk(App[None]):
while path is None:
answer = await self.push_screen_wait(TextPrompt(RESUME_QUESTION, placeholder=str(Path.home() / "resume.pdf"), hint=RESUME_HINT))
if not answer:
self.say("Press e when you have a resume to give me.")
self.say("Press e when you have a resume.")
return
path = Path(answer.strip("\"' ")).expanduser()
if not path.is_file():
self.say(f"No file at {path}.")
path = None
self.resume = None
self.say(f"Reading {path.name}...")
self.show_heading("reading your resume")
self.say(f"Reading {path.name}.")
await session.load_resume(path)
self.show_heading()
self.say(f"Looking for: {session.config.filters.titles()}")
self.say(f"In: {', '.join(session.config.filters.locations)}")
self.say(f"Looking for {session.config.filters.titles()} in {', '.join(session.config.filters.locations[:3])}.")
if not session.mail()[1]:
await self.set_up_mail()
self.show_heading("searching")
await session.find(self.say)
self.refresh_jobs()
await self.judge()
@@ -443,24 +428,18 @@ class AIHawk(App[None]):
async def judge(self) -> None:
session = self.session
await session.funnel(self.say, self.show_heading)
if session.store.with_status("new"):
self.say("Reading the best postings; a minute or two.")
# The counters the session offers are not shown: the scores filling the list are the progress.
await session.funnel(self.say, lambda _: None)
self.refresh_jobs()
items = session.store.with_status("shortlisted")
if not items:
self.say("Every posting found so far has been read.")
return
done = 0
def scored() -> None:
nonlocal done
done += 1
self.show_heading(f"scoring {done}/{len(items)}")
self.refresh_jobs()
self.show_heading(f"scoring 0/{len(items)}")
self.say(f"Scoring the {len(items)} that look best.")
await session.score(items, scored, self.say)
self.say("Done. Enter shows why, space selects, a sends the selected, m scores the next ones.")
self.say(f"Scoring {len(items)}.")
await session.score(items, self.refresh_jobs, self.say)
self.say("Done.")
# The keys.
@@ -472,7 +451,6 @@ class AIHawk(App[None]):
self.selected.discard(item.job.id)
else:
self.selected.add(item.job.id)
self.show_heading()
self.query_one(DataTable).update_cell(item.job.id, "mark", self.row(item)[0])
self.query_one(DataTable).action_cursor_down()
@@ -481,9 +459,12 @@ class AIHawk(App[None]):
self.action_details()
def action_details(self) -> None:
"""The job under the cursor: what was sent and what the page said when it was tried, otherwise why it is listed."""
item = self.current()
if item:
self.push_screen(JobDetails(item))
if not item:
return
tried = next((sent for sent in self.session.store.sent() if sent.job.id == item.job.id), None)
self.push_screen(SentDetails(tried) if tried else JobDetails(item))
def action_open_link(self) -> None:
item = self.current()
@@ -507,25 +488,25 @@ class AIHawk(App[None]):
if sent:
self.push_screen(SentList(sent))
else:
self.say("Nothing has been sent yet. Space selects a job, a sends it.")
self.say("Nothing sent yet.")
def setting_rows(self) -> list[tuple[str, str, str]]:
config = self.session.config
titles = [form.text for form in config.filters.plan.nodes[0].forms] if config.filters.plan.nodes else list(config.filters.include)
return [
("resume", "Resume", "loaded" if config.cv.is_file() else "none yet"),
("mail", "Your mail", self.mail_words()),
("sign_in", "Sign in to a job site", "the browser opens, you sign in there, Enter here"),
("key", "OrcaRouter key", f"from {KEY_ENV}" if config.key_from_environment() else "saved" if config.api_key() else "none yet"),
("titles", "Look for", ", ".join(titles)),
("places", "Where", ", ".join(config.filters.locations)),
("exclude", "Leave out", ", ".join(config.filters.exclude)),
("max_age_days", "Posted within (days)", str(config.max_age_days)),
("resume", "Resume", "loaded" if config.cv.is_file() else "none yet"),
("answers", "Your answers", f"{len(self.session.store.answers())}, given with the CV to every form"),
("mail", "Your mail", self.mail_words()),
("key", "OrcaRouter key", f"from {KEY_ENV}" if config.key_from_environment() else "saved" if config.api_key() else "none yet"),
("sign_in", "Sign in to a job site", "the browser opens, you sign in there"),
("model", "Forms model", config.model),
("judge_model", "Scoring model", config.judge_model),
("quick_model", "Search plan model", config.quick_model),
("decision_model", "Shortlist model", config.decision_model),
("answers", "Your answers", f"{len(self.session.store.answers())} saved, given with the CV to every form"),
("folder", "Your files", str(config.workspace)),
]
@@ -536,7 +517,7 @@ class AIHawk(App[None]):
def action_settings(self) -> None:
if self.applying() or self.signing_in:
self.say("One moment: the browser is in use.")
self.say("The browser is busy.")
else:
self.settings()
@@ -560,7 +541,7 @@ class AIHawk(App[None]):
await self.change_key()
else:
label, now = next((label, now) for key, label, now in self.setting_rows() if key == choice)
answer = await self.push_screen_wait(TextPrompt(label, value=now, hint="Enter saves it · Esc leaves it as it was"))
answer = await self.push_screen_wait(TextPrompt(label, value=now, hint="Enter saves it · Esc leaves it"))
if answer is not None:
try:
self.session.change_setting(choice, answer)
@@ -569,32 +550,32 @@ class AIHawk(App[None]):
self.say(str(error))
self.show_heading()
if search_again:
self.say("Searching again with the new settings.")
self.say("Searching again.")
self.start()
def mail_words(self) -> str:
email, provider = self.session.mail()
if provider in MAILBOXES:
return f"{MAIL_NAMES[provider]}, {email}"
return "not set up: a code a site sends you cannot be read"
return "not set up"
async def set_up_mail(self) -> bool:
"""The mailbox the job sites write to: its address, then the browser open on it for the person to sign in once, and the
inbox checked. Esc at any point leaves it for later. Whether the inbox was reached."""
session = self.session
email, provider = session.mail()
answer = await self.push_screen_wait(TextPrompt(MAIL_QUESTION, value=email, placeholder="[email protected]", hint="Enter · Esc leaves it for later"))
answer = await self.push_screen_wait(TextPrompt(MAIL_QUESTION, value=email, placeholder="[email protected]", hint=MAIL_HINT))
if not answer or "@" not in answer:
session.set_mail(email, "later")
self.say("Mail left for later: press e, then Your mail. A code a site sends you cannot be read until then.")
self.say("Mail left for later: e, then Your mail.")
return False
email = answer.strip()
provider = mail_provider(email) or (provider if provider in MAILBOXES else "")
while provider not in MAILBOXES:
choice = await self.push_screen_wait(TextPrompt(f"Where is {email}? Type gmail or outlook.", placeholder="gmail", hint="Enter · Esc leaves it for later"))
choice = await self.push_screen_wait(TextPrompt(f"Where is {email}?", placeholder="gmail", hint="gmail or outlook · Enter · Esc for later"))
if choice is None:
session.set_mail(email, "later")
self.say("Mail left for later: press e, then Your mail.")
self.say("Mail left for later: e, then Your mail.")
return False
provider = choice.strip().lower()
session.set_mail(email, provider)
@@ -604,7 +585,7 @@ class AIHawk(App[None]):
try:
async def wait() -> None:
await self.push_screen_wait(SignIn(f"{name} is open in the browser window. Sign in once with {email}: AIHawk only reads the codes that job sites send you. Press Enter here when you see your inbox."))
await self.push_screen_wait(SignIn(f"{name} is open in the browser. Sign in with {email}.", hint="Enter here when you see your inbox"))
reached = await session.sign_in_mail(provider, wait)
except Exception as error: # noqa: BLE001 Said on the screen, as for the other browser openings.
@@ -613,11 +594,11 @@ class AIHawk(App[None]):
finally:
self.signing_in = False
if reached:
self.say(f"Signed in to {name}. It stays signed in for every application.")
self.say(f"Signed in to {name}.")
return True
again = await self.push_screen_wait(TextPrompt(f"{name} still shows its sign-in page. Enter opens it again, Esc leaves it for later.", hint="Enter · Esc"))
again = await self.push_screen_wait(TextPrompt(f"{name} still shows its sign-in page.", hint="Enter opens it again · Esc for later"))
if again is None:
self.say("Mail left for later: press e, then Your mail.")
self.say("Mail left for later: e, then Your mail.")
return False
async def change_answers(self) -> None:
@@ -642,7 +623,7 @@ class AIHawk(App[None]):
if config.key_from_environment():
self.say(f"The key comes from the {KEY_ENV} variable, which wins over one typed here.")
return
key = await self.push_screen_wait(TextPrompt(f"Your OrcaRouter key. Get one at {KEY_URL} ; it stays on this computer.", placeholder="sk-orca-...", password=True, hint="Enter saves it · Esc leaves it as it was"))
key = await self.push_screen_wait(TextPrompt(KEY_QUESTION, placeholder="sk-orca-...", password=True, hint=KEY_HINT))
if key:
self.session.save_key(key)
@@ -657,11 +638,8 @@ class AIHawk(App[None]):
self.start()
def action_apply(self) -> None:
if self.signing_in:
self.say("One moment: the browser is open for signing in.")
return
if self.applying():
self.say("One moment: still applying.")
if self.signing_in or self.applying():
self.say("The browser is busy.")
return
self.apply_selected()
@@ -671,26 +649,22 @@ class AIHawk(App[None]):
def action_sign_in(self) -> None:
# One browser per profile: an application and the sign-in window never open it at the same time.
if self.signing_in:
self.say("The browser is already open for signing in.")
elif self.applying():
self.say("Not now: an application is using the browser. Wait until it is done.")
if self.signing_in or self.applying():
self.say("The browser is busy.")
else:
self.sign_in()
@work(group="sign_in")
async def sign_in(self) -> None:
self.signing_in = True
self.say("Opening the browser for you to sign in.")
async def wait() -> None:
await self.push_screen_wait(SignIn())
try:
await self.session.sign_in(wait)
self.say("The browser is closed. Applications use the same browser profile, so the sign-ins carry over.")
except Exception as error: # noqa: BLE001 The screen stays up and says what went wrong, as in start.
self.say(f"The sign-in browser did not open: {error}")
self.say(f"The browser did not open: {error}")
finally:
self.signing_in = False
@@ -710,7 +684,6 @@ class AIHawk(App[None]):
return
unread: list[Stored] = []
self.busy = True
self.show_heading("applying")
try:
results = await self.session.send(ready, self.say, self.refresh_jobs)
unread = [item for item, outcome in results if MAIL_NOT_SIGNED_IN in outcome.summary]
@@ -720,8 +693,6 @@ class AIHawk(App[None]):
self.busy = False
self.selected -= {item.job.id for item in chosen}
self.refresh_jobs()
self.show_heading()
self.say("Press s for every application and what each page said.")
if unread:
await self.mail_then_retry(unread)
@@ -732,7 +703,6 @@ class AIHawk(App[None]):
if not await self.set_up_mail():
return
self.busy = True
self.show_heading("applying again")
try:
await self.session.send(items, self.say, self.refresh_jobs)
except Exception as error: # noqa: BLE001
@@ -740,7 +710,6 @@ class AIHawk(App[None]):
finally:
self.busy = False
self.refresh_jobs()
self.show_heading()
def can_send(self, item: Stored) -> bool:
"""Whether the job goes in the batch: all of them, but one already sent."""
+15 -15
View File
@@ -135,12 +135,13 @@ async def settle(pilot: Any, app: AIHawk) -> None:
raise AssertionError("the flow never settled")
def test_a_judged_posting_with_no_text_says_so_in_its_status_and_keeps_its_score(home: Path) -> None:
def test_a_judged_posting_with_no_text_keeps_its_score_and_the_row_carries_no_status(home: Path) -> None:
app = AIHawk(home)
judged = Stored(Job("x", "Co", "Engineer", "Milan", "u", "u", ""), "scored", 3.0, None, None, None)
read = Stored(Job("y", "Co", "Engineer", "Milan", "u", "u", "A posting."), "scored", 3.0, None, None, None)
assert app.row(judged)[1] == "3.0" and app.row(judged)[-1] == "title only"
assert app.row(read)[-1] == ""
# The row says the score and nothing else; that the text was missing is said in the details, on Enter.
assert app.row(judged)[1] == "3.0" and len(app.row(judged)) == len(app.row(read)) == 5
assert str(app.row(judged)[0]) == "" and str(app.row(read)[0]) == ""
def companies(app: AIHawk) -> list[str]:
@@ -257,7 +258,7 @@ def test_space_space_a_then_enter_sends_each_selected_job_once_and_asks_no_quest
await pilot.pause(0.1)
assert not isinstance(app.screen, TextPrompt)
table = app.query_one(DataTable)
assert [table.get_row_at(index)[5] for index in range(2)] == ["sent", "sent"]
assert [str(table.get_row_at(index)[0]) for index in range(2)] == ["✓", "✓"]
assert app.selected == set()
asyncio.run(run())
@@ -280,7 +281,7 @@ def test_escape_on_the_batch_sends_none_and_the_selection_stays(home: Path, resu
await pilot.press("escape")
await wait_for_apply(pilot, app)
assert app.selected == {"high"}
assert app.query_one(DataTable).get_row_at(0)[5] == ""
assert str(app.query_one(DataTable).get_row_at(0)[0]) == "●"
asyncio.run(run())
assert calls == []
@@ -373,7 +374,7 @@ def test_s_with_nothing_tried_says_how_to_send(home: Path, resume: Path) -> None
await pilot.press("s")
await pilot.pause(0.1)
assert not isinstance(app.screen, SentList)
assert "Nothing has been sent yet" in logged(app)
assert "Nothing sent yet" in logged(app)
asyncio.run(run())
@@ -392,25 +393,24 @@ def test_each_send_says_how_it_went_and_where_to_read_more(home: Path, resume: P
await wait_for_apply(pilot, app)
log = logged(app)
assert "Bestco: sent and confirmed. The page says thank you." in log
assert "Press s for every application" in log
asyncio.run(run())
def test_at_80_columns_the_status_shows_and_at_100_the_location_too(home: Path, resume: Path) -> None:
def test_at_80_columns_the_role_is_the_last_column_and_at_100_the_location_is(home: Path, resume: Path) -> None:
async def run() -> None:
for width, labels in ((80, 5), (100, 6)):
for width, labels in ((80, 5), (100, 6)): # the columns before the status column was dropped; one fewer now
app = scripted_app(home, resume)
async with app.run_test(size=(width, 24)) as pilot:
await settle(pilot, app)
table = app.query_one(DataTable)
assert len(table.ordered_columns) == labels and str(table.ordered_columns[-1].label) == "Status"
assert len(table.ordered_columns) == labels - 1 and str(table.ordered_columns[-1].label) == ("Location" if width >= 100 else "Role")
assert sum(column.width + 2 for column in table.ordered_columns) <= width - 2
asyncio.run(run())
def test_a_jobs_details_can_select_it_and_the_heading_counts_the_selection(home: Path, resume: Path) -> None:
def test_a_jobs_details_can_select_it_and_the_row_shows_the_mark(home: Path, resume: Path) -> None:
async def run() -> None:
app = scripted_app(home, resume)
async with app.run_test(size=(140, 40)) as pilot:
@@ -420,7 +420,7 @@ def test_a_jobs_details_can_select_it_and_the_heading_counts_the_selection(home:
await pilot.press("space")
await pilot.pause(0.1)
assert not isinstance(app.screen, JobDetails) and len(app.selected) == 1
assert "1 selected" in str(app.query_one("#heading", Static).render())
assert str(app.query_one(DataTable).get_row_at(0)[0]) == "●"
asyncio.run(run())
@@ -674,7 +674,7 @@ def test_a_failed_model_call_says_in_plain_words_what_happened_and_keeps_the_det
assert text.endswith(f"({error})")
def test_ctrl_c_at_the_key_prompt_leaves_and_the_prompt_says_how(home: Path, monkeypatch: pytest.MonkeyPatch) -> None:
def test_ctrl_c_at_the_key_prompt_leaves(home: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ORCAROUTER_API_KEY")
async def run() -> None:
@@ -682,7 +682,7 @@ def test_ctrl_c_at_the_key_prompt_leaves_and_the_prompt_says_how(home: Path, mon
async with app.run_test(size=(80, 24)) as pilot:
await settle(pilot, app)
assert isinstance(app.screen, TextPrompt)
assert "Ctrl+C quits" in str(app.screen.query_one(".hint", Static).content)
assert "Enter saves it" in str(app.screen.query_one(".hint", Static).content)
await pilot.press("ctrl+c")
await pilot.pause(0.1)
assert not app.is_running
@@ -886,7 +886,7 @@ def test_the_mail_question_left_with_escape_is_not_asked_again_at_the_next_start
await pilot.press("escape")
await settle(pilot, app)
assert companies(app) == ["Bestco", "Lowly", "Noplugin"]
assert ("mail", "Your mail", "not set up: a code a site sends you cannot be read") in app.setting_rows()
assert ("mail", "Your mail", "not set up") in app.setting_rows()
asyncio.run(run())
assert read_answers(load_config(home))["mail_provider"] == "later"