lazy-github 0.1.7__tar.gz → 0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {lazy_github-0.1.7 → lazy_github-0.2}/PKG-INFO +2 -2
  2. {lazy_github-0.1.7 → lazy_github-0.2}/README.md +1 -1
  3. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/cli.py +18 -1
  4. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/config.py +21 -5
  5. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/git_cli.py +5 -5
  6. lazy_github-0.2/lazy_github/lib/github/branches.py +28 -0
  7. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/issues.py +9 -3
  8. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/pull_requests.py +18 -0
  9. lazy_github-0.2/lazy_github/lib/github/workflows.py +41 -0
  10. lazy_github-0.2/lazy_github/lib/logging.py +27 -0
  11. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/messages.py +25 -1
  12. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/models/github.py +47 -2
  13. lazy_github-0.2/lazy_github/ui/app.py +54 -0
  14. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/new_comment.py +14 -9
  15. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/new_issue.py +19 -14
  16. lazy_github-0.2/lazy_github/ui/screens/new_pull_request.py +214 -0
  17. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/primary.py +84 -27
  18. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/settings.py +24 -6
  19. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/command_log.py +14 -10
  20. lazy_github-0.2/lazy_github/ui/widgets/common.py +187 -0
  21. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/conversations.py +9 -1
  22. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/issues.py +58 -24
  23. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/pull_requests.py +42 -14
  24. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/repositories.py +23 -15
  25. lazy_github-0.2/lazy_github/ui/widgets/workflows.py +137 -0
  26. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/PKG-INFO +2 -2
  27. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/SOURCES.txt +6 -2
  28. {lazy_github-0.1.7 → lazy_github-0.2}/pyproject.toml +1 -1
  29. lazy_github-0.1.7/lazy_github/ui/app.py +0 -26
  30. lazy_github-0.1.7/lazy_github/ui/widgets/actions.py +0 -14
  31. lazy_github-0.1.7/lazy_github/ui/widgets/common.py +0 -110
  32. {lazy_github-0.1.7 → lazy_github-0.2}/LICENSE +0 -0
  33. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/__main__.py +0 -0
  34. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/constants.py +0 -0
  35. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/context.py +0 -0
  36. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/auth.py +0 -0
  37. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/client.py +0 -0
  38. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/repositories.py +0 -0
  39. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/utils.py +0 -0
  40. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/auth.py +0 -0
  41. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/edit_issue.py +0 -0
  42. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/info.py +0 -0
  43. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/dependency_links.txt +0 -0
  44. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/entry_points.txt +0 -0
  45. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/requires.txt +0 -0
  46. {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/top_level.txt +0 -0
  47. {lazy_github-0.1.7 → lazy_github-0.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lazy-github
3
- Version: 0.1.7
3
+ Version: 0.2
4
4
  Summary: A terminal UI for interacting with Github
5
5
  Author-email: Chris Chapline <gizmo385@users.noreply.github.com>
6
6
  Maintainer-email: Chris Chapline <gizmo385@users.noreply.github.com>
@@ -47,7 +47,7 @@ inspiration from the [lazygit](https://github.com/jesseduffield/lazygit) project
47
47
  [Textual](https://textual.textualize.io/) to drive the terminal UI interactions.
48
48
 
49
49
 
50
- ![Example screenshot](./images/lazy-github-conversation-ui.svg)
50
+ ![Example screenshot](https://raw.githubusercontent.com/gizmo385/lazy-github/main/images/lazy-github-conversation-ui.svg)
51
51
 
52
52
 
53
53
  Currently, it supports the following:
@@ -5,7 +5,7 @@ inspiration from the [lazygit](https://github.com/jesseduffield/lazygit) project
5
5
  [Textual](https://textual.textualize.io/) to drive the terminal UI interactions.
6
6
 
7
7
 
8
- ![Example screenshot](./images/lazy-github-conversation-ui.svg)
8
+ ![Example screenshot](https://raw.githubusercontent.com/gizmo385/lazy-github/main/images/lazy-github-conversation-ui.svg)
9
9
 
10
10
 
11
11
  Currently, it supports the following:
@@ -1,12 +1,16 @@
1
1
  import click
2
+ import rich
3
+ import shutil
2
4
 
3
5
  from lazy_github.lib.config import _CONFIG_FILE_LOCATION, Config
6
+ from lazy_github.lib.context import LazyGithubContext
4
7
  from lazy_github.ui.app import app
5
8
 
6
9
 
7
10
  @click.group(invoke_without_command=True)
8
11
  @click.pass_context
9
12
  def cli(ctx: click.Context) -> None:
13
+ """A Terminal UI for interacting with Github"""
10
14
  if ctx.invoked_subcommand is None:
11
15
  run()
12
16
 
@@ -21,7 +25,7 @@ def run():
21
25
  def dump_config():
22
26
  """Dump the current configuration, as it would be loaded by LazyGithub"""
23
27
  print(f"Config file location: {_CONFIG_FILE_LOCATION} (exists => {_CONFIG_FILE_LOCATION.exists()})")
24
- print(Config.load_config().model_dump_json(indent=4))
28
+ rich.print_json(Config.load_config().model_dump_json())
25
29
 
26
30
 
27
31
  @cli.command
@@ -37,3 +41,16 @@ def clear_config():
37
41
  """Reset the user's settings"""
38
42
  _CONFIG_FILE_LOCATION.unlink(missing_ok=True)
39
43
  print("Your settings have been cleared")
44
+
45
+
46
+ @cli.command
47
+ @click.option("--no-confirm", is_flag=True, default=False, help="Don't ask for confirmation")
48
+ def clear_cache(no_confirm: bool):
49
+ """Reset the lazy-github cache"""
50
+ cache_directory = LazyGithubContext.config.cache.cache_directory
51
+ if no_confirm or click.confirm(f"Confirm deletion of everything in {cache_directory}"):
52
+ if cache_directory.exists():
53
+ shutil.rmtree(cache_directory)
54
+ print("Cache cleared")
55
+ else:
56
+ print("Canceling cache deletion")
@@ -2,9 +2,10 @@ import json
2
2
  from contextlib import contextmanager
3
3
  from datetime import timedelta
4
4
  from pathlib import Path
5
- from typing import Generator, List, Literal, Optional
5
+ from typing import Any, Generator, Literal, Optional
6
+ from textual.theme import BUILTIN_THEMES, Theme
6
7
 
7
- from pydantic import BaseModel
8
+ from pydantic import BaseModel, field_serializer, field_validator
8
9
 
9
10
  from lazy_github.lib.constants import CONFIG_FOLDER, IssueOwnerFilter, IssueStateFilter
10
11
 
@@ -33,7 +34,7 @@ class IssueSettings(BaseModel):
33
34
 
34
35
 
35
36
  class RepositorySettings(BaseModel):
36
- favorites: List[str] = []
37
+ favorites: list[str] = []
37
38
 
38
39
 
39
40
  class CacheSettings(BaseModel):
@@ -44,13 +45,27 @@ class CacheSettings(BaseModel):
44
45
 
45
46
 
46
47
  class AppearanceSettings(BaseModel):
47
- dark_mode: bool = True
48
+ theme: Theme = BUILTIN_THEMES["textual-dark"]
48
49
  # Settings to configure which UI elements to display by default
49
50
  show_command_log: bool = True
50
- show_actions: bool = True
51
+ show_workflows: bool = True
51
52
  show_issues: bool = True
52
53
  show_pull_requests: bool = True
53
54
 
55
+ @field_serializer("theme")
56
+ @classmethod
57
+ def serialize_theme(cls, theme: Theme | str) -> str:
58
+ return theme.name if isinstance(theme, Theme) else theme
59
+
60
+ @field_validator("theme", mode="before")
61
+ @classmethod
62
+ def validate_theme(cls, theme_name: Any) -> Theme:
63
+ return BUILTIN_THEMES.get(theme_name, BUILTIN_THEMES["textual-dark"])
64
+
65
+
66
+ class CoreConfig(BaseModel):
67
+ logfile_path: Path = CONFIG_FOLDER / "lazy_github.log"
68
+
54
69
 
55
70
  _CONFIG_INSTANCE: Optional["Config"] = None
56
71
 
@@ -63,6 +78,7 @@ class Config(BaseModel):
63
78
  issues: IssueSettings = IssueSettings()
64
79
  cache: CacheSettings = CacheSettings()
65
80
  api: ApiConfig = ApiConfig()
81
+ core: CoreConfig = CoreConfig()
66
82
 
67
83
  @classmethod
68
84
  def load_config(cls) -> "Config":
@@ -1,5 +1,5 @@
1
- import subprocess
2
1
  import re
2
+ from subprocess import DEVNULL, SubprocessError, check_output
3
3
 
4
4
  # Regex designed to match git@github.com:gizmo385/lazy-github.git:
5
5
  # ".+:" Match everything to the first colon
@@ -13,8 +13,8 @@ _GIT_REMOTE_REGEX = re.compile(r".+:([^\/]+)\/([^.]+).git")
13
13
  def current_local_repo_full_name(remote: str = "origin") -> str | None:
14
14
  """Returns the owner/name associated with the remote of the git repo in the current working directory."""
15
15
  try:
16
- output = subprocess.check_output(["git", "remote", "get-url", remote]).decode().strip()
17
- except subprocess.SubprocessError:
16
+ output = check_output(["git", "remote", "get-url", remote], stderr=DEVNULL).decode().strip()
17
+ except SubprocessError:
18
18
  return None
19
19
 
20
20
  if matches := re.match(_GIT_REMOTE_REGEX, output):
@@ -25,6 +25,6 @@ def current_local_repo_full_name(remote: str = "origin") -> str | None:
25
25
  def current_local_branch_name() -> str | None:
26
26
  """Returns the name of the current branch for the git repo in the current working directory."""
27
27
  try:
28
- return subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip()
29
- except subprocess.SubprocessError:
28
+ return check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=DEVNULL).decode().strip()
29
+ except SubprocessError:
30
30
  return None
@@ -0,0 +1,28 @@
1
+ from lazy_github.lib.constants import DIFF_CONTENT_ACCEPT_TYPE
2
+ from lazy_github.models.github import Branch, Repository
3
+ from lazy_github.lib.context import LazyGithubContext, github_headers
4
+
5
+
6
+ async def list_branches(repo: Repository, per_page: int = 30, page: int = 1) -> list[Branch]:
7
+ """List branches on the specified repo"""
8
+ query_params = {"page": page, "per_page": per_page}
9
+ response = await LazyGithubContext.client.get(
10
+ f"/repos/{repo.owner.login}/{repo.name}/branches",
11
+ headers=github_headers(),
12
+ params=query_params,
13
+ )
14
+ response.raise_for_status()
15
+ return [Branch(**branch) for branch in response.json()]
16
+
17
+
18
+ async def get_branch(repo: Repository, branch_name: str) -> Branch | None:
19
+ url = f"/repos/{repo.owner.login}/{repo.name}/branches/{branch_name}"
20
+ response = await LazyGithubContext.client.get(url, headers=github_headers())
21
+ return Branch(**response.json())
22
+
23
+
24
+ async def compare_branches(repo: Repository, base_branch: Branch, head_branch: Branch) -> str:
25
+ url = f"/repos/{repo.owner.login}/{repo.name}/compare/{base_branch.name}..{head_branch.name}"
26
+ response = await LazyGithubContext.client.get(url, headers=github_headers(accept=DIFF_CONTENT_ACCEPT_TYPE))
27
+ response.raise_for_status()
28
+ return response.text
@@ -4,6 +4,8 @@ from lazy_github.lib.constants import IssueOwnerFilter, IssueStateFilter
4
4
  from lazy_github.lib.context import LazyGithubContext, github_headers
5
5
  from lazy_github.models.github import Issue, IssueComment, PartialPullRequest, Repository
6
6
 
7
+ DEFAULT_PAGE_SIZE = 30
8
+
7
9
 
8
10
  class UpdateIssuePayload(TypedDict):
9
11
  title: str | None
@@ -11,15 +13,19 @@ class UpdateIssuePayload(TypedDict):
11
13
  state: str | None
12
14
 
13
15
 
14
- async def list_issues(repo: Repository, state: IssueStateFilter, owner: IssueOwnerFilter) -> list[Issue]:
16
+ async def list_issues(
17
+ repo: Repository, state: IssueStateFilter, owner: IssueOwnerFilter, page: int = 1, per_page: int = DEFAULT_PAGE_SIZE
18
+ ) -> list[Issue]:
15
19
  """Fetch issues (included pull requests) from the repo matching the state/owner filters"""
16
- query_params = {"state": str(state).lower()}
20
+ query_params = {"state": str(state).lower(), "page": page, "per_page": per_page}
17
21
  if owner == IssueOwnerFilter.MINE:
18
22
  user = await LazyGithubContext.client.user()
19
23
  query_params["creator"] = user.login
20
24
 
21
25
  response = await LazyGithubContext.client.get(
22
- f"/repos/{repo.owner.login}/{repo.name}/issues", headers=github_headers(), params=query_params
26
+ f"/repos/{repo.owner.login}/{repo.name}/issues",
27
+ headers=github_headers(),
28
+ params=query_params,
23
29
  )
24
30
  response.raise_for_status()
25
31
  result: list[Issue] = []
@@ -19,6 +19,24 @@ async def list_for_repo(repo: Repository) -> list[PartialPullRequest]:
19
19
  return [i for i in issues if isinstance(i, PartialPullRequest)]
20
20
 
21
21
 
22
+ async def create_pull_request(
23
+ repo: Repository, title: str, body: str, base_ref: str, head_ref: str, draft: bool = False
24
+ ) -> FullPullRequest:
25
+ user = await LazyGithubContext.client.user()
26
+ url = f"/repos/{repo.owner.login}/{repo.name}/pulls"
27
+ request_body = {
28
+ "title": title,
29
+ "draft": draft,
30
+ "base": base_ref,
31
+ "head": f"{user.login}:{head_ref}",
32
+ }
33
+ if body:
34
+ request_body["body"] = body
35
+ response = await LazyGithubContext.client.post(url, headers=github_headers(), json=request_body)
36
+ response.raise_for_status()
37
+ return FullPullRequest(**response.json(), repo=repo)
38
+
39
+
22
40
  async def get_full_pull_request(partial_pr: PartialPullRequest) -> FullPullRequest:
23
41
  """Converts a partial pull request into a full pull request"""
24
42
  url = f"/repos/{partial_pr.repo.owner.login}/{partial_pr.repo.name}/pulls/{partial_pr.number}"
@@ -0,0 +1,41 @@
1
+ from httpx import HTTPError
2
+ from lazy_github.lib.logging import lg
3
+ from lazy_github.lib.context import LazyGithubContext
4
+ from lazy_github.models.github import Repository, Workflow, WorkflowRun
5
+
6
+
7
+ async def list_workflows(repository: Repository, page: int = 1, per_page: int = 30) -> list[Workflow]:
8
+ """Lists available Github action workflows on the specified repo"""
9
+ query_params = {"page": page, "per_page": per_page}
10
+ url = f"/repos/{repository.owner.login}/{repository.name}/actions/workflows"
11
+ try:
12
+ response = await LazyGithubContext.client.get(url, params=query_params)
13
+ response.raise_for_status()
14
+ raw_json = response.json()
15
+ except HTTPError:
16
+ lg.exception("Error retrieving actions from the Github API")
17
+ return []
18
+ else:
19
+ if workflows := raw_json.get("workflows"):
20
+ return [Workflow(**w) for w in workflows]
21
+ else:
22
+ return []
23
+
24
+
25
+ async def list_workflow_runs(repository: Repository, page: int = 1, per_page: int = 30) -> list[WorkflowRun]:
26
+ """Lists github workflows runs on the specified repo"""
27
+ query_params = {"page": page, "per_page": per_page}
28
+ url = f"/repos/{repository.owner.login}/{repository.name}/actions/runs"
29
+
30
+ try:
31
+ response = await LazyGithubContext.client.get(url, params=query_params)
32
+ response.raise_for_status()
33
+ raw_json = response.json()
34
+ except HTTPError:
35
+ lg.exception("Error retrieving action runs from the Github API")
36
+ return []
37
+ else:
38
+ if workflows := raw_json.get("workflow_runs"):
39
+ return [WorkflowRun(**w) for w in workflows]
40
+ else:
41
+ return []
@@ -0,0 +1,27 @@
1
+ import logging
2
+ from lazy_github.lib.context import LazyGithubContext
3
+
4
+
5
+ # A universal logging format that we can use
6
+ class LazyGithubLogFormatter(logging.Formatter):
7
+ def __init__(self, include_exceptions: bool = True) -> None:
8
+ super().__init__("%(levelname)s %(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
9
+ self.include_exceptions = include_exceptions
10
+
11
+ def format(self, record: logging.LogRecord) -> str:
12
+ if not self.include_exceptions:
13
+ record.exc_info = None
14
+ record.exc_text = None
15
+ return super().format(record)
16
+
17
+
18
+ lg = logging.Logger("lazy_github", level=logging.DEBUG)
19
+ _lg_file_handler = logging.FileHandler(filename=LazyGithubContext.config.core.logfile_path)
20
+ _lg_file_handler.setFormatter(LazyGithubLogFormatter())
21
+ lg.addHandler(_lg_file_handler)
22
+
23
+
24
+ # Override the logging level for a bunch of noisy library loggers
25
+ _LOGGERS_TO_MUTE = ["hishel", "httpx", "httpcore", "markdown_it", "asyncio"]
26
+ for other_logger_name in _LOGGERS_TO_MUTE:
27
+ logging.getLogger(other_logger_name).setLevel(logging.WARNING)
@@ -2,7 +2,7 @@ from functools import cached_property
2
2
 
3
3
  from textual.message import Message
4
4
 
5
- from lazy_github.models.github import Issue, PartialPullRequest, Repository
5
+ from lazy_github.models.github import FullPullRequest, Issue, IssueComment, PartialPullRequest, Repository
6
6
 
7
7
 
8
8
  class RepoSelected(Message):
@@ -59,3 +59,27 @@ class IssuesAndPullRequestsFetched(Message):
59
59
  for issue in self.issues_and_pull_requests
60
60
  if isinstance(issue, Issue) and not isinstance(issue, PartialPullRequest)
61
61
  ]
62
+
63
+
64
+ class PullRequestCreated(Message):
65
+ def __init__(self, pull_request: FullPullRequest) -> None:
66
+ super().__init__()
67
+ self.pull_request = pull_request
68
+
69
+
70
+ class IssueCreated(Message):
71
+ def __init__(self, issue: Issue) -> None:
72
+ super().__init__()
73
+ self.issue = issue
74
+
75
+
76
+ class NewCommentCreated(Message):
77
+ def __init__(self, comment: IssueComment) -> None:
78
+ super().__init__()
79
+ self.comment = comment
80
+
81
+
82
+ class SettingsModalDismissed(Message):
83
+ def __init__(self, changed: bool) -> None:
84
+ super().__init__()
85
+ self.changed = changed
@@ -22,9 +22,9 @@ class RepositoryPermission(BaseModel):
22
22
  class Repository(BaseModel):
23
23
  name: str
24
24
  full_name: str
25
- default_branch: str
25
+ default_branch: str | None = None
26
26
  private: bool
27
- archived: bool
27
+ archived: bool | None = None
28
28
  owner: User
29
29
  description: str | None = None
30
30
  permissions: RepositoryPermission | None = None
@@ -108,6 +108,7 @@ class ReviewState(StrEnum):
108
108
  APPROVED = "APPROVED"
109
109
  CHANGES_REQUESTED = "CHANGES_REQUESTED"
110
110
  COMMENTED = "COMMENTED"
111
+ DISMISSED = "DISMISSED"
111
112
 
112
113
 
113
114
  class ReviewComment(IssueComment):
@@ -126,3 +127,47 @@ class Review(BaseModel):
126
127
  state: ReviewState
127
128
  comments: list[ReviewComment] = []
128
129
  submitted_at: datetime
130
+
131
+
132
+ class Commit(BaseModel):
133
+ sha: str
134
+ url: str
135
+
136
+
137
+ class Branch(BaseModel):
138
+ name: str
139
+ commit: Commit
140
+ protected: bool
141
+
142
+
143
+ class WorkflowState(StrEnum):
144
+ ACTIVE = "active"
145
+ DELETED = "deleted"
146
+ DISABLED_FORK = "disabled_fork"
147
+ DISABLED_INACTIVITY = "disabled_inactivity"
148
+ DISABLED_MANUALLY = "disabled_manually "
149
+
150
+
151
+ class Workflow(BaseModel):
152
+ name: str
153
+ state: WorkflowState
154
+ path: str
155
+ created_at: datetime
156
+ updated_at: datetime
157
+ url: str
158
+
159
+
160
+ class WorkflowRun(BaseModel):
161
+ name: str
162
+ display_title: str
163
+ path: str
164
+ run_number: int
165
+ head_branch: str
166
+ status: str
167
+ conclusion: str | None = None
168
+ event: str
169
+ actor: User
170
+ triggering_actor: User
171
+ repository: Repository
172
+ created_at: datetime
173
+ updated_at: datetime
@@ -0,0 +1,54 @@
1
+ from textual import log
2
+ from textual.app import App
3
+ from textual.theme import Theme
4
+
5
+ from lazy_github.lib.context import LazyGithubContext
6
+ from lazy_github.lib.github.auth import GithubAuthenticationRequired
7
+ from lazy_github.lib.messages import SettingsModalDismissed
8
+ from lazy_github.ui.screens.auth import AuthenticationModal
9
+ from lazy_github.ui.screens.primary import LazyGithubMainScreen
10
+
11
+
12
+ class LazyGithub(App):
13
+ BINDINGS = [("q", "quit", "Quit"), ("ctrl+p", "command_palette"), ("ctrl+m", "maximize", "Maximize")]
14
+
15
+ has_shown_maximize_toast: bool = False
16
+
17
+ async def authenticate_with_github(self):
18
+ try:
19
+ # We pull the user here to validate auth
20
+ _ = await LazyGithubContext.client.user()
21
+ self.push_screen(LazyGithubMainScreen(id="main-screen"))
22
+ except GithubAuthenticationRequired:
23
+ log("Triggering auth with github")
24
+ self.push_screen(AuthenticationModal(id="auth-modal"))
25
+
26
+ async def on_ready(self):
27
+ await self.authenticate_with_github()
28
+
29
+ def on_settings_modal_dismissed(self, message: SettingsModalDismissed) -> None:
30
+ if not message.changed:
31
+ return
32
+
33
+ self.notify("Settings updated")
34
+ if isinstance(LazyGithubContext.config.appearance.theme, Theme):
35
+ self.theme = LazyGithubContext.config.appearance.theme.name
36
+ else:
37
+ self.theme = LazyGithubContext.config.appearance.theme
38
+
39
+ self.query_one("#main-screen", LazyGithubMainScreen).handle_settings_update()
40
+
41
+ def on_mount(self) -> None:
42
+ self.theme = LazyGithubContext.config.appearance.theme.name
43
+
44
+ def action_maximize(self) -> None:
45
+ if self.screen.is_maximized:
46
+ return
47
+ if self.screen.focused is not None:
48
+ # We don't need to repeatedly show this to the user
49
+ if self.screen.maximize(self.screen.focused) and not self.has_shown_maximize_toast:
50
+ self.notify("Current view maximized. Press [b]escape[/b] to return.", title="View Maximized")
51
+ self.has_shown_maximize_toast = True
52
+
53
+
54
+ app = LazyGithub()
@@ -6,8 +6,9 @@ from textual.screen import ModalScreen
6
6
  from textual.widgets import Button, Label, Markdown, Rule, TextArea
7
7
 
8
8
  from lazy_github.lib.github import issues, pull_requests
9
+ from lazy_github.lib.messages import NewCommentCreated
9
10
  from lazy_github.models.github import Issue, IssueComment, Repository, Review, ReviewComment
10
- from lazy_github.ui.widgets.command_log import log_event
11
+ from lazy_github.lib.logging import lg
11
12
 
12
13
  CommmentReplyTarget = ReviewComment | Review | IssueComment
13
14
 
@@ -83,23 +84,23 @@ class NewCommentContainer(Container):
83
84
  body = self.query_one("#new_comment_body", TextArea).text
84
85
  try:
85
86
  if isinstance(self.reply_to, ReviewComment):
86
- await pull_requests.reply_to_review_comment(self.repo, self.issue, self.reply_to, body)
87
+ new_comment = await pull_requests.reply_to_review_comment(self.repo, self.issue, self.reply_to, body)
87
88
  else:
88
- await issues.create_comment(self.issue, body)
89
- except HTTPStatusError as hse:
89
+ new_comment = await issues.create_comment(self.issue, body)
90
+ except HTTPStatusError:
90
91
  # TODO: We should handle the error case better here
91
- log_event(f"Error while posting comment for issue #{self.issue.number}: {hse}")
92
+ lg.exception(f"Error while posting comment for issue #{self.issue.number}")
92
93
  self.app.pop_screen()
93
94
  else:
94
- log_event(f"Successfully posted new comment for issue #{self.issue.number}")
95
- self.app.pop_screen()
95
+ lg.info(f"Successfully posted new comment for issue #{self.issue.number}")
96
+ self.post_message(NewCommentCreated(new_comment))
96
97
 
97
98
  @on(Button.Pressed, "#cancel_comment")
98
99
  def cancel_comment(self, _: Button.Pressed) -> None:
99
100
  self.app.pop_screen()
100
101
 
101
102
 
102
- class NewCommentModal(ModalScreen):
103
+ class NewCommentModal(ModalScreen[IssueComment | None]):
103
104
  DEFAULT_CSS = """
104
105
  NewCommentModal {
105
106
  border: ascii green;
@@ -134,5 +135,9 @@ class NewCommentModal(ModalScreen):
134
135
  def compose(self) -> ComposeResult:
135
136
  yield NewCommentContainer(self.repo, self.issue, self.reply_to)
136
137
 
138
+ @on(NewCommentCreated)
139
+ def on_comment_created(self, message: NewCommentCreated) -> None:
140
+ self.dismiss(message.comment)
141
+
137
142
  def action_cancel(self) -> None:
138
- self.app.pop_screen()
143
+ self.dismiss(None)
@@ -4,8 +4,10 @@ from textual.containers import Container, Horizontal, ScrollableContainer
4
4
  from textual.screen import ModalScreen
5
5
  from textual.widgets import Button, Input, Label, Rule, TextArea
6
6
 
7
+ from lazy_github.lib.context import LazyGithubContext
7
8
  from lazy_github.lib.github import issues
8
- from lazy_github.models.github import Repository
9
+ from lazy_github.lib.messages import IssueCreated
10
+ from lazy_github.models.github import Issue
9
11
 
10
12
 
11
13
  class NewIssueContainer(Container):
@@ -23,11 +25,8 @@ class NewIssueContainer(Container):
23
25
  }
24
26
  """
25
27
 
26
- def __init__(self, repo: Repository, *args, **kwargs) -> None:
27
- super().__init__(*args, **kwargs)
28
- self.repo = repo
29
-
30
28
  def compose(self) -> ComposeResult:
29
+ assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo in new PR modal"
31
30
  with ScrollableContainer():
32
31
  yield Label("[bold]Title[/bold]")
33
32
  yield Input(placeholder="Title", id="new_issue_title")
@@ -47,6 +46,8 @@ class NewIssueContainer(Container):
47
46
 
48
47
  @on(Button.Pressed, "#save_new_issue")
49
48
  async def submit_new_issue(self, _: Button) -> None:
49
+ assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo from application context!"
50
+
50
51
  title = self.query_one("#new_issue_title", Input).value
51
52
  body = self.query_one("#new_issue_body", TextArea).text
52
53
  if not str(title):
@@ -57,12 +58,13 @@ class NewIssueContainer(Container):
57
58
  return
58
59
 
59
60
  self.notify("Creating new issue...")
60
- new_issue = await issues.create_issue(self.repo, title, body)
61
- self.notify(f"Successfully updated created issue #{new_issue.number}")
62
- self.app.pop_screen()
61
+ new_issue = await issues.create_issue(LazyGithubContext.current_repo, title, body)
62
+ self.notify(f"Successfully created issue #{new_issue.number}")
63
+ self.post_message(IssueCreated(new_issue))
63
64
 
64
65
 
65
- class NewIssueModal(ModalScreen):
66
+ class NewIssueModal(ModalScreen[Issue | None]):
67
+ BINDINGS = [("ESC, q", "cancel", "Cancel")]
66
68
  DEFAULT_CSS = """
67
69
  NewIssueModal {
68
70
  align: center middle;
@@ -77,9 +79,12 @@ class NewIssueModal(ModalScreen):
77
79
  }
78
80
  """
79
81
 
80
- def __init__(self, repo: Repository, *args, **kwargs) -> None:
81
- super().__init__(*args, **kwargs)
82
- self.repo = repo
83
-
84
82
  def compose(self) -> ComposeResult:
85
- yield NewIssueContainer(self.repo)
83
+ yield NewIssueContainer()
84
+
85
+ def action_cancel(self) -> None:
86
+ self.dismiss()
87
+
88
+ @on(IssueCreated)
89
+ def on_issue_created(self, message: IssueCreated) -> None:
90
+ self.dismiss(message.issue)