Skip to content

Commit ea8bc06

Browse files
Emit terminal hyperlinks (#19)
Implements enough of the [terminal hyperlink specification][1] to allow us to include links to PRs, where they exist, in the terminal output. This specification is implemented by several terminals, including: - kitty - wezterm - iTerm2 - Alacritty - Windows Terminal - Konsole - xterm.js (VS Code's backing terminal implementation) See [this tracking repository][2] for a full listing. The ECMA-48 specification requires that terminals that do not understand the OSC-8 command code silently ignore it; in compliant terminals that do not implement the hyperlink specification, users will simply observe no changes to the output of `stack-pr`. For cases where outputting these escape codes is still undesirable I have added a `--no-hyperlinks` flag. [1]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda [2]: https://github.com/Alhadis/OSC8-Adoption/
1 parent 495a5f8 commit ea8bc06

File tree

1 file changed

+31
-13
lines changed

1 file changed

+31
-13
lines changed

src/stack_pr/cli.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ def base(self, base: str):
269269
def has_missing_info(self) -> bool:
270270
return None in (self._pr, self._head, self._base)
271271

272-
def pprint(self):
272+
def pprint(self, links: bool):
273273
s = b(self.commit.commit_id()[:8])
274274
pr_string = None
275275
if self.has_pr():
@@ -290,10 +290,14 @@ def pprint(self):
290290
if pr_string or branch_string:
291291
s += ")"
292292
s += ": " + self.commit.title()
293+
294+
if links and self.has_pr():
295+
s = link(self.pr, s)
296+
293297
return s
294298

295299
def __repr__(self):
296-
return self.pprint()
300+
return self.pprint(False)
297301

298302
def read_metadata(self):
299303
self.commit.commit_msg()
@@ -341,6 +345,16 @@ def red(s: str):
341345
return bcolors.FAIL + s + bcolors.ENDC
342346

343347

348+
# https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
349+
def link(location: str, text: str):
350+
"""
351+
Emits a link to the terminal using the terminal hyperlink specification.
352+
353+
Does not properly implement file URIs. Only use with web URIs.
354+
"""
355+
return f"\033]8;;{location}\033\\{text}\033]8;;\033\\"
356+
357+
344358
def error(msg):
345359
print(red("\nERROR: ") + msg)
346360

@@ -470,10 +484,10 @@ def verify(st: List[StackEntry], check_base: bool = False):
470484
raise RuntimeError
471485

472486

473-
def print_stack(st: List[StackEntry], level=1):
487+
def print_stack(st: List[StackEntry], links: bool, level=1):
474488
log(b("Stack:"), level=level)
475489
for e in reversed(st):
476-
log(" * " + e.pprint(), level=level)
490+
log(" * " + e.pprint(links), level=level)
477491

478492

479493
def draft_bitmask_type(value: str) -> List[bool]:
@@ -722,10 +736,11 @@ class CommonArgs(NamedTuple):
722736
head: str
723737
remote: str
724738
target: str
739+
hyperlinks: bool
725740

726741
@classmethod
727742
def from_args(cls, args: argparse.Namespace) -> "CommonArgs":
728-
return cls(args.base, args.head, args.remote, args.target)
743+
return cls(args.base, args.head, args.remote, args.target, args.hyperlinks)
729744

730745

731746
# If the base isn't explicitly specified, find the merge base between
@@ -745,7 +760,7 @@ def deduce_base(args: CommonArgs) -> CommonArgs:
745760
deduced_base = get_command_output(
746761
["git", "merge-base", args.head, f"{args.remote}/{args.target}"]
747762
)
748-
return CommonArgs(deduced_base, args.head, args.remote, args.target)
763+
return CommonArgs(deduced_base, args.head, args.remote, args.target, args.hyperlinks)
749764

750765

751766
def print_tips_after_export(st: List[StackEntry], args: CommonArgs):
@@ -797,7 +812,7 @@ def command_submit(
797812
# elements
798813
init_local_branches(st, args.remote)
799814
set_base_branches(st, args.target)
800-
print_stack(st)
815+
print_stack(st, args.hyperlinks)
801816

802817
# If the current branch contains commits from the stack, we will need to
803818
# rebase it in the end since the commits will be modified.
@@ -857,7 +872,7 @@ def command_submit(
857872
# LAND
858873
# ===----------------------------------------------------------------------=== #
859874
def rebase_pr(e: StackEntry, remote: str, target: str):
860-
log(b("Rebasing ") + e.pprint(), level=2)
875+
log(b("Rebasing ") + e.pprint(False), level=2)
861876
# Rebase the head branch to the most recent 'origin/main'
862877
run_shell_command(["git", "fetch", "--prune", remote])
863878
cmd = ["git", "checkout", f"{remote}/{e.head}", "-B", e.head]
@@ -883,7 +898,7 @@ def rebase_pr(e: StackEntry, remote: str, target: str):
883898

884899

885900
def land_pr(e: StackEntry, remote: str, target: str):
886-
log(b("Landing ") + e.pprint(), level=2)
901+
log(b("Landing ") + e.pprint(False), level=2)
887902
# Rebase the head branch to the most recent 'origin/main'
888903
run_shell_command(["git", "fetch", "--prune", remote])
889904
cmd = ["git", "checkout", f"{remote}/{e.head}", "-B", e.head]
@@ -965,7 +980,7 @@ def command_land(args: CommonArgs):
965980
# already be there from the metadata that commits need to have by that
966981
# point.
967982
set_base_branches(st, args.target)
968-
print_stack(st)
983+
print_stack(st, args.hyperlinks)
969984

970985
# Verify that the stack is correct before trying to land it.
971986
verify(st, check_base=True)
@@ -977,7 +992,7 @@ def command_land(args: CommonArgs):
977992
if len(st) > 1:
978993
log(h("Rebasing the rest of the stack"), level=1)
979994
prs_to_rebase = st[1:]
980-
print_stack(prs_to_rebase)
995+
print_stack(prs_to_rebase, args.hyperlinks)
981996
for e in prs_to_rebase:
982997
rebase_pr(e, args.remote, args.target)
983998
# Change the target of the new bottom-most PR in the stack to 'target'
@@ -1028,7 +1043,7 @@ def command_abandon(args: CommonArgs):
10281043

10291044
init_local_branches(st, args.remote)
10301045
set_base_branches(st, args.target)
1031-
print_stack(st)
1046+
print_stack(st, args.hyperlinks)
10321047

10331048
log(h("Stripping stack metadata from commit messages"), level=1)
10341049

@@ -1106,7 +1121,7 @@ def command_view(args: CommonArgs):
11061121

11071122
set_head_branches(st, args.remote)
11081123
set_base_branches(st, args.target)
1109-
print_stack(st)
1124+
print_stack(st, args.hyperlinks)
11101125
print_tips_after_view(st, args)
11111126
log(h(blue("SUCCESS!")), level=1)
11121127

@@ -1128,6 +1143,9 @@ def create_argparser() -> argparse.ArgumentParser:
11281143
common_parser.add_argument(
11291144
"-T", "--target", default="main", help="Remote target branch"
11301145
)
1146+
common_parser.add_argument(
1147+
"--hyperlinks", action=argparse.BooleanOptionalAction, default=True, help="Enable or disable hyperlink support."
1148+
)
11311149

11321150
parser_submit = subparsers.add_parser(
11331151
"submit",

0 commit comments

Comments
 (0)