add gitignore
This commit is contained in:
1 parent
0d1ff65479
commit
f2c44dbc0d
8 files changed
+839
-77
No files matched your search
+361
-4
@@ -117,8 +117,9 @@ from textual.screen import ModalScreen # noqa: E402
|
||||
from textual.theme import Theme # noqa: E402
|
||||
from textual.widgets import ( # noqa: E402
|
||||
Button, Checkbox, ContentSwitcher, DirectoryTree, Footer, Input, Label, ListItem, ListView,
|
||||
RichLog, Select, Static, TabbedContent, TabPane, TextArea, Tree,
|
||||
OptionList, RichLog, Select, Static, TabbedContent, TabPane, TextArea, Tree,
|
||||
)
|
||||
from textual.widgets.option_list import Option # noqa: E402
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Colours (GitHub Desktop dark-ish)
|
||||
@@ -442,6 +443,10 @@ class Git:
|
||||
def file_diff(self, fc: FileChange, has_head: bool) -> str:
|
||||
if fc.kind == "?":
|
||||
return self._untracked_diff(fc.path)
|
||||
if fc.kind == "D" and has_head and os.path.exists(os.path.join(self.path, fc.path)):
|
||||
# un-tracked but still on disk: show what leaves the repository
|
||||
r = self.run("diff", "--cached", "--no-ext-diff", "HEAD", "--", fc.path, log=False)
|
||||
return r.out if r.ok else r.message
|
||||
args = ["diff", "HEAD" if has_head else EMPTY_TREE, "--no-ext-diff", "-M", "--"]
|
||||
if fc.orig:
|
||||
args.append(fc.orig)
|
||||
@@ -947,6 +952,69 @@ def existing_folder(path: str) -> str:
|
||||
return path if path and os.path.isdir(path) else os.path.expanduser("~")
|
||||
|
||||
|
||||
def gitignore_pattern(path: str, folder: bool = False) -> str:
|
||||
"""A .gitignore line matching exactly this path (from the repo root)."""
|
||||
p = path.replace("\\", "/").strip("/")
|
||||
p = re.sub(r"([*?\[])", r"\\\1", p) # glob characters match literally
|
||||
if p.endswith(" "):
|
||||
p = p[:-1] + "\\ " # a trailing space would be dropped
|
||||
return "/" + p + ("/" if folder else "")
|
||||
|
||||
|
||||
def parent_folders(path: str) -> List[str]:
|
||||
"""'a/b/c/file.txt' -> ['a/b/c', 'a/b', 'a'] (deepest first, like GitHub Desktop)."""
|
||||
parts = path.replace("\\", "/").split("/")[:-1]
|
||||
return ["/".join(parts[:i]) for i in range(len(parts), 0, -1)]
|
||||
|
||||
|
||||
def copy_to_system_clipboard(text: str) -> bool:
|
||||
"""Put text on the OS clipboard. Returns False if there's no way to (caller falls back to OSC 52)."""
|
||||
try:
|
||||
if IS_WIN:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
user32.OpenClipboard.argtypes = [wintypes.HWND]
|
||||
user32.OpenClipboard.restype = wintypes.BOOL
|
||||
user32.EmptyClipboard.restype = wintypes.BOOL
|
||||
user32.SetClipboardData.argtypes = [wintypes.UINT, wintypes.HANDLE]
|
||||
user32.SetClipboardData.restype = wintypes.HANDLE
|
||||
user32.CloseClipboard.restype = wintypes.BOOL
|
||||
kernel32.GlobalAlloc.argtypes = [wintypes.UINT, ctypes.c_size_t]
|
||||
kernel32.GlobalAlloc.restype = wintypes.HGLOBAL
|
||||
kernel32.GlobalLock.argtypes = [wintypes.HGLOBAL]
|
||||
kernel32.GlobalLock.restype = ctypes.c_void_p
|
||||
kernel32.GlobalUnlock.argtypes = [wintypes.HGLOBAL]
|
||||
data = text.encode("utf-16-le") + b"\0\0"
|
||||
handle = kernel32.GlobalAlloc(0x0002, len(data)) # GMEM_MOVEABLE
|
||||
ptr = kernel32.GlobalLock(handle)
|
||||
ctypes.memmove(ptr, data, len(data))
|
||||
kernel32.GlobalUnlock(handle)
|
||||
for _ in range(10): # another app may hold it briefly
|
||||
if user32.OpenClipboard(None):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
return False
|
||||
try:
|
||||
user32.EmptyClipboard()
|
||||
return bool(user32.SetClipboardData(13, handle)) # CF_UNICODETEXT
|
||||
finally:
|
||||
user32.CloseClipboard()
|
||||
if sys.platform == "darwin":
|
||||
return subprocess.run(["pbcopy"], input=text.encode("utf-8")).returncode == 0
|
||||
if os.environ.get("WAYLAND_DISPLAY") and shutil.which("wl-copy"):
|
||||
return subprocess.run(["wl-copy"], input=text.encode("utf-8")).returncode == 0
|
||||
if os.environ.get("DISPLAY"):
|
||||
for cmd in (["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]):
|
||||
if shutil.which(cmd[0]):
|
||||
return subprocess.run(cmd, input=text.encode("utf-8")).returncode == 0
|
||||
except (OSError, AttributeError, ValueError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def file_manager_name() -> str:
|
||||
return "Explorer" if IS_WIN else ("Finder" if sys.platform == "darwin" else "file manager")
|
||||
|
||||
@@ -1155,22 +1223,64 @@ class Tick(Static):
|
||||
style=f"bold {C_BLUE}" if self.checked else C_MUTED)
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
if event.button != 1: # right-click: let the row open its menu
|
||||
return
|
||||
event.stop()
|
||||
self.checked = not self.checked
|
||||
self.post_message(self.Toggled(self))
|
||||
|
||||
|
||||
def fit_path(path: str, width: int) -> str:
|
||||
"""Shorten a path in the middle so the file name stays visible (like GitHub Desktop)."""
|
||||
if width <= 0 or len(path) <= width:
|
||||
return path
|
||||
name = path.rsplit("/", 1)[-1]
|
||||
if len(name) + 8 > width: # the name alone barely fits: cut the middle of it all
|
||||
head = max(3, width // 3)
|
||||
return path[:head] + "…" + path[-(width - head - 1):]
|
||||
return path[:width - len(name) - 2] + "…/" + name
|
||||
|
||||
|
||||
class PathLabel(Static):
|
||||
"""A file path that fits its width, keeping the file name."""
|
||||
|
||||
def __init__(self, fc: FileChange, **kw) -> None:
|
||||
super().__init__(**kw)
|
||||
self.fc = fc
|
||||
|
||||
def render(self) -> Text:
|
||||
width = self.size.width
|
||||
shown = fit_path(self.fc.path, width - (len(self.fc.orig) + 3 if self.fc.orig and width > 60 else 0))
|
||||
t = Text(no_wrap=True, overflow="ellipsis")
|
||||
if self.fc.orig and width > 60:
|
||||
t.append(self.fc.orig + " → ", style=C_DIM)
|
||||
d, sep, name = shown.rpartition("/")
|
||||
if sep:
|
||||
t.append(d + "/", style=C_MUTED)
|
||||
t.append(name)
|
||||
return t
|
||||
|
||||
|
||||
class FileRow(ListItem):
|
||||
class MenuRequested(Message):
|
||||
def __init__(self, row: "FileRow", x: int, y: int) -> None:
|
||||
super().__init__()
|
||||
self.row, self.x, self.y = row, x, y
|
||||
|
||||
def __init__(self, fc: FileChange, checked: bool) -> None:
|
||||
super().__init__(classes="filerow")
|
||||
self.fc = fc
|
||||
self._checked = checked
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
if event.button == 3: # right-click -> context menu (the row also gets selected)
|
||||
self.post_message(self.MenuRequested(self, event.screen_x, event.screen_y))
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
icon, color, _ = KIND_ICON.get(self.fc.kind, ("●", C_YELLOW, ""))
|
||||
with Horizontal():
|
||||
yield Tick(self._checked, key=self.fc.path, classes="tick")
|
||||
yield Label(path_text(self.fc), classes="fpath")
|
||||
yield PathLabel(self.fc, classes="fpath")
|
||||
yield Label(Text(icon, style=f"bold {color}"), classes="fkind")
|
||||
|
||||
|
||||
@@ -1418,6 +1528,55 @@ class AddLocalDialog(ChooseFolderMixin, ModalScreen):
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
class ContextMenu(ModalScreen):
|
||||
"""A small popup menu at a screen position. Returns (item id, (x, y) of that item) or None.
|
||||
|
||||
items: (id, label) or (id, label, enabled), or None for a separator line.
|
||||
"""
|
||||
BINDINGS = [Binding("escape", "cancel", "Close"), Binding("left", "cancel", "Back", show=False)]
|
||||
|
||||
def __init__(self, items, x: int, y: int) -> None:
|
||||
super().__init__()
|
||||
self.items, self.x, self.y = items, x, y
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
options = []
|
||||
for item in self.items:
|
||||
if item is None:
|
||||
options.append(None)
|
||||
else:
|
||||
ident, label, *rest = item
|
||||
options.append(Option(Text(label), id=ident, disabled=bool(rest) and not rest[0]))
|
||||
yield OptionList(*options, id="ctx-menu")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
menu = self.query_one("#ctx-menu", OptionList)
|
||||
menu.styles.offset = (self.x, self.y)
|
||||
menu.focus()
|
||||
self.call_after_refresh(self._keep_on_screen)
|
||||
|
||||
def _keep_on_screen(self) -> None:
|
||||
menu = self.query_one("#ctx-menu", OptionList)
|
||||
w, h = menu.outer_size
|
||||
x = max(0, min(self.x, self.size.width - w))
|
||||
y = max(0, min(self.y, self.size.height - h))
|
||||
menu.styles.offset = (x, y)
|
||||
|
||||
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
||||
event.stop()
|
||||
menu = self.query_one("#ctx-menu", OptionList)
|
||||
anchor = (menu.region.right, menu.region.y + 1 + event.option_index - menu.scroll_offset.y)
|
||||
self.dismiss((event.option.id, anchor))
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
menu = self.query_one("#ctx-menu", OptionList)
|
||||
if not menu.region.contains(event.screen_x, event.screen_y):
|
||||
self.dismiss(None) # clicked outside the menu
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
class SimpleRow(ListItem):
|
||||
def __init__(self, value, *lines: Text) -> None:
|
||||
super().__init__()
|
||||
@@ -1992,6 +2151,10 @@ TabPane { padding: 0; height: 1fr; }
|
||||
#gitlog { height: 12; border-top: solid #444d56; background: #1f2428; }
|
||||
|
||||
ModalScreen { align: center middle; background: rgba(0, 0, 0, 0.55); }
|
||||
ContextMenu { align: left top; background: rgba(0, 0, 0, 0.15); }
|
||||
#ctx-menu { width: auto; min-width: 30; max-width: 80; height: auto; max-height: 24;
|
||||
background: #2f363d; border: round #586069; padding: 0; }
|
||||
#ctx-menu > .option-list--option { padding: 0 1; }
|
||||
.dialog { width: 76; max-width: 95%; height: auto; max-height: 90%;
|
||||
background: #2f363d; border: round #586069; padding: 1 2; }
|
||||
.dialog.wide { width: 90; }
|
||||
@@ -2041,6 +2204,8 @@ class GitDesk(App):
|
||||
Binding("l", "toggle_log", "Log"),
|
||||
Binding("r", "refresh", "Refresh"),
|
||||
Binding("space", "toggle_file", "Include file", show=False),
|
||||
Binding("m", "file_menu", "File menu", show=False),
|
||||
Binding("shift+f10", "file_menu", "File menu", show=False),
|
||||
Binding("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
@@ -2148,7 +2313,7 @@ class GitDesk(App):
|
||||
return False
|
||||
|
||||
APP_KEYS = {"fetch", "pull", "push", "branches", "stash", "stashes", "open_repo", "discard", "view_remote",
|
||||
"new_repo", "clone_repo", "add_remote", "repositories",
|
||||
"new_repo", "clone_repo", "add_remote", "repositories", "file_menu",
|
||||
"toggle_log", "refresh", "toggle_file", "quit"}
|
||||
|
||||
def check_action(self, action: str, parameters) -> Optional[bool]:
|
||||
@@ -2466,7 +2631,8 @@ class GitDesk(App):
|
||||
icon, color, label = KIND_ICON.get(fc.kind, ("●", C_YELLOW, ""))
|
||||
title = Text(no_wrap=True, overflow="ellipsis")
|
||||
title.append(f"{icon} ", style=f"bold {color}")
|
||||
title.append(fc.path, style="bold")
|
||||
room = self.query_one("#diff-header").size.width - len(label) - 48 # leave space for the buttons
|
||||
title.append(fit_path(fc.path, max(20, room)), style="bold")
|
||||
title.append(f" {label}", style=C_MUTED)
|
||||
self.query_one("#diff-title", Label).update(title)
|
||||
self.query_one("#open-file").display = True
|
||||
@@ -2628,6 +2794,160 @@ class GitDesk(App):
|
||||
self.update_counts()
|
||||
self.update_commit_button()
|
||||
|
||||
@on(FileRow.MenuRequested)
|
||||
def _row_menu(self, event: FileRow.MenuRequested) -> None:
|
||||
if not self.busy:
|
||||
self.run_worker(self.file_menu_flow(event.row.fc, event.x, event.y), exit_on_error=False)
|
||||
|
||||
def action_file_menu(self) -> None:
|
||||
lv = self.query_one("#files", ListView)
|
||||
row = lv.highlighted_child
|
||||
if self.mode == "changes" and isinstance(row, FileRow) and not self.busy:
|
||||
self.run_worker(self.file_menu_flow(row.fc, row.region.x + 6, row.region.y + 1), exit_on_error=False)
|
||||
|
||||
async def file_menu_flow(self, fc: FileChange, x: int, y: int) -> None:
|
||||
full = os.path.normpath(os.path.join(self.git.path, fc.path))
|
||||
exists = os.path.exists(full)
|
||||
ext = os.path.splitext(fc.path)[1]
|
||||
folders = parent_folders(fc.path)
|
||||
editor = "Visual Studio Code" if shutil.which("code") else editor_name()
|
||||
items = [
|
||||
("discard", "Discard changes...", not fc.is_conflict),
|
||||
None,
|
||||
("ignore_file", "Ignore file (add to .gitignore)"),
|
||||
("ignore_folder", "Ignore folder (add to .gitignore) >", bool(folders)),
|
||||
("ignore_ext", f"Ignore all {ext} files (add to .gitignore)" if ext else "Ignore all files of this type",
|
||||
bool(ext)),
|
||||
None,
|
||||
("copy_path", "Copy file path"),
|
||||
("copy_rel", "Copy relative file path"),
|
||||
None,
|
||||
("reveal", f"Show in {file_manager_name()}"),
|
||||
("open_editor", f"Open in {editor}", exists),
|
||||
("open_default", "Open with default program", exists),
|
||||
]
|
||||
res = await self.push_screen_wait(ContextMenu(items, x, y))
|
||||
if not res:
|
||||
return
|
||||
choice, (ax, ay) = res
|
||||
if choice == "discard":
|
||||
await self.discard_flow([fc])
|
||||
elif choice == "ignore_file":
|
||||
await self.ignore_flow(gitignore_pattern(fc.path))
|
||||
elif choice == "ignore_folder":
|
||||
sub = await self.push_screen_wait(ContextMenu([(f, "/" + f) for f in folders], ax, ay - 1))
|
||||
if sub:
|
||||
await self.ignore_flow(gitignore_pattern(sub[0], folder=True))
|
||||
elif choice == "ignore_ext":
|
||||
await self.ignore_flow("*" + re.sub(r"([*?\[])", r"\\\1", ext))
|
||||
elif choice == "copy_path":
|
||||
self.copy_text(full)
|
||||
elif choice == "copy_rel":
|
||||
self.copy_text(os.path.normpath(fc.path))
|
||||
elif choice == "reveal":
|
||||
self.reveal_in_file_manager(full)
|
||||
elif choice == "open_editor":
|
||||
self.action_open_editor(full)
|
||||
elif choice == "open_default":
|
||||
self.open_default(full)
|
||||
|
||||
def copy_text(self, text: str) -> None:
|
||||
if not copy_to_system_clipboard(text):
|
||||
self.copy_to_clipboard(text) # terminal clipboard (OSC 52) - works in Windows Terminal
|
||||
self.notify(f"Copied: {text}")
|
||||
|
||||
def reveal_in_file_manager(self, path: str) -> None:
|
||||
try:
|
||||
if IS_WIN:
|
||||
if os.path.exists(path):
|
||||
subprocess.Popen(f'explorer /select,"{path}"')
|
||||
else:
|
||||
os.startfile(os.path.dirname(path)) # type: ignore[attr-defined]
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.Popen(["open", "-R", path] if os.path.exists(path) else ["open", os.path.dirname(path)])
|
||||
elif shutil.which("xdg-open") and (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
|
||||
subprocess.Popen(["xdg-open", os.path.dirname(path)], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
else:
|
||||
self.notify("No file manager available here.", severity="warning")
|
||||
except OSError as e:
|
||||
self.notify(f"Couldn't open the file manager: {e}", severity="error")
|
||||
|
||||
def open_default(self, path: str) -> None:
|
||||
try:
|
||||
if IS_WIN:
|
||||
os.startfile(path) # type: ignore[attr-defined]
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.Popen(["open", path])
|
||||
elif shutil.which("xdg-open") and (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
|
||||
subprocess.Popen(["xdg-open", path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
else:
|
||||
self.notify("No desktop here to open files with.", severity="warning")
|
||||
return
|
||||
self.notify(f"Opening {os.path.basename(path)}")
|
||||
except OSError as e:
|
||||
self.notify(f"Couldn't open it: {e}", severity="error")
|
||||
|
||||
async def ignore_flow(self, pattern: str) -> None:
|
||||
"""Add a line to the repo's .gitignore; offer to stop tracking files it now covers."""
|
||||
if not self.idle_or_warn():
|
||||
return
|
||||
g = self.git
|
||||
ignore_file = os.path.join(g.path, ".gitignore")
|
||||
try:
|
||||
with open(ignore_file, encoding="utf-8", errors="replace") as fh:
|
||||
current = fh.read()
|
||||
except FileNotFoundError:
|
||||
current = ""
|
||||
except OSError as e:
|
||||
await self.error_dialog("Couldn't read .gitignore", str(e))
|
||||
return
|
||||
if pattern in (line.strip() for line in current.splitlines()):
|
||||
self.notify(f"{pattern} is already in .gitignore.")
|
||||
return
|
||||
|
||||
def tracked_ignored() -> Set[str]:
|
||||
r = g.run("ls-files", "-z", "--cached", "--ignored", "--exclude-standard", log=False)
|
||||
return {f for f in r.out.split("\0") if f}
|
||||
|
||||
before = await asyncio.to_thread(tracked_ignored)
|
||||
try:
|
||||
with open(ignore_file, "a", encoding="utf-8", newline="\n") as fh:
|
||||
if current and not current.endswith("\n"):
|
||||
fh.write("\n")
|
||||
fh.write(pattern + "\n")
|
||||
except OSError as e:
|
||||
await self.error_dialog("Couldn't update .gitignore", str(e))
|
||||
return
|
||||
self._log_cmd([f"(gitdesk added '{pattern}' to .gitignore)"], Result(0))
|
||||
newly = sorted(await asyncio.to_thread(tracked_ignored) - before)
|
||||
self.notify(f"Added {pattern} to .gitignore.")
|
||||
if newly:
|
||||
body = Text(f"{plural(len(newly), 'file')} matching {pattern} "
|
||||
f"{'is' if len(newly) == 1 else 'are'} already tracked by git, so ignoring "
|
||||
f"{'it' if len(newly) == 1 else 'them'} won't hide changes:\n\n")
|
||||
for f in newly[:10]:
|
||||
body.append(f" {f}\n", style="bold")
|
||||
if len(newly) > 10:
|
||||
body.append(f" … and {len(newly) - 10} more\n", style=C_MUTED)
|
||||
body.append("\nStop tracking them? They stay on your disk; they're removed from the repository "
|
||||
"in your next commit (anyone who pulls will see them deleted).", style=C_MUTED)
|
||||
choice = await self.ask("Stop tracking these files too?", body,
|
||||
[("untrack", "Stop tracking", "warning"), ("keep", "Keep tracking", "default")],
|
||||
cancel="keep")
|
||||
if choice == "untrack":
|
||||
async def flow():
|
||||
for i in range(0, len(newly), 100): # keep command lines short on Windows
|
||||
r = await self.gitop("--literal-pathspecs", "rm", "-q", "--cached", "--", *newly[i:i + 100])
|
||||
if not r.ok:
|
||||
await self.fail("Couldn't stop tracking the files", r)
|
||||
return
|
||||
self.notify(f"Stopped tracking {plural(len(newly), 'file')}. Commit to finish.")
|
||||
self.start_op("Updating", flow)
|
||||
return
|
||||
await self.refresh_state(force=True)
|
||||
|
||||
def action_toggle_file(self) -> None:
|
||||
lv = self.query_one("#files", ListView)
|
||||
if self.mode != "changes" or not lv.has_focus or not isinstance(lv.highlighted_child, FileRow):
|
||||
@@ -3236,9 +3556,30 @@ class GitDesk(App):
|
||||
if r.ok:
|
||||
now = await self.load_state()
|
||||
present = {f.path for f in now.files}
|
||||
# A "deleted" file that's still on disk was un-tracked (git rm --cached);
|
||||
# the reset above undid that, so redo it rather than adding the file back.
|
||||
untrack = [f.path for f in files if f.kind == "D"
|
||||
and os.path.exists(os.path.join(self.git.path, f.path))]
|
||||
specs = []
|
||||
for f in files:
|
||||
if f.path in untrack:
|
||||
continue
|
||||
specs += [p for p in (f.path, f.orig) if p and p in present]
|
||||
if untrack:
|
||||
r = await self.gitop("--literal-pathspecs", "rm", "-q", "--cached", "--", *untrack)
|
||||
if not r.ok:
|
||||
await self.fail("Commit failed", r)
|
||||
return
|
||||
if not specs and untrack:
|
||||
r = await self.gitop("commit", "--cleanup=whitespace", "-F", "-", input=message)
|
||||
if not r.ok:
|
||||
await self.fail("Commit failed", r)
|
||||
return
|
||||
self.query_one("#summary", Input).value = ""
|
||||
self.query_one("#description", TextArea).text = ""
|
||||
self.excluded.clear()
|
||||
self.notify(f"Committed: {summary}")
|
||||
return
|
||||
if not specs:
|
||||
self.notify("The ticked files have no changes left to commit.", severity="warning")
|
||||
return
|
||||
@@ -3638,7 +3979,23 @@ class GitDesk(App):
|
||||
await self.run_action(action)
|
||||
|
||||
|
||||
def selftest() -> int:
|
||||
"""Start the UI headless for a moment and quit - proves every module made it into a build."""
|
||||
import tempfile
|
||||
|
||||
async def autopilot(pilot) -> None:
|
||||
await pilot.pause(0.5)
|
||||
pilot.app.exit(0 if pilot.app.query("#tb-repo") else 1)
|
||||
|
||||
nowhere = os.path.join(tempfile.gettempdir(), "gitdesk-selftest-no-such-folder")
|
||||
rc = GitDesk(nowhere).run(headless=True, size=(120, 40), auto_pilot=autopilot)
|
||||
print("gitdesk selftest ok" if rc == 0 else f"gitdesk selftest failed ({rc})")
|
||||
return 0 if rc == 0 else 1
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--selftest":
|
||||
sys.exit(selftest())
|
||||
if shutil.which("git") is None:
|
||||
print("gitdesk: git isn't installed or isn't in your PATH.")
|
||||
sys.exit(1)
|
||||
|
||||
Reference in new issue
Block a user