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.
Files changed (49) hide show
  1. {lazy_github-0.2.3 → lazy_github-0.2.4}/PKG-INFO +1 -1
  2. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/bindings.py +25 -9
  3. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/config.py +0 -1
  4. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/constants.py +4 -3
  5. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/workflows.py +16 -1
  6. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github_cli.py +10 -3
  7. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/messages.py +7 -1
  8. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/models/github.py +19 -1
  9. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/edit_issue.py +2 -2
  10. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/new_comment.py +2 -2
  11. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/new_issue.py +2 -2
  12. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/new_pull_request.py +3 -17
  13. lazy_github-0.2.4/lazy_github/ui/screens/notifications.py +165 -0
  14. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/primary.py +7 -0
  15. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/settings.py +2 -2
  16. lazy_github-0.2.4/lazy_github/ui/screens/trigger_workflow.py +120 -0
  17. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/common.py +2 -2
  18. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/workflows.py +21 -3
  19. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/PKG-INFO +1 -1
  20. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/SOURCES.txt +2 -0
  21. {lazy_github-0.2.3 → lazy_github-0.2.4}/pyproject.toml +1 -1
  22. {lazy_github-0.2.3 → lazy_github-0.2.4}/LICENSE +0 -0
  23. {lazy_github-0.2.3 → lazy_github-0.2.4}/README.md +0 -0
  24. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/__main__.py +0 -0
  25. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/cli.py +0 -0
  26. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/context.py +0 -0
  27. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/git_cli.py +0 -0
  28. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/auth.py +0 -0
  29. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/branches.py +0 -0
  30. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/client.py +0 -0
  31. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/issues.py +0 -0
  32. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/pull_requests.py +0 -0
  33. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/github/repositories.py +0 -0
  34. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/logging.py +0 -0
  35. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/lib/utils.py +0 -0
  36. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/app.py +0 -0
  37. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/screens/auth.py +0 -0
  38. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/command_log.py +0 -0
  39. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/conversations.py +0 -0
  40. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/info.py +0 -0
  41. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/issues.py +0 -0
  42. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/pull_requests.py +0 -0
  43. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/ui/widgets/repositories.py +0 -0
  44. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github/version.py +0 -0
  45. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/dependency_links.txt +0 -0
  46. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/entry_points.txt +0 -0
  47. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/requires.txt +0 -0
  48. {lazy_github-0.2.3 → lazy_github-0.2.4}/lazy_github.egg-info/top_level.txt +0 -0
  49. {lazy_github-0.2.3 → lazy_github-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lazy-github
3
- Version: 0.2.3
3
+ Version: 0.2.4
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>
@@ -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", "Open command pallete", id="app.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", "Open new issue", id="issue.new")
14
- EDIT_ISSUE = Binding("E", "edit_issue", "Edit issue", id="issue.edit")
15
- OPEN_PULL_REQUEST = Binding("P", "open_pull_request", "Open new pull request", id="pull_request.new")
16
- NEW_COMMENT = Binding("n", "new_comment", "New comment", id="conversation.comment.new")
17
- REPLY_TO_REVIEW = Binding("r", "reply_to_review", "Reply to review", id="conversation.review.reply")
18
- REPLY_TO_COMMENT = Binding("r", "reply_to_individual_comment", "Reply to comment", id="conversation.comment.reply")
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 favorite", id="repositories.favorite")
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
- CANCEL_DIALOG = Binding("q, ESC", "cancel", "Cancel", id="modal.cancel")
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
@@ -40,7 +40,6 @@ class NotificationSettings(BaseModel):
40
40
  """Controls the settings for the optional notification feature, which relies on the standard GitHub CLI."""
41
41
 
42
42
  enabled: bool = False
43
- show_all_notifications: bool = True
44
43
 
45
44
 
46
45
  class BindingsSettings(BaseModel):
@@ -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
- IS_PRIVATE = "✔"
16
- IS_PUBLIC = "✘"
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 IS_PRIVATE if private else IS_PUBLIC
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[str]:
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[str] = []
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["subject"]["title"] for n in parsed]
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.CANCEL_DIALOG]
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 action_cancel(self) -> None:
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.CANCEL_DIALOG]
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 action_cancel(self) -> None:
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.CANCEL_DIALOG]
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 action_cancel(self) -> None:
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.CANCEL_DIALOG]
191
+ BINDINGS = [LazyGithubBindings.CLOSE_DIALOG]
206
192
 
207
193
  def compose(self) -> ComposeResult:
208
194
  yield NewPullRequestContainer()
209
195
 
210
- def action_cancel(self) -> None:
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.CANCEL_DIALOG, LazyGithubBindings.SEARCH_DIALOG]
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 action_cancel(self) -> None:
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, Input, Footer
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.number: w for w in new_workflows})
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.name] = 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] = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lazy-github
3
- Version: 0.2.3
3
+ Version: 0.2.4
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>
@@ -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
@@ -2,7 +2,7 @@
2
2
  name = "lazy-github"
3
3
  description = "A terminal UI for interacting with Github"
4
4
  readme = "README.md"
5
- version = "0.2.3"
5
+ version = "0.2.4"
6
6
  authors = [
7
7
  { name = "Chris Chapline", email = "gizmo385@users.noreply.github.com" },
8
8
  ]
File without changes
File without changes
File without changes