lazy-github 0.2.3__tar.gz → 0.2.4__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.
- {lazy_github-0.2.3 → lazy_github-0.2.4}/PKG-INFO +1 -1
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/bindings.py +25 -9
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/config.py +0 -1
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/constants.py +4 -3
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/workflows.py +16 -1
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github_cli.py +10 -3
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/messages.py +7 -1
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/models/github.py +19 -1
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/edit_issue.py +2 -2
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/new_comment.py +2 -2
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/new_issue.py +2 -2
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/new_pull_request.py +3 -17
- lazy_github-0.2.4/lazy_github/ui/screens/notifications.py +165 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/primary.py +7 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/settings.py +2 -2
- lazy_github-0.2.4/lazy_github/ui/screens/trigger_workflow.py +120 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/common.py +2 -2
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/workflows.py +21 -3
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/PKG-INFO +1 -1
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/SOURCES.txt +2 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/pyproject.toml +1 -1
- {lazy_github-0.2.3 → lazy_github-0.2.4}/LICENSE +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/README.md +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/__main__.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/cli.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/context.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/git_cli.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/auth.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/branches.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/client.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/issues.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/pull_requests.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/repositories.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/logging.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/utils.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/app.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/auth.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/command_log.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/conversations.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/info.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/issues.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/pull_requests.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/repositories.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/version.py +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/dependency_links.txt +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/entry_points.txt +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/requires.txt +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/top_level.txt +0 -0
- {lazy_github-0.2.3 → lazy_github-0.2.4}/setup.cfg +0 -0
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
from textual.binding import Binding
|
|
2
2
|
|
|
3
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
3
4
|
from lazy_github.lib.utils import classproperty
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class LazyGithubBindings:
|
|
7
8
|
# Global App Bindings
|
|
8
9
|
QUIT_APP = Binding("q", "quit", "Quit", id="app.quit")
|
|
9
|
-
OPEN_COMMAND_PALLETE = Binding("ctrl+p", "command_palette", "
|
|
10
|
+
OPEN_COMMAND_PALLETE = Binding("ctrl+p", "command_palette", "Commands", id="app.command_palette")
|
|
10
11
|
MAXIMIZE_WIDGET = Binding("ctrl+m", "maximize", "Maximize", id="app.maximize_widget")
|
|
11
12
|
|
|
12
13
|
# Triggering creation flows
|
|
13
|
-
OPEN_ISSUE = Binding("I", "open_issue", "
|
|
14
|
-
EDIT_ISSUE = Binding("E", "edit_issue", "Edit
|
|
15
|
-
OPEN_PULL_REQUEST = Binding("P", "open_pull_request", "
|
|
16
|
-
NEW_COMMENT = Binding("n", "new_comment", "New
|
|
17
|
-
REPLY_TO_REVIEW = Binding("r", "reply_to_review", "Reply to
|
|
18
|
-
REPLY_TO_COMMENT = Binding("r", "reply_to_individual_comment", "Reply to
|
|
14
|
+
OPEN_ISSUE = Binding("I", "open_issue", "New Issue", id="issue.new")
|
|
15
|
+
EDIT_ISSUE = Binding("E", "edit_issue", "Edit Issue", id="issue.edit")
|
|
16
|
+
OPEN_PULL_REQUEST = Binding("P", "open_pull_request", "New PR", id="pull_request.new")
|
|
17
|
+
NEW_COMMENT = Binding("n", "new_comment", "New Comment", id="conversation.comment.new")
|
|
18
|
+
REPLY_TO_REVIEW = Binding("r", "reply_to_review", "Reply to Review", id="conversation.review.reply")
|
|
19
|
+
REPLY_TO_COMMENT = Binding("r", "reply_to_individual_comment", "Reply to Comment", id="conversation.comment.reply")
|
|
19
20
|
|
|
20
21
|
# Repository actions
|
|
21
|
-
TOGGLE_FAVORITE_REPO = Binding("ctrl+f", "toggle_favorite_repo", "Toggle
|
|
22
|
+
TOGGLE_FAVORITE_REPO = Binding("ctrl+f", "toggle_favorite_repo", "Toggle Favorite", id="repositories.favorite")
|
|
22
23
|
|
|
23
24
|
# Common widget bindings
|
|
24
25
|
SELECT_ENTRY = Binding("enter,space", "select_cursor", "Select table entry", id="common.table.select", show=False)
|
|
@@ -38,9 +39,24 @@ class LazyGithubBindings:
|
|
|
38
39
|
TABLE_PAGE_LEFT = Binding("^", "page_left", "Table page left", show=False, id="common.table.page_left")
|
|
39
40
|
TABLE_PAGE_RIGHT = Binding("$", "page_right", "Table page right", show=False, id="common.table.page_right")
|
|
40
41
|
|
|
42
|
+
# Workflows
|
|
43
|
+
TRIGGER_WORKFLOW = Binding("T", "trigger_workflow", "Trigger Workflow", show=True, id="workflows.trigger")
|
|
44
|
+
|
|
45
|
+
# Notifications
|
|
46
|
+
OPEN_NOTIFICATIONS_MODAL = Binding(
|
|
47
|
+
"ctrl+n",
|
|
48
|
+
"view_notifications",
|
|
49
|
+
"Notifications",
|
|
50
|
+
id="notifications.open",
|
|
51
|
+
show=LazyGithubContext.config.notifications.enabled,
|
|
52
|
+
)
|
|
53
|
+
MARK_NOTIFICATION_READ = Binding("R", "mark_read", "Mark as Read", id="notifications.mark_read")
|
|
54
|
+
VIEW_READ_NOTIFICATIONS = Binding("r", "view_read", "View Read Notifications", id="notifications.view_read")
|
|
55
|
+
VIEW_UNREAD_NOTIFICATIONS = Binding("u", "view_unread", "View Unread Notifications", id="notifications.view_unread")
|
|
56
|
+
|
|
41
57
|
# Dialog bindings
|
|
42
58
|
SUBMIT_DIALOG = Binding("shift+enter", "submit", "Submit", id="modal.submit")
|
|
43
|
-
|
|
59
|
+
CLOSE_DIALOG = Binding("q, ESC", "close", "Close", id="modal.close")
|
|
44
60
|
SEARCH_DIALOG = Binding("/", "search", "Search", id="modal.search")
|
|
45
61
|
|
|
46
62
|
# Focusing different UI elements
|
|
@@ -12,8 +12,9 @@ DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
|
|
|
12
12
|
# Symbols used in various UI tables
|
|
13
13
|
IS_FAVORITED = "[green]★[/green]"
|
|
14
14
|
IS_NOT_FAVORITED = "☆"
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
CHECKMARK = "✔"
|
|
16
|
+
X_MARK = "✘"
|
|
17
|
+
BULLET_POINT = "•"
|
|
17
18
|
|
|
18
19
|
NOTIFICATION_REFRESH_INTERVAL = 60
|
|
19
20
|
|
|
@@ -27,7 +28,7 @@ def favorite_string(favorite: bool) -> str:
|
|
|
27
28
|
|
|
28
29
|
def private_string(private: bool) -> str:
|
|
29
30
|
"""Helper function to return the right string to indicate if something is private"""
|
|
30
|
-
return
|
|
31
|
+
return CHECKMARK if private else X_MARK
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class IssueStateFilter(StrEnum):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from httpx import HTTPError
|
|
2
2
|
|
|
3
|
-
from lazy_github.lib.context import LazyGithubContext
|
|
3
|
+
from lazy_github.lib.context import LazyGithubContext, github_headers
|
|
4
4
|
from lazy_github.lib.logging import lg
|
|
5
5
|
from lazy_github.models.github import Repository, Workflow, WorkflowRun
|
|
6
6
|
|
|
@@ -40,3 +40,18 @@ async def list_workflow_runs(repository: Repository, page: int = 1, per_page: in
|
|
|
40
40
|
return [WorkflowRun(**w) for w in workflows]
|
|
41
41
|
else:
|
|
42
42
|
return []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def create_dispatch_event(repository: Repository, workflow: Workflow, branch: str) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Creates a workflow dispatch event for the specified workflow. For properly configured workflows, this will trigger
|
|
48
|
+
a new one against the specified branch
|
|
49
|
+
"""
|
|
50
|
+
url = f"/repos/{repository.owner.login}/{repository.name}/actions/workflows/{workflow.id}/dispatches"
|
|
51
|
+
body = {"ref": branch}
|
|
52
|
+
response = await LazyGithubContext.client.post(url, headers=github_headers(), json=body)
|
|
53
|
+
try:
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
except HTTPError:
|
|
56
|
+
lg.exception("Error creating workflow dispatch event!")
|
|
57
|
+
return response.is_success
|
|
@@ -2,6 +2,8 @@ import asyncio
|
|
|
2
2
|
import json
|
|
3
3
|
from asyncio.subprocess import PIPE, Process
|
|
4
4
|
|
|
5
|
+
from lazy_github.models.github import Notification
|
|
6
|
+
|
|
5
7
|
NOTIFICATIONS_PAGE_COUNT = 30
|
|
6
8
|
|
|
7
9
|
|
|
@@ -20,18 +22,23 @@ async def is_logged_in() -> bool:
|
|
|
20
22
|
return False
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
async def fetch_notifications(all: bool) -> list[
|
|
25
|
+
async def fetch_notifications(all: bool) -> list[Notification]:
|
|
24
26
|
"""Fetches notifications on GitHub. If all=True, then previously read notifications will also be returned"""
|
|
25
27
|
result = await _run_gh_cli_command(f'api "/notifications?all={str(all).lower()}"')
|
|
26
28
|
await result.wait()
|
|
27
|
-
notifications: list[
|
|
29
|
+
notifications: list[Notification] = []
|
|
28
30
|
if result.stdout:
|
|
29
31
|
stdout = await result.stdout.read()
|
|
30
32
|
parsed = json.loads(stdout.decode())
|
|
31
|
-
notifications = [n
|
|
33
|
+
notifications = [Notification(**n) for n in parsed]
|
|
32
34
|
return notifications
|
|
33
35
|
|
|
34
36
|
|
|
37
|
+
async def mark_notification_as_read(notification: Notification) -> None:
|
|
38
|
+
result = await _run_gh_cli_command(f"--method PATCH api /notifications/threads/{notification.id}")
|
|
39
|
+
await result.wait()
|
|
40
|
+
|
|
41
|
+
|
|
35
42
|
async def unread_notification_count() -> int:
|
|
36
43
|
"""Returns the number of currently unread notifications on GitHub"""
|
|
37
44
|
return len(await fetch_notifications(all=False))
|
|
@@ -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 FullPullRequest, Issue, IssueComment, PartialPullRequest, Repository
|
|
5
|
+
from lazy_github.models.github import Branch, FullPullRequest, Issue, IssueComment, PartialPullRequest, Repository
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class RepoSelected(Message):
|
|
@@ -83,3 +83,9 @@ class SettingsModalDismissed(Message):
|
|
|
83
83
|
def __init__(self, changed: bool) -> None:
|
|
84
84
|
super().__init__()
|
|
85
85
|
self.changed = changed
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class BranchesLoaded(Message):
|
|
89
|
+
def __init__(self, branches: list[Branch]) -> None:
|
|
90
|
+
super().__init__()
|
|
91
|
+
self.branches = branches
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from enum import StrEnum
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class User(BaseModel):
|
|
@@ -149,6 +149,7 @@ class WorkflowState(StrEnum):
|
|
|
149
149
|
|
|
150
150
|
|
|
151
151
|
class Workflow(BaseModel):
|
|
152
|
+
id: int
|
|
152
153
|
name: str
|
|
153
154
|
state: WorkflowState
|
|
154
155
|
path: str
|
|
@@ -171,3 +172,20 @@ class WorkflowRun(BaseModel):
|
|
|
171
172
|
repository: Repository
|
|
172
173
|
created_at: datetime
|
|
173
174
|
updated_at: datetime
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class NotificationSubject(BaseModel):
|
|
178
|
+
title: str
|
|
179
|
+
url: str | None
|
|
180
|
+
latest_comment_url: str | None
|
|
181
|
+
subject_type: str = Field(alias="type")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Notification(BaseModel):
|
|
185
|
+
id: int
|
|
186
|
+
repository: Repository
|
|
187
|
+
subject: NotificationSubject
|
|
188
|
+
reason: str
|
|
189
|
+
unread: bool
|
|
190
|
+
updated_at: datetime
|
|
191
|
+
last_read_at: datetime | None
|
|
@@ -66,7 +66,7 @@ class EditIssueContainer(Container):
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
class EditIssueModal(ModalScreen):
|
|
69
|
-
BINDINGS = [LazyGithubBindings.
|
|
69
|
+
BINDINGS = [LazyGithubBindings.CLOSE_DIALOG]
|
|
70
70
|
DEFAULT_CSS = """
|
|
71
71
|
EditIssueModal {
|
|
72
72
|
align: center middle;
|
|
@@ -89,5 +89,5 @@ class EditIssueModal(ModalScreen):
|
|
|
89
89
|
yield EditIssueContainer(self.issue)
|
|
90
90
|
yield LazyGithubFooter()
|
|
91
91
|
|
|
92
|
-
def
|
|
92
|
+
def action_close(self) -> None:
|
|
93
93
|
self.app.pop_screen()
|
|
@@ -119,7 +119,7 @@ class NewCommentModal(ModalScreen[IssueComment | None]):
|
|
|
119
119
|
}
|
|
120
120
|
"""
|
|
121
121
|
|
|
122
|
-
BINDINGS = [LazyGithubBindings.
|
|
122
|
+
BINDINGS = [LazyGithubBindings.CLOSE_DIALOG]
|
|
123
123
|
|
|
124
124
|
def __init__(
|
|
125
125
|
self,
|
|
@@ -142,5 +142,5 @@ class NewCommentModal(ModalScreen[IssueComment | None]):
|
|
|
142
142
|
def on_comment_created(self, message: NewCommentCreated) -> None:
|
|
143
143
|
self.dismiss(message.comment)
|
|
144
144
|
|
|
145
|
-
def
|
|
145
|
+
def action_close(self) -> None:
|
|
146
146
|
self.dismiss(None)
|
|
@@ -66,7 +66,7 @@ class NewIssueContainer(Container):
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
class NewIssueModal(ModalScreen[Issue | None]):
|
|
69
|
-
BINDINGS = [LazyGithubBindings.
|
|
69
|
+
BINDINGS = [LazyGithubBindings.CLOSE_DIALOG]
|
|
70
70
|
|
|
71
71
|
DEFAULT_CSS = """
|
|
72
72
|
NewIssueModal {
|
|
@@ -86,7 +86,7 @@ class NewIssueModal(ModalScreen[Issue | None]):
|
|
|
86
86
|
yield NewIssueContainer()
|
|
87
87
|
yield LazyGithubFooter()
|
|
88
88
|
|
|
89
|
-
def
|
|
89
|
+
def action_close(self) -> None:
|
|
90
90
|
self.dismiss()
|
|
91
91
|
|
|
92
92
|
@on(IssueCreated)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from textual import on, suggester, validation, work
|
|
2
2
|
from textual.app import ComposeResult
|
|
3
3
|
from textual.containers import Horizontal, VerticalScroll
|
|
4
|
-
from textual.message import Message
|
|
5
4
|
from textual.screen import ModalScreen
|
|
6
5
|
from textual.widgets import Button, Input, Label, Markdown, Rule, Switch, TextArea
|
|
7
6
|
|
|
@@ -9,23 +8,10 @@ from lazy_github.lib.bindings import LazyGithubBindings
|
|
|
9
8
|
from lazy_github.lib.context import LazyGithubContext
|
|
10
9
|
from lazy_github.lib.github.branches import list_branches
|
|
11
10
|
from lazy_github.lib.github.pull_requests import create_pull_request
|
|
12
|
-
from lazy_github.lib.messages import PullRequestCreated
|
|
11
|
+
from lazy_github.lib.messages import BranchesLoaded, PullRequestCreated
|
|
13
12
|
from lazy_github.models.github import Branch, FullPullRequest
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
class BranchesLoaded(Message):
|
|
17
|
-
def __init__(self, branches: list[Branch]) -> None:
|
|
18
|
-
super().__init__()
|
|
19
|
-
self.branches = branches
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class BranchesSelected(Message):
|
|
23
|
-
def __init__(self, head_ref: str, base_ref: str) -> None:
|
|
24
|
-
super().__init__()
|
|
25
|
-
self.head_ref = head_ref
|
|
26
|
-
self.base_ref = base_ref
|
|
27
|
-
|
|
28
|
-
|
|
29
15
|
class BranchSelection(Horizontal):
|
|
30
16
|
DEFAULT_CSS = """
|
|
31
17
|
BranchSelection {
|
|
@@ -202,12 +188,12 @@ class NewPullRequestModal(ModalScreen[FullPullRequest | None]):
|
|
|
202
188
|
}
|
|
203
189
|
"""
|
|
204
190
|
|
|
205
|
-
BINDINGS = [LazyGithubBindings.
|
|
191
|
+
BINDINGS = [LazyGithubBindings.CLOSE_DIALOG]
|
|
206
192
|
|
|
207
193
|
def compose(self) -> ComposeResult:
|
|
208
194
|
yield NewPullRequestContainer()
|
|
209
195
|
|
|
210
|
-
def
|
|
196
|
+
def action_close(self) -> None:
|
|
211
197
|
self.dismiss(None)
|
|
212
198
|
|
|
213
199
|
@on(PullRequestCreated)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from textual import on, work
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.containers import Container
|
|
4
|
+
from textual.coordinate import Coordinate
|
|
5
|
+
from textual.message import Message
|
|
6
|
+
from textual.screen import ModalScreen
|
|
7
|
+
from textual.widgets import Markdown, TabbedContent, TabPane
|
|
8
|
+
|
|
9
|
+
from lazy_github.lib.bindings import LazyGithubBindings
|
|
10
|
+
from lazy_github.lib.constants import BULLET_POINT, CHECKMARK
|
|
11
|
+
from lazy_github.lib.github_cli import fetch_notifications, mark_notification_as_read
|
|
12
|
+
from lazy_github.models.github import Notification
|
|
13
|
+
from lazy_github.ui.widgets.common import LazyGithubFooter, SearchableDataTable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotificationMarkedAsRead(Message):
|
|
17
|
+
def __init__(self, notification: Notification) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.notification = notification
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _NotificationsTableTabPane(TabPane):
|
|
23
|
+
def __init__(self, prefix: str, *args, **kwargs) -> None:
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self.notifications: dict[int, Notification] = {}
|
|
26
|
+
self.searchable_table: SearchableDataTable = SearchableDataTable(
|
|
27
|
+
table_id=f"{prefix}_notifications_table",
|
|
28
|
+
search_input_id=f"{prefix}_notifications_table_search_input",
|
|
29
|
+
sort_key="updated_at",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def compose(self) -> ComposeResult:
|
|
33
|
+
yield self.searchable_table
|
|
34
|
+
|
|
35
|
+
def remove_notification(self, notification: Notification) -> None:
|
|
36
|
+
self.searchable_table.table.remove_row(row_key=str(notification.id))
|
|
37
|
+
|
|
38
|
+
def add_notification(self, notification: Notification) -> None:
|
|
39
|
+
self.notifications[notification.id] = notification
|
|
40
|
+
self.searchable_table.table.add_row(
|
|
41
|
+
notification.updated_at.strftime("%c"),
|
|
42
|
+
notification.subject.subject_type,
|
|
43
|
+
notification.subject.title,
|
|
44
|
+
notification.reason.replace("_", " ").title(),
|
|
45
|
+
notification.id,
|
|
46
|
+
key=str(notification.id),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def on_mount(self) -> None:
|
|
50
|
+
self.searchable_table.loading = True
|
|
51
|
+
self.searchable_table.table.cursor_type = "row"
|
|
52
|
+
self.searchable_table.table.add_column("Updated At", key="updated_at")
|
|
53
|
+
self.searchable_table.table.add_column("Subject", key="subject")
|
|
54
|
+
self.searchable_table.table.add_column("Title", key="title")
|
|
55
|
+
self.searchable_table.table.add_column("Reason", key="reason")
|
|
56
|
+
self.searchable_table.table.add_column("Thread ID", key="id")
|
|
57
|
+
|
|
58
|
+
self.id_column = self.searchable_table.table.get_column_index("id")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ReadNotificationTabPane(_NotificationsTableTabPane):
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
super().__init__(id="read", prefix="read", title=f"[green]{CHECKMARK}Read[/green]")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class UnreadNotificationTabPane(_NotificationsTableTabPane):
|
|
67
|
+
BINDINGS = [LazyGithubBindings.MARK_NOTIFICATION_READ]
|
|
68
|
+
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
super().__init__(id="unread", prefix="unread", title=f"[red]{BULLET_POINT}Unread[/red]")
|
|
71
|
+
|
|
72
|
+
async def action_mark_read(self) -> None:
|
|
73
|
+
current_row = self.searchable_table.table.cursor_row
|
|
74
|
+
id_coord = Coordinate(current_row, self.id_column)
|
|
75
|
+
id = self.searchable_table.table.get_cell_at(id_coord)
|
|
76
|
+
notification_to_mark = self.notifications[int(id)]
|
|
77
|
+
|
|
78
|
+
self.post_message(NotificationMarkedAsRead(notification_to_mark))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class NotificationsContainer(Container):
|
|
82
|
+
DEFAULT_CSS = """
|
|
83
|
+
NotificationsContainer {
|
|
84
|
+
dock: top;
|
|
85
|
+
max-height: 80%;
|
|
86
|
+
align: center middle;
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
BINDINGS = [LazyGithubBindings.VIEW_READ_NOTIFICATIONS, LazyGithubBindings.VIEW_UNREAD_NOTIFICATIONS]
|
|
91
|
+
|
|
92
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
93
|
+
super().__init__(*args, **kwargs)
|
|
94
|
+
self.unread_tab = UnreadNotificationTabPane()
|
|
95
|
+
self.read_tab = ReadNotificationTabPane()
|
|
96
|
+
|
|
97
|
+
def compose(self) -> ComposeResult:
|
|
98
|
+
yield Markdown("# Notifications")
|
|
99
|
+
with TabbedContent():
|
|
100
|
+
yield self.unread_tab
|
|
101
|
+
yield self.read_tab
|
|
102
|
+
|
|
103
|
+
@on(NotificationMarkedAsRead)
|
|
104
|
+
async def notification_marked_read(self, message: NotificationMarkedAsRead) -> None:
|
|
105
|
+
await mark_notification_as_read(message.notification)
|
|
106
|
+
self.unread_tab.remove_notification(message.notification)
|
|
107
|
+
self.read_tab.add_notification(message.notification)
|
|
108
|
+
|
|
109
|
+
def action_view_read(self) -> None:
|
|
110
|
+
self.query_one(TabbedContent).active = "read"
|
|
111
|
+
self.read_tab.searchable_table.table.focus()
|
|
112
|
+
|
|
113
|
+
def action_view_unread(self) -> None:
|
|
114
|
+
self.query_one(TabbedContent).active = "unread"
|
|
115
|
+
self.unread_tab.searchable_table.table.focus()
|
|
116
|
+
|
|
117
|
+
@work
|
|
118
|
+
async def load_notifications(self) -> None:
|
|
119
|
+
notifications = await fetch_notifications(True)
|
|
120
|
+
|
|
121
|
+
unread_count = 0
|
|
122
|
+
for notification in notifications:
|
|
123
|
+
if notification.unread:
|
|
124
|
+
unread_count += 1
|
|
125
|
+
self.unread_tab.add_notification(notification)
|
|
126
|
+
else:
|
|
127
|
+
self.read_tab.add_notification(notification)
|
|
128
|
+
|
|
129
|
+
self.unread_tab.searchable_table.loading = False
|
|
130
|
+
self.read_tab.searchable_table.loading = False
|
|
131
|
+
|
|
132
|
+
if unread_count:
|
|
133
|
+
self.action_view_unread()
|
|
134
|
+
else:
|
|
135
|
+
self.action_view_read()
|
|
136
|
+
|
|
137
|
+
def on_mount(self) -> None:
|
|
138
|
+
self.read_tab.searchable_table.loading = True
|
|
139
|
+
self.unread_tab.searchable_table.loading = True
|
|
140
|
+
|
|
141
|
+
self.load_notifications()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class NotificationsModal(ModalScreen[None]):
|
|
145
|
+
DEFAULT_CSS = """
|
|
146
|
+
NotificationsModal {
|
|
147
|
+
height: 80%;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
NotificationsContainer {
|
|
151
|
+
width: 100;
|
|
152
|
+
max-height: 50;
|
|
153
|
+
border: thick $background 80%;
|
|
154
|
+
background: $surface-lighten-3;
|
|
155
|
+
}
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
BINDINGS = [LazyGithubBindings.CLOSE_DIALOG]
|
|
159
|
+
|
|
160
|
+
def compose(self) -> ComposeResult:
|
|
161
|
+
yield NotificationsContainer(id="notifications")
|
|
162
|
+
yield LazyGithubFooter()
|
|
163
|
+
|
|
164
|
+
async def action_close(self) -> None:
|
|
165
|
+
self.dismiss()
|
|
@@ -29,6 +29,7 @@ from lazy_github.lib.messages import (
|
|
|
29
29
|
from lazy_github.models.github import Repository
|
|
30
30
|
from lazy_github.ui.screens.new_issue import NewIssueModal
|
|
31
31
|
from lazy_github.ui.screens.new_pull_request import NewPullRequestModal
|
|
32
|
+
from lazy_github.ui.screens.notifications import NotificationsModal
|
|
32
33
|
from lazy_github.ui.screens.settings import SettingsModal
|
|
33
34
|
from lazy_github.ui.widgets.command_log import CommandLogSection
|
|
34
35
|
from lazy_github.ui.widgets.common import LazyGithubContainer, LazyGithubFooter
|
|
@@ -322,6 +323,7 @@ class MainScreenCommandProvider(Provider):
|
|
|
322
323
|
|
|
323
324
|
|
|
324
325
|
class LazyGithubMainScreen(Screen):
|
|
326
|
+
BINDINGS = [LazyGithubBindings.OPEN_NOTIFICATIONS_MODAL]
|
|
325
327
|
COMMANDS = {MainScreenCommandProvider}
|
|
326
328
|
notification_refresh_timer: Timer | None = None
|
|
327
329
|
|
|
@@ -331,6 +333,11 @@ class LazyGithubMainScreen(Screen):
|
|
|
331
333
|
yield MainViewPane()
|
|
332
334
|
yield LazyGithubFooter()
|
|
333
335
|
|
|
336
|
+
@work
|
|
337
|
+
async def action_view_notifications(self) -> None:
|
|
338
|
+
await self.app.push_screen_wait(NotificationsModal())
|
|
339
|
+
self.refresh_notification_count()
|
|
340
|
+
|
|
334
341
|
async def on_mount(self) -> None:
|
|
335
342
|
if LazyGithubContext.config.notifications.enabled:
|
|
336
343
|
self.refresh_notification_count()
|
|
@@ -205,7 +205,7 @@ class SettingsContainer(Container):
|
|
|
205
205
|
}
|
|
206
206
|
"""
|
|
207
207
|
|
|
208
|
-
BINDINGS = [LazyGithubBindings.SUBMIT_DIALOG, LazyGithubBindings.
|
|
208
|
+
BINDINGS = [LazyGithubBindings.SUBMIT_DIALOG, LazyGithubBindings.CLOSE_DIALOG, LazyGithubBindings.SEARCH_DIALOG]
|
|
209
209
|
|
|
210
210
|
def __init__(self) -> None:
|
|
211
211
|
super().__init__()
|
|
@@ -294,7 +294,7 @@ class SettingsContainer(Container):
|
|
|
294
294
|
async def cancel_settings(self, _: Button.Pressed) -> None:
|
|
295
295
|
self.post_message(SettingsModalDismissed(False))
|
|
296
296
|
|
|
297
|
-
async def
|
|
297
|
+
async def action_close(self) -> None:
|
|
298
298
|
self.post_message(SettingsModalDismissed(False))
|
|
299
299
|
|
|
300
300
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from textual import on, suggester, work
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.containers import Container, Horizontal
|
|
4
|
+
from textual.screen import ModalScreen
|
|
5
|
+
from textual.validation import Length
|
|
6
|
+
from textual.widgets import Button, Input, Label, Markdown
|
|
7
|
+
|
|
8
|
+
from lazy_github.lib.bindings import LazyGithubBindings
|
|
9
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
10
|
+
from lazy_github.lib.github.branches import list_branches
|
|
11
|
+
from lazy_github.lib.github.workflows import create_dispatch_event
|
|
12
|
+
from lazy_github.lib.messages import BranchesLoaded
|
|
13
|
+
from lazy_github.models.github import Workflow
|
|
14
|
+
from lazy_github.ui.widgets.common import LazyGithubFooter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TriggerWorkflowButtons(Horizontal):
|
|
18
|
+
DEFAULT_CSS = """
|
|
19
|
+
TriggerWorkflowButtons {
|
|
20
|
+
align: center middle;
|
|
21
|
+
height: auto;
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
24
|
+
Button {
|
|
25
|
+
margin: 1;
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def compose(self) -> ComposeResult:
|
|
30
|
+
yield Button("Trigger", id="trigger", variant="success")
|
|
31
|
+
yield Button("Cancel", id="cancel", variant="error")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TriggerWorkflowContainer(Container):
|
|
35
|
+
DEFAULT_CSS = """
|
|
36
|
+
TriggerWorkflowContainer {
|
|
37
|
+
align: center middle;
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, workflow: Workflow) -> None:
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.workflow = workflow
|
|
44
|
+
|
|
45
|
+
def compose(self) -> ComposeResult:
|
|
46
|
+
assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo in new PR modal"
|
|
47
|
+
yield Markdown(f"# Triggering workflow: {self.workflow.name}")
|
|
48
|
+
yield Label("[bold]Branch[/bold]")
|
|
49
|
+
yield Input(
|
|
50
|
+
id="branch_to_build",
|
|
51
|
+
placeholder="Choose a branch",
|
|
52
|
+
validators=Length(minimum=1),
|
|
53
|
+
value=LazyGithubContext.current_repo.default_branch,
|
|
54
|
+
)
|
|
55
|
+
yield TriggerWorkflowButtons()
|
|
56
|
+
|
|
57
|
+
@on(BranchesLoaded)
|
|
58
|
+
def handle_loaded_branches(self, message: BranchesLoaded) -> None:
|
|
59
|
+
self.branches = {b.name: b for b in message.branches}
|
|
60
|
+
branch_suggester = suggester.SuggestFromList(self.branches.keys())
|
|
61
|
+
self.query_one("#branch_to_build", Input).suggester = branch_suggester
|
|
62
|
+
|
|
63
|
+
@work
|
|
64
|
+
async def fetch_branches(self) -> None:
|
|
65
|
+
# This shouldn't happen since the current repo needs to be set to open this modal, but we'll validate it to
|
|
66
|
+
# make sure
|
|
67
|
+
assert LazyGithubContext.current_repo is not None, "Current repo unexpectedly missing in new PR modal"
|
|
68
|
+
|
|
69
|
+
branches = await list_branches(LazyGithubContext.current_repo)
|
|
70
|
+
self.post_message(BranchesLoaded(branches))
|
|
71
|
+
|
|
72
|
+
async def on_mount(self) -> None:
|
|
73
|
+
self.fetch_branches()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TriggerWorkflowModal(ModalScreen[bool]):
|
|
77
|
+
DEFAULT_CSS = """
|
|
78
|
+
TriggerWorkflowModal {
|
|
79
|
+
height: 80%;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
TriggerWorkflowContainer {
|
|
83
|
+
dock: top;
|
|
84
|
+
width: 60;
|
|
85
|
+
max-height: 20;
|
|
86
|
+
border: thick $background 80%;
|
|
87
|
+
background: $surface-lighten-3;
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
BINDINGS = [LazyGithubBindings.SUBMIT_DIALOG, LazyGithubBindings.CLOSE_DIALOG]
|
|
92
|
+
|
|
93
|
+
def __init__(self, workflow: Workflow) -> None:
|
|
94
|
+
super().__init__()
|
|
95
|
+
self.workflow = workflow
|
|
96
|
+
|
|
97
|
+
def compose(self) -> ComposeResult:
|
|
98
|
+
yield TriggerWorkflowContainer(self.workflow)
|
|
99
|
+
yield LazyGithubFooter()
|
|
100
|
+
|
|
101
|
+
@on(Button.Pressed, "#submit")
|
|
102
|
+
async def action_submit(self) -> None:
|
|
103
|
+
assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo!"
|
|
104
|
+
branch_input = self.query_one("#branch_to_build", Input)
|
|
105
|
+
branch_input.validate(branch_input.value)
|
|
106
|
+
if branch_input.is_valid:
|
|
107
|
+
if await create_dispatch_event(LazyGithubContext.current_repo, self.workflow, branch_input.value):
|
|
108
|
+
self.dismiss(True)
|
|
109
|
+
else:
|
|
110
|
+
self.notify(
|
|
111
|
+
"Could not trigger build - are you sure this workflow supports dispatch events?",
|
|
112
|
+
title="Error Triggering Build",
|
|
113
|
+
severity="error",
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
self.notify("You must enter a branch!", title="Validation Error", severity="error")
|
|
117
|
+
|
|
118
|
+
@on(Button.Pressed, "#cancel")
|
|
119
|
+
async def action_close(self) -> None:
|
|
120
|
+
self.dismiss(False)
|
|
@@ -4,7 +4,7 @@ from textual import on, work
|
|
|
4
4
|
from textual.app import ComposeResult
|
|
5
5
|
from textual.containers import Container, Vertical
|
|
6
6
|
from textual.events import Blur
|
|
7
|
-
from textual.widgets import DataTable,
|
|
7
|
+
from textual.widgets import DataTable, Footer, Input
|
|
8
8
|
|
|
9
9
|
from lazy_github.lib.bindings import LazyGithubBindings
|
|
10
10
|
|
|
@@ -186,7 +186,7 @@ class LazyGithubContainer(Container):
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
LazyGithubContainer:focus-within {
|
|
189
|
-
height: 40%;
|
|
189
|
+
min-height: 40%;
|
|
190
190
|
border: solid $success;
|
|
191
191
|
}
|
|
192
192
|
"""
|
|
@@ -3,15 +3,19 @@ from functools import partial
|
|
|
3
3
|
from textual import work
|
|
4
4
|
from textual.app import ComposeResult
|
|
5
5
|
from textual.containers import Container
|
|
6
|
+
from textual.coordinate import Coordinate
|
|
6
7
|
from textual.widgets import DataTable, TabbedContent, TabPane
|
|
7
8
|
|
|
9
|
+
from lazy_github.lib.bindings import LazyGithubBindings
|
|
8
10
|
from lazy_github.lib.github.workflows import list_workflow_runs, list_workflows
|
|
11
|
+
from lazy_github.lib.logging import lg
|
|
9
12
|
from lazy_github.models.github import Repository, Workflow, WorkflowRun
|
|
13
|
+
from lazy_github.ui.screens.trigger_workflow import TriggerWorkflowModal
|
|
10
14
|
from lazy_github.ui.widgets.common import LazilyLoadedDataTable, LazyGithubContainer
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
def workflow_to_cell(workflow: Workflow) -> tuple[str | int, ...]:
|
|
14
|
-
return (workflow.name, workflow.created_at.strftime("%c"), workflow.updated_at.strftime("%c"), workflow.path)
|
|
18
|
+
return (workflow.name, workflow.created_at.strftime("%c"), workflow.updated_at.strftime("%c"), str(workflow.path))
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
def workflow_run_to_cell(run: WorkflowRun) -> tuple[str | int, ...]:
|
|
@@ -19,6 +23,7 @@ def workflow_run_to_cell(run: WorkflowRun) -> tuple[str | int, ...]:
|
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class AvailableWorkflowsContainers(Container):
|
|
26
|
+
BINDINGS = [LazyGithubBindings.TRIGGER_WORKFLOW]
|
|
22
27
|
workflows: dict[str, Workflow] = {}
|
|
23
28
|
|
|
24
29
|
def compose(self) -> ComposeResult:
|
|
@@ -47,12 +52,14 @@ class AvailableWorkflowsContainers(Container):
|
|
|
47
52
|
self.table.add_column("Updated", key="updated")
|
|
48
53
|
self.table.add_column("Path", key="path")
|
|
49
54
|
|
|
55
|
+
self.path_column_id = self.table.get_column_index("path")
|
|
56
|
+
|
|
50
57
|
async def fetch_more_workflows(
|
|
51
58
|
self, repo: Repository, batch_size: int, batch_to_fetch: int
|
|
52
59
|
) -> list[tuple[str | int, ...]]:
|
|
53
60
|
next_page = await list_workflows(repo, page=batch_to_fetch, per_page=batch_size)
|
|
54
61
|
new_workflows = [w for w in next_page if not isinstance(w, Workflow)]
|
|
55
|
-
self.workflows.update({w.
|
|
62
|
+
self.workflows.update({w.path: w for w in new_workflows})
|
|
56
63
|
|
|
57
64
|
return [workflow_to_cell(w) for w in new_workflows]
|
|
58
65
|
|
|
@@ -61,7 +68,7 @@ class AvailableWorkflowsContainers(Container):
|
|
|
61
68
|
self.workflows = {}
|
|
62
69
|
rows = []
|
|
63
70
|
for workflow in workflows:
|
|
64
|
-
self.workflows[workflow.
|
|
71
|
+
self.workflows[workflow.path] = workflow
|
|
65
72
|
rows.append(workflow_to_cell(workflow))
|
|
66
73
|
|
|
67
74
|
self.searchable_table.set_rows(rows)
|
|
@@ -69,6 +76,17 @@ class AvailableWorkflowsContainers(Container):
|
|
|
69
76
|
self.searchable_table.can_load_more = True
|
|
70
77
|
self.searchable_table.current_batch = 1
|
|
71
78
|
|
|
79
|
+
def get_selected_workflow(self) -> Workflow:
|
|
80
|
+
workflow_path_coord = Coordinate(self.table.cursor_row, self.path_column_id)
|
|
81
|
+
return self.workflows[self.table.get_cell_at(workflow_path_coord)]
|
|
82
|
+
|
|
83
|
+
@work
|
|
84
|
+
async def action_trigger_workflow(self) -> None:
|
|
85
|
+
workflow = self.get_selected_workflow()
|
|
86
|
+
lg.info(f"Triggering workflow {workflow.name}")
|
|
87
|
+
if await self.app.push_screen_wait(TriggerWorkflowModal(workflow)):
|
|
88
|
+
self.notify("Successfully triggered workflow")
|
|
89
|
+
|
|
72
90
|
|
|
73
91
|
class WorkflowRunsContainer(Container):
|
|
74
92
|
workflow_runs: dict[int, WorkflowRun] = {}
|
|
@@ -33,8 +33,10 @@ lazy_github/ui/screens/edit_issue.py
|
|
|
33
33
|
lazy_github/ui/screens/new_comment.py
|
|
34
34
|
lazy_github/ui/screens/new_issue.py
|
|
35
35
|
lazy_github/ui/screens/new_pull_request.py
|
|
36
|
+
lazy_github/ui/screens/notifications.py
|
|
36
37
|
lazy_github/ui/screens/primary.py
|
|
37
38
|
lazy_github/ui/screens/settings.py
|
|
39
|
+
lazy_github/ui/screens/trigger_workflow.py
|
|
38
40
|
lazy_github/ui/widgets/command_log.py
|
|
39
41
|
lazy_github/ui/widgets/common.py
|
|
40
42
|
lazy_github/ui/widgets/conversations.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|