Compare commits
14 Commits
better-res
...
ghostty
| Author | SHA1 | Date | |
|---|---|---|---|
|
616489c69e
|
|||
|
8aa57ee3d7
|
|||
|
6576cc5e3c
|
|||
|
1a09eb588d
|
|||
|
c8cb209165
|
|||
|
645af1f660
|
|||
|
bb87d7d84c
|
|||
|
507d77a0de
|
|||
|
9f6e607e78
|
|||
|
3d68f08e1d
|
|||
|
cf3dba8fe4
|
|||
|
8ecb7d4382
|
|||
|
1e30e9bf9e
|
|||
|
fe6c7d8c12
|
BIN
.github/assets/CommandNotch-Demo.mp4
vendored
Normal file
BIN
.github/assets/CommandNotch-Open.png
vendored
Normal file
|
After Width: | Height: | Size: 846 KiB |
BIN
.github/assets/CommandNotch-Settings.png
vendored
Normal file
|
After Width: | Height: | Size: 575 KiB |
BIN
.github/assets/bch-receiving-address.png
vendored
Normal file
|
After Width: | Height: | Size: 58 KiB |
422
.github/demo/aurora.py
vendored
Executable file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Looping bathtub-and-duck terminal animation for Downterm demos."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import math
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
CSI = "\x1b["
|
||||
RESET = f"{CSI}0m"
|
||||
HIDE_CURSOR = f"{CSI}?25l"
|
||||
SHOW_CURSOR = f"{CSI}?25h"
|
||||
ALT_SCREEN_ON = f"{CSI}?1049h"
|
||||
ALT_SCREEN_OFF = f"{CSI}?1049l"
|
||||
CLEAR = f"{CSI}2J"
|
||||
HOME = f"{CSI}H"
|
||||
|
||||
TUB = "tub"
|
||||
TUB_DARK = "tub_dark"
|
||||
WATER = "water"
|
||||
WATER_DEEP = "water_deep"
|
||||
FOAM = "foam"
|
||||
BUBBLE = "bubble"
|
||||
DUCK = "duck"
|
||||
DUCK_LIGHT = "duck_light"
|
||||
BEAK = "beak"
|
||||
EYE = "eye"
|
||||
LABEL = "label"
|
||||
LABEL_SHADOW = "label_shadow"
|
||||
|
||||
RGB = {
|
||||
TUB: (171, 177, 191),
|
||||
TUB_DARK: (112, 118, 133),
|
||||
WATER: (71, 185, 214),
|
||||
WATER_DEEP: (36, 121, 161),
|
||||
FOAM: (196, 242, 250),
|
||||
BUBBLE: (184, 233, 245),
|
||||
DUCK: (255, 208, 64),
|
||||
DUCK_LIGHT: (255, 232, 132),
|
||||
BEAK: (255, 154, 58),
|
||||
EYE: (251, 251, 253),
|
||||
LABEL: (207, 212, 222),
|
||||
LABEL_SHADOW: (74, 79, 92),
|
||||
}
|
||||
|
||||
WATER_SHADE = " .:-=+*#"
|
||||
|
||||
|
||||
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render a looping bathtub-and-duck animation."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fps",
|
||||
type=float,
|
||||
default=18.0,
|
||||
help="Target frames per second. Default: 18",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speed",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Animation speed multiplier. Default: 1.0",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--duration",
|
||||
type=float,
|
||||
default=0.0,
|
||||
help="Optional run duration in seconds. Default: infinite",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
default="",
|
||||
help="Optional centered label, e.g. --label Downterm",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mono",
|
||||
action="store_true",
|
||||
help="Disable color and render as monochrome ASCII.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def terminal_size() -> tuple[int, int]:
|
||||
size = shutil.get_terminal_size((90, 28))
|
||||
return max(40, size.columns), max(12, size.lines)
|
||||
|
||||
|
||||
def clamp(value: float, low: float, high: float) -> float:
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def scene_size(width: int, height: int) -> tuple[int, int]:
|
||||
visible_height = max(8, height - 1)
|
||||
scene_width = min(max(48, width - 2), width, 110)
|
||||
scene_height = min(max(16, visible_height - 1), visible_height, 24)
|
||||
return scene_width, scene_height
|
||||
|
||||
|
||||
def make_canvas(width: int, height: int) -> list[list[tuple[str, str | None]]]:
|
||||
return [[(" ", None) for _ in range(width)] for _ in range(height)]
|
||||
|
||||
|
||||
def put(canvas: list[list[tuple[str, str | None]]], x: int, y: int, char: str, color: str | None) -> None:
|
||||
if 0 <= y < len(canvas) and 0 <= x < len(canvas[y]) and char:
|
||||
canvas[y][x] = (char[0], color)
|
||||
|
||||
|
||||
def draw_text(
|
||||
canvas: list[list[tuple[str, str | None]]],
|
||||
x: int,
|
||||
y: int,
|
||||
text: str,
|
||||
color: str | None,
|
||||
) -> None:
|
||||
for index, char in enumerate(text):
|
||||
if char != " ":
|
||||
put(canvas, x + index, y, char, color)
|
||||
|
||||
|
||||
def draw_sprite_text(
|
||||
canvas: list[list[tuple[str, str | None]]],
|
||||
x: int,
|
||||
y: int,
|
||||
text: str,
|
||||
color: str | None,
|
||||
) -> None:
|
||||
for index, char in enumerate(text):
|
||||
if char == " ":
|
||||
continue
|
||||
if char == "~":
|
||||
put(canvas, x + index, y, " ", None)
|
||||
continue
|
||||
put(canvas, x + index, y, char, color)
|
||||
|
||||
|
||||
def water_char(intensity: float) -> str:
|
||||
index = int(clamp(intensity, 0.0, 0.9999) * len(WATER_SHADE))
|
||||
return WATER_SHADE[index]
|
||||
|
||||
|
||||
def tub_geometry(scene_width: int, scene_height: int) -> dict[str, int]:
|
||||
top_y = 1
|
||||
notch_width = max(28, min(scene_width - 18, int(scene_width * 0.44)))
|
||||
rim_left = max(8, (scene_width - notch_width) // 2)
|
||||
rim_right = min(scene_width - 9, rim_left + notch_width)
|
||||
wall_left = rim_left + 1
|
||||
wall_right = rim_right - 1
|
||||
body_bottom = min(scene_height - 10, top_y + max(6, scene_height // 3))
|
||||
water_top = top_y + 2
|
||||
return {
|
||||
"rim_left": rim_left,
|
||||
"rim_right": rim_right,
|
||||
"top_y": top_y,
|
||||
"wall_left": wall_left,
|
||||
"wall_right": wall_right,
|
||||
"body_bottom": body_bottom,
|
||||
"water_top": water_top,
|
||||
}
|
||||
|
||||
|
||||
def draw_tub(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int]) -> None:
|
||||
rim_left = geometry["rim_left"]
|
||||
rim_right = geometry["rim_right"]
|
||||
top_y = geometry["top_y"]
|
||||
wall_left = geometry["wall_left"]
|
||||
wall_right = geometry["wall_right"]
|
||||
body_bottom = geometry["body_bottom"]
|
||||
|
||||
draw_text(canvas, 2, top_y, "_" * max(0, rim_left - 2), TUB)
|
||||
put(canvas, rim_left, top_y, "_", TUB)
|
||||
draw_text(canvas, rim_right + 1, top_y, "_" * max(0, len(canvas[0]) - rim_right - 3), TUB)
|
||||
put(canvas, rim_right, top_y, "_", TUB)
|
||||
|
||||
for y in range(top_y + 1, body_bottom):
|
||||
put(canvas, wall_left, y, "|", TUB)
|
||||
put(canvas, wall_right, y, "|", TUB)
|
||||
|
||||
put(canvas, wall_left, body_bottom, "\\", TUB_DARK)
|
||||
draw_text(canvas, wall_left + 1, body_bottom, "_" * max(0, wall_right - wall_left - 1), TUB_DARK)
|
||||
put(canvas, wall_right, body_bottom, "/", TUB_DARK)
|
||||
|
||||
|
||||
def draw_water_fill(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
|
||||
rim_left = geometry["wall_left"] + 1
|
||||
rim_right = geometry["wall_right"] - 1
|
||||
water_top = geometry["water_top"]
|
||||
body_bottom = geometry["body_bottom"] - 1
|
||||
|
||||
depth = max(1, body_bottom - water_top)
|
||||
for row in range(depth + 1):
|
||||
y = water_top + row
|
||||
for x in range(rim_left, rim_right + 1):
|
||||
wave = math.sin((x * 0.23) + (t * 2.1) + row * 0.55)
|
||||
shimmer = math.sin((x * 0.07) - (t * 1.35))
|
||||
if row == 0:
|
||||
char = "~" if wave > -0.22 else "-"
|
||||
color = FOAM if shimmer > 0.25 else WATER
|
||||
else:
|
||||
density = 0.40 + row / max(1, depth)
|
||||
value = 0.45 + (wave * 0.25) + (shimmer * 0.10) + density * 0.30
|
||||
char = water_char(value)
|
||||
color = WATER if row <= 2 else WATER_DEEP
|
||||
put(canvas, x, y, char, color)
|
||||
|
||||
|
||||
def draw_water_surface(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
|
||||
left = geometry["wall_left"] + 1
|
||||
right = geometry["wall_right"] - 1
|
||||
y = geometry["water_top"]
|
||||
|
||||
for x in range(left, right + 1):
|
||||
wave = math.sin((x * 0.23) + (t * 2.1))
|
||||
shimmer = math.sin((x * 0.11) - (t * 1.15))
|
||||
char = "~" if wave > -0.18 else "-"
|
||||
color = FOAM if shimmer > 0.35 else WATER
|
||||
put(canvas, x, y, char, color)
|
||||
|
||||
|
||||
def draw_bubbles(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
|
||||
rim_left = geometry["wall_left"]
|
||||
water_top = geometry["water_top"]
|
||||
|
||||
seeds = (
|
||||
(0.00, rim_left + 11, 0.9),
|
||||
(0.29, rim_left + 19, 1.1),
|
||||
(0.57, rim_left + 27, 0.8),
|
||||
(0.82, rim_left + 35, 1.0),
|
||||
)
|
||||
|
||||
for offset, base_x, speed in seeds:
|
||||
progress = (t * 0.11 * speed + offset) % 1.0
|
||||
y = water_top + 4 - int(progress * 9)
|
||||
x = base_x + int(round(math.sin((t * 0.8 * speed) + offset * 8.0) * 2))
|
||||
put(canvas, x, y, "o" if progress < 0.5 else ".", BUBBLE)
|
||||
|
||||
|
||||
def draw_duck(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
|
||||
rim_left = geometry["wall_left"]
|
||||
rim_right = geometry["wall_right"]
|
||||
water_top = geometry["water_top"]
|
||||
|
||||
span = rim_right - rim_left
|
||||
bob = math.sin(t * 1.35) * 0.55
|
||||
drift = math.sin(t * 0.55) * 2.0
|
||||
|
||||
blink_phase = int(t * 2.1) % 12
|
||||
eye_char = "-" if blink_phase == 7 else "."
|
||||
x = int(rim_left + span * 0.34 + drift)
|
||||
y = int(water_top - 1 + bob)
|
||||
|
||||
rows = [
|
||||
" __",
|
||||
f" ___(~{eye_char})>",
|
||||
" \\~<_.~) ",
|
||||
" `---' ",
|
||||
]
|
||||
colors = [DUCK_LIGHT, DUCK, DUCK, DUCK]
|
||||
|
||||
for index, row in enumerate(rows):
|
||||
draw_sprite_text(canvas, x, y + index, row, colors[index])
|
||||
|
||||
put(canvas, x + 8, y + 1, ">", BEAK)
|
||||
put(canvas, x + 6, y + 1, eye_char, EYE)
|
||||
|
||||
if 0.74 < (t * 0.18) % 1.0 < 0.90:
|
||||
splash_x = x + 9
|
||||
splash_y = geometry["water_top"] - 1
|
||||
for dx, dy, char in ((0, 0, "."), (2, -1, "o"), (4, 0, "."), (5, -2, "o")):
|
||||
put(canvas, splash_x + dx, splash_y + dy, char, BUBBLE)
|
||||
|
||||
|
||||
def logo_rows(text: str) -> list[str]:
|
||||
if text == "commandNotch":
|
||||
return [
|
||||
" ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ ",
|
||||
" ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗",
|
||||
" ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║",
|
||||
" ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║",
|
||||
" ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝",
|
||||
" ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ",
|
||||
"",
|
||||
" ███╗ ██╗ ██████╗ ████████╗ ██████╗██╗ ██╗",
|
||||
" ████╗ ██║██╔═══██╗╚══██╔══╝██╔════╝██║ ██║",
|
||||
" ██╔██╗ ██║██║ ██║ ██║ ██║ ███████║",
|
||||
" ██║╚██╗██║██║ ██║ ██║ ██║ ██╔══██║",
|
||||
" ██║ ╚████║╚██████╔╝ ██║ ╚██████╗██║ ██║",
|
||||
" ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝",
|
||||
]
|
||||
|
||||
return [text]
|
||||
|
||||
|
||||
def draw_label(canvas: list[list[tuple[str, str | None]]], label: str) -> None:
|
||||
logo = logo_rows(label or "commandNotch")
|
||||
start_y = len(canvas) - len(logo) - 1
|
||||
|
||||
for index, row in enumerate(logo):
|
||||
start_x = max(0, (len(canvas[0]) - len(row)) // 2)
|
||||
draw_text(canvas, start_x, start_y + index, row, LABEL)
|
||||
|
||||
|
||||
def scene_canvas(scene_width: int, scene_height: int, t: float, label: str) -> list[list[tuple[str, str | None]]]:
|
||||
canvas = make_canvas(scene_width, scene_height)
|
||||
geometry = tub_geometry(scene_width, scene_height)
|
||||
draw_tub(canvas, geometry)
|
||||
draw_water_fill(canvas, geometry, t)
|
||||
draw_water_surface(canvas, geometry, t)
|
||||
draw_duck(canvas, geometry, t)
|
||||
draw_bubbles(canvas, geometry, t)
|
||||
draw_label(canvas, label)
|
||||
return canvas
|
||||
|
||||
|
||||
def ansi_rgb(color_name: str | None) -> str:
|
||||
if color_name is None:
|
||||
return RESET
|
||||
red, green, blue = RGB[color_name]
|
||||
return f"{CSI}38;2;{red};{green};{blue}m"
|
||||
|
||||
|
||||
def render_scene_line(cells: list[tuple[str, str | None]], mono: bool) -> str:
|
||||
if mono:
|
||||
return "".join(char for char, _ in cells)
|
||||
|
||||
chunks: list[str] = []
|
||||
current_color: str | None = None
|
||||
for char, color in cells:
|
||||
if color != current_color:
|
||||
chunks.append(ansi_rgb(color))
|
||||
current_color = color
|
||||
chunks.append(char)
|
||||
chunks.append(RESET)
|
||||
return "".join(chunks)
|
||||
|
||||
|
||||
def render_frame(width: int, height: int, t: float, label: str, mono: bool) -> str:
|
||||
visible_height = max(8, height - 1)
|
||||
scene_width, scene_height = scene_size(width, height)
|
||||
scene = scene_canvas(scene_width, scene_height, t, label)
|
||||
top_padding = max(0, (visible_height - scene_height) // 2)
|
||||
left_padding = max(0, (width - scene_width) // 2)
|
||||
|
||||
pieces: list[str] = [HOME]
|
||||
blank_line = " " * width
|
||||
|
||||
for row in range(visible_height):
|
||||
if top_padding <= row < top_padding + scene_height:
|
||||
scene_row = scene[row - top_padding]
|
||||
rendered = render_scene_line(scene_row, mono)
|
||||
right_padding = max(0, width - left_padding - scene_width)
|
||||
line = (" " * left_padding) + rendered + (" " * right_padding)
|
||||
else:
|
||||
line = blank_line
|
||||
|
||||
pieces.append(line)
|
||||
if row != visible_height - 1:
|
||||
pieces.append("\n")
|
||||
|
||||
return "".join(pieces)
|
||||
|
||||
|
||||
def restore_terminal() -> None:
|
||||
sys.stdout.write(f"{RESET}{SHOW_CURSOR}{ALT_SCREEN_OFF}")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def install_cleanup() -> None:
|
||||
def handle_signal(signum, _frame) -> None:
|
||||
restore_terminal()
|
||||
raise SystemExit(128 + signum)
|
||||
|
||||
atexit.register(restore_terminal)
|
||||
signal.signal(signal.SIGINT, handle_signal)
|
||||
signal.signal(signal.SIGTERM, handle_signal)
|
||||
|
||||
|
||||
def main(argv: Sequence[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
if args.fps <= 0:
|
||||
raise SystemExit("--fps must be greater than 0")
|
||||
if args.speed <= 0:
|
||||
raise SystemExit("--speed must be greater than 0")
|
||||
|
||||
install_cleanup()
|
||||
|
||||
sys.stdout.write(f"{ALT_SCREEN_ON}{HIDE_CURSOR}{CLEAR}")
|
||||
sys.stdout.flush()
|
||||
|
||||
frame_interval = 1.0 / args.fps
|
||||
start = time.perf_counter()
|
||||
frame_index = 0
|
||||
|
||||
while True:
|
||||
now = time.perf_counter()
|
||||
elapsed = now - start
|
||||
if args.duration > 0 and elapsed >= args.duration:
|
||||
break
|
||||
|
||||
width, height = terminal_size()
|
||||
t = elapsed * args.speed
|
||||
sys.stdout.write(render_frame(width, height, t, args.label, args.mono))
|
||||
sys.stdout.flush()
|
||||
|
||||
frame_index += 1
|
||||
target = start + (frame_index * frame_interval)
|
||||
sleep_for = target - time.perf_counter()
|
||||
if sleep_for > 0:
|
||||
time.sleep(sleep_for)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
7
.gitignore
vendored
@@ -79,7 +79,8 @@ jspm_packages/
|
||||
dist/
|
||||
build/
|
||||
|
||||
**/Release*
|
||||
CommandNotch 20*
|
||||
# Mac... files
|
||||
**/.DS_Store
|
||||
|
||||
**/.DS_Store
|
||||
# Releases
|
||||
releases/
|
||||
89
CONTRIBUTING.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for contributing to CommandNotch.
|
||||
|
||||
## Before You Start
|
||||
|
||||
- Use macOS 14+.
|
||||
- Install Xcode 16+.
|
||||
- Install `xcodegen` with Homebrew.
|
||||
|
||||
## Local Setup
|
||||
|
||||
```bash
|
||||
cd CommandNotch
|
||||
xcodegen generate --spec project.yml
|
||||
open CommandNotch.xcodeproj
|
||||
```
|
||||
|
||||
## Useful Commands
|
||||
|
||||
Generate the project:
|
||||
|
||||
```bash
|
||||
cd CommandNotch
|
||||
xcodegen generate --spec project.yml
|
||||
```
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
cd CommandNotch
|
||||
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
|
||||
xcodebuild build -project CommandNotch.xcodeproj -scheme CommandNotch -destination 'platform=macOS'
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
cd CommandNotch
|
||||
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
|
||||
xcodebuild test -project CommandNotch.xcodeproj -scheme CommandNotch -destination 'platform=macOS'
|
||||
```
|
||||
|
||||
## What Helps Most
|
||||
|
||||
- Bug fixes with a clear reproduction path.
|
||||
- UI polish that keeps the app feeling intentional instead of generic.
|
||||
- Accessibility improvements.
|
||||
- Tests around workspace, screen, settings, and hotkey behavior.
|
||||
- Docs and onboarding improvements.
|
||||
|
||||
## Code Guidelines
|
||||
|
||||
- Keep changes targeted.
|
||||
- Preserve the existing SwiftUI + AppKit split.
|
||||
- Prefer typed settings and explicit state ownership over hidden side effects.
|
||||
- Add or update tests when you change behavior.
|
||||
- Regenerate the Xcode project with XcodeGen if you add or remove files.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- Keep PRs small enough to review comfortably.
|
||||
- Explain the user-facing impact.
|
||||
- Note any follow-up work or tradeoffs.
|
||||
- Include screenshots for visible UI changes when possible.
|
||||
- Mention the exact build/test command you used.
|
||||
|
||||
## Issues
|
||||
|
||||
When filing a bug, include:
|
||||
|
||||
- macOS version
|
||||
- what you expected
|
||||
- what actually happened
|
||||
- reproduction steps
|
||||
- screenshots or recordings if they help
|
||||
|
||||
## Media Updates
|
||||
|
||||
README screenshots live in `.github/assets/`.
|
||||
|
||||
If you update the UI significantly, refresh:
|
||||
|
||||
- `.github/assets/CommandNotch-Open.png`
|
||||
- `.github/assets/CommandNotch-Settings.png`
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be released under the MIT License in this repository.
|
||||
863
CommandNotch/CommandNotch.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,863 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
088FE22B3308AAB25027E086 /* NotchShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F57837A7115DEEE11E14B40 /* NotchShape.swift */; };
|
||||
0DE8E7F29FBAA7E2B613E221 /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3C8C11834931A9D53BC2B6 /* AppearanceSettingsView.swift */; };
|
||||
0F133E8A88D2E313D90C32AD /* WindowFrameCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */; };
|
||||
12F68EDA880030DAB644FF5F /* LaunchAtLoginHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210D7783A614FD7190F5DDD /* LaunchAtLoginHelper.swift */; };
|
||||
154F363D434A26105C5999B5 /* WorkspaceRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591FCE91AF83A8A8E44E1625 /* WorkspaceRegistryTests.swift */; };
|
||||
187F4B521BFC3BD29ADA79E3 /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB7FE2BDE5C25B7E599F340 /* HotkeyManager.swift */; };
|
||||
1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */; };
|
||||
2089566A2BBAA65EA82119B3 /* NotchOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6D136A0B3FC79DDE12A826 /* NotchOrchestratorTests.swift */; };
|
||||
21373D6E9C2F34FD63CEC6A5 /* TerminalScrollCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */; };
|
||||
229567E62C617B045E92E0FE /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 1375B54FF94D210B46F18540 /* ghostty */; };
|
||||
2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */; };
|
||||
23E2DDCF36D0DAB2EA72C39C /* NotchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */; };
|
||||
26AE379040149CBE05B314BB /* WorkspacesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70A31EFACF23DD9262A040E /* WorkspacesSettingsView.swift */; };
|
||||
278010607B0D552DCC8996C5 /* CommandNotchUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 726B935606FD961FD7E8C2BE /* CommandNotchUITests.swift */; };
|
||||
28F609FCF422E735F567EE32 /* SwiftTermBackendSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3942F3843D0A13C145576760 /* SwiftTermBackendSession.swift */; };
|
||||
2DF22798D3A7514E2A9183FC /* WorkspaceSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */; };
|
||||
34AF69BF7AF8DC78ADE3774A /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */; };
|
||||
3B69CB3CDEC2E5F2DCE600F9 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */; };
|
||||
3DC413CDD97DF0233F039518 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D58535EA23D3C2A5D0520DB3 /* Carbon.framework */; };
|
||||
40183737D8C237022D0882FD /* TerminalScrollbackEstimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */; };
|
||||
4BA9D6274CFD6A87DC397B8E /* MouseAwareTerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982BA8965FE92A59D66F378B /* MouseAwareTerminalView.swift */; };
|
||||
4CFA0C79095ACE0A327A2469 /* WorkspaceRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */; };
|
||||
4D335F67B71F7DD977B6AEF9 /* AppSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C98C406899F4B242075AF /* AppSettingsStore.swift */; };
|
||||
4E0AD14B7427532271E485AA /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */; };
|
||||
507A67E770DEFAF5BC321FCF /* WindowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9220E02B6D470DD05CA540C /* WindowCoordinator.swift */; };
|
||||
5379DF2FACE924BDDB584377 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = C921E6435A64AA07A0FEA4D5 /* SwiftTerm */; };
|
||||
567914B84D0EEF6E6E9149A9 /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = 3A03006228968FE12FCBF588 /* terminfo */; };
|
||||
5A3FDFCF30A1AAFE070290E9 /* SettingsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 728B3125F7F7FDB7313D2DC6 /* SettingsWindowController.swift */; };
|
||||
5F3534F66A5DBE4E081AFFA6 /* SwiftTermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C55F29B779DA0E5C5FC8627 /* SwiftTermView.swift */; };
|
||||
65C7DB7296C6C6A77598A1F4 /* TerminalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E37A6DCD9C5DE1FE11C4C1CD /* TerminalSettingsView.swift */; };
|
||||
6624A60CB121A22A03365071 /* WorkspaceSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF0FFBC96F2446687D6474F4 /* WorkspaceSwitcherView.swift */; };
|
||||
6A18F6635B509FF58669F505 /* HotkeySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03D042117E59DCA9D553844 /* HotkeySettingsView.swift */; };
|
||||
6DD4DAE72E5C3858B230D94C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A8DCFA77626F9999B432FE /* AppDelegate.swift */; };
|
||||
6F249EDFA2D654457DF385F1 /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567E85A2ED628460CEC760DB /* TerminalManager.swift */; };
|
||||
771088361F981A9AAE976F3C /* SettingsBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900F0476BE9E3600FBD371BB /* SettingsBindings.swift */; };
|
||||
787477BB6D2F8AC723EEDA7D /* ScreenContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7181BB1F3926B457445105E5 /* ScreenContext.swift */; };
|
||||
7A69B5AAC686174BCA54D0F0 /* TerminalSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E1791BB45E1505500ACC67 /* TerminalSession.swift */; };
|
||||
7A7D3455D50D041CF698E786 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74463E4EAB78F56345360CD5 /* AppSettings.swift */; };
|
||||
7AB14019B5CC6ED84B96FA47 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6ADD641A088A5005DD0EB4A6 /* Assets.xcassets */; };
|
||||
7D5F3C3B5E173B6F06901812 /* AppSettingsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CA6F2C2CA32D0AA58F6C43 /* AppSettingsStoreTests.swift */; };
|
||||
86BB4A41E8EC44A5F45CE995 /* WorkspaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */; };
|
||||
88113BA9B217DA579C36BEBE /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72A1D3D12BAC593838B3125C /* TabBar.swift */; };
|
||||
88D87155D79C493D8956AA3B /* AnimationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE99DBAB656EA80A117D2EE1 /* AnimationSettingsView.swift */; };
|
||||
8A1B2A4CD61B0D9A4BAB075B /* ScreenRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7F7D8D15A1BC4EE43DDDB /* ScreenRegistryTests.swift */; };
|
||||
99262FD443753BA518221CBD /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DB445256B2E0228F7679EE7 /* GhosttyKit.xcframework */; };
|
||||
9F535B9516A4AE9FB536B1BD /* HotkeyBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BB1C403BC2157756F572ACF /* HotkeyBinding.swift */; };
|
||||
A74E14B8AD19C53820853D8E /* NSScreen+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDC7451F0E7ACFE0BEC5473 /* NSScreen+Extensions.swift */; };
|
||||
AE8215538D4B026A00BAD241 /* WorkspaceStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B957BD1F6120D2592613ED /* WorkspaceStore.swift */; };
|
||||
B406183CC7B5DCE76989A066 /* AppSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB28950392C0198E69F3564B /* AppSettingsController.swift */; };
|
||||
B605A9311557251A85183383 /* AboutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48198AFE5473B0F7AECAB3FB /* AboutSettingsView.swift */; };
|
||||
C84FA3100884649FE92BF5DD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A64A11F27E65B342B991629A /* ContentView.swift */; };
|
||||
CFBBE994BA6BAD4658AAB9CB /* TerminalCommandArrowBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D19729317029008D81F361 /* TerminalCommandArrowBehaviorTests.swift */; };
|
||||
D088BC850F37E717844761C6 /* CommandNotchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5159CB9DBE2BAA0D2E201C39 /* CommandNotchApp.swift */; };
|
||||
D2468B19D6F0A2C1DFDFE2B7 /* ScreenContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */; };
|
||||
D4CEB4B895F75D91FA892A06 /* PopoutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB593A2546BF2C0BE8E40387 /* PopoutWindowController.swift */; };
|
||||
D6BBC4DE23DEA39D9713469A /* TerminalScrollWheelRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7315EA485A52A8DC42DF925E /* TerminalScrollWheelRouterTests.swift */; };
|
||||
DAD3AB4A0DAADA32C02D959E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3125FD3DC55420122CF85D80 /* SettingsView.swift */; };
|
||||
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22AA47452CF798A977A6F47 /* TerminalCommandArrowBehavior.swift */; };
|
||||
DD116B0FCC341D66F1534EC4 /* TerminalScrollbackEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */; };
|
||||
E63FD5862C0EC54E284F6A0F /* ScreenRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */; };
|
||||
E792411FA82E79E810F4B4C3 /* HotkeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */; };
|
||||
EE72479BA5A25FF31BACCC50 /* NotchOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */; };
|
||||
F8ED522FDC96B1F9AD8933F7 /* ScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB8AF76C0F728897A26D7EF /* ScreenManager.swift */; };
|
||||
F8F8DE2F26608259D72635B7 /* AppSettingsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5FF5623898FA150C3B70D4 /* AppSettingsControllerTests.swift */; };
|
||||
FF6BB7E881BCBB4FD6CC4011 /* GhosttyBackendSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFC12727EA1C097E0EF9DF2 /* GhosttyBackendSession.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
13E720E97D079D298D124BBE /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = E99C2EDB39F7B64C1540BCA8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D5585E5732CD067DF6EF0C69;
|
||||
remoteInfo = CommandNotch;
|
||||
};
|
||||
9D6387038E18398CA764147F /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = E99C2EDB39F7B64C1540BCA8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D5585E5732CD067DF6EF0C69;
|
||||
remoteInfo = CommandNotch;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRegistry.swift; sourceTree = "<group>"; };
|
||||
1375B54FF94D210B46F18540 /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = CommandNotch/Resources/ghostty; sourceTree = SOURCE_ROOT; };
|
||||
165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollbackEstimator.swift; sourceTree = "<group>"; };
|
||||
27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
|
||||
297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = "<group>"; };
|
||||
2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderView.swift; sourceTree = "<group>"; };
|
||||
3125FD3DC55420122CF85D80 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
35CBC14E11EBD8486457CE91 /* CommandNotchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommandNotchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3942F3843D0A13C145576760 /* SwiftTermBackendSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermBackendSession.swift; sourceTree = "<group>"; };
|
||||
39CA6F2C2CA32D0AA58F6C43 /* AppSettingsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsStoreTests.swift; sourceTree = "<group>"; };
|
||||
3A03006228968FE12FCBF588 /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = CommandNotch/Resources/terminfo; sourceTree = SOURCE_ROOT; };
|
||||
3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceController.swift; sourceTree = "<group>"; };
|
||||
3F57837A7115DEEE11E14B40 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
|
||||
3F5FF5623898FA150C3B70D4 /* AppSettingsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsControllerTests.swift; sourceTree = "<group>"; };
|
||||
40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
48198AFE5473B0F7AECAB3FB /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = "<group>"; };
|
||||
496267F03E261FEC9EBD5A9D /* CommandNotchUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommandNotchUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
49E1791BB45E1505500ACC67 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = "<group>"; };
|
||||
4AFC12727EA1C097E0EF9DF2 /* GhosttyBackendSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyBackendSession.swift; sourceTree = "<group>"; };
|
||||
4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchState.swift; sourceTree = "<group>"; };
|
||||
4DB445256B2E0228F7679EE7 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GhosttyKit.xcframework; path = Vendor/GhosttyKit.xcframework; sourceTree = "<group>"; };
|
||||
4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStoreTests.swift; sourceTree = "<group>"; };
|
||||
5159CB9DBE2BAA0D2E201C39 /* CommandNotchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandNotchApp.swift; sourceTree = "<group>"; };
|
||||
567E85A2ED628460CEC760DB /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = "<group>"; };
|
||||
591FCE91AF83A8A8E44E1625 /* WorkspaceRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRegistryTests.swift; sourceTree = "<group>"; };
|
||||
5DDC7451F0E7ACFE0BEC5473 /* NSScreen+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+Extensions.swift"; sourceTree = "<group>"; };
|
||||
6ADD641A088A5005DD0EB4A6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
7181BB1F3926B457445105E5 /* ScreenContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenContext.swift; sourceTree = "<group>"; };
|
||||
726B935606FD961FD7E8C2BE /* CommandNotchUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandNotchUITests.swift; sourceTree = "<group>"; };
|
||||
728B3125F7F7FDB7313D2DC6 /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
|
||||
72A1D3D12BAC593838B3125C /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
|
||||
7315EA485A52A8DC42DF925E /* TerminalScrollWheelRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollWheelRouterTests.swift; sourceTree = "<group>"; };
|
||||
74463E4EAB78F56345360CD5 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||
7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSummary.swift; sourceTree = "<group>"; };
|
||||
8210D7783A614FD7190F5DDD /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; };
|
||||
8796768C84519077354A95C7 /* CommandNotch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CommandNotch.entitlements; sourceTree = "<group>"; };
|
||||
8B3C8C11834931A9D53BC2B6 /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
|
||||
8BB1C403BC2157756F572ACF /* HotkeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyBinding.swift; sourceTree = "<group>"; };
|
||||
8CB8AF76C0F728897A26D7EF /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; };
|
||||
900F0476BE9E3600FBD371BB /* SettingsBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBindings.swift; sourceTree = "<group>"; };
|
||||
982BA8965FE92A59D66F378B /* MouseAwareTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseAwareTerminalView.swift; sourceTree = "<group>"; };
|
||||
9C55F29B779DA0E5C5FC8627 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; };
|
||||
9E6C98C406899F4B242075AF /* AppSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsStore.swift; sourceTree = "<group>"; };
|
||||
A64A11F27E65B342B991629A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenContextTests.swift; sourceTree = "<group>"; };
|
||||
AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenRegistry.swift; sourceTree = "<group>"; };
|
||||
B5AA68DC0DD3FE07B56EB6EC /* CommandNotch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CommandNotch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C0D19729317029008D81F361 /* TerminalCommandArrowBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCommandArrowBehaviorTests.swift; sourceTree = "<group>"; };
|
||||
CB593A2546BF2C0BE8E40387 /* PopoutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoutWindowController.swift; sourceTree = "<group>"; };
|
||||
CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; };
|
||||
D03D042117E59DCA9D553844 /* HotkeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeySettingsView.swift; sourceTree = "<group>"; };
|
||||
D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFrameCalculatorTests.swift; sourceTree = "<group>"; };
|
||||
D58535EA23D3C2A5D0520DB3 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
|
||||
D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollbackEstimatorTests.swift; sourceTree = "<group>"; };
|
||||
DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOrchestrator.swift; sourceTree = "<group>"; };
|
||||
DF0FFBC96F2446687D6474F4 /* WorkspaceSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSwitcherView.swift; sourceTree = "<group>"; };
|
||||
E37A6DCD9C5DE1FE11C4C1CD /* TerminalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSettingsView.swift; sourceTree = "<group>"; };
|
||||
E7A8DCFA77626F9999B432FE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
EB28950392C0198E69F3564B /* AppSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsController.swift; sourceTree = "<group>"; };
|
||||
EEB7FE2BDE5C25B7E599F340 /* HotkeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyManager.swift; sourceTree = "<group>"; };
|
||||
EEC7F7D8D15A1BC4EE43DDDB /* ScreenRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenRegistryTests.swift; sourceTree = "<group>"; };
|
||||
EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchWindow.swift; sourceTree = "<group>"; };
|
||||
F22AA47452CF798A977A6F47 /* TerminalCommandArrowBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCommandArrowBehavior.swift; sourceTree = "<group>"; };
|
||||
F3B957BD1F6120D2592613ED /* WorkspaceStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStore.swift; sourceTree = "<group>"; };
|
||||
F70A31EFACF23DD9262A040E /* WorkspacesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacesSettingsView.swift; sourceTree = "<group>"; };
|
||||
F9220E02B6D470DD05CA540C /* WindowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCoordinator.swift; sourceTree = "<group>"; };
|
||||
FE99DBAB656EA80A117D2EE1 /* AnimationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationSettingsView.swift; sourceTree = "<group>"; };
|
||||
FF6D136A0B3FC79DDE12A826 /* NotchOrchestratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOrchestratorTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
F3D057FF4247F13A1BBAE547 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5379DF2FACE924BDDB584377 /* SwiftTerm in Frameworks */,
|
||||
99262FD443753BA518221CBD /* GhosttyKit.xcframework in Frameworks */,
|
||||
3DC413CDD97DF0233F039518 /* Carbon.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
00BC30DD7FA5C3C26404733B /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
74463E4EAB78F56345360CD5 /* AppSettings.swift */,
|
||||
EB28950392C0198E69F3564B /* AppSettingsController.swift */,
|
||||
9E6C98C406899F4B242075AF /* AppSettingsStore.swift */,
|
||||
4AFC12727EA1C097E0EF9DF2 /* GhosttyBackendSession.swift */,
|
||||
8BB1C403BC2157756F572ACF /* HotkeyBinding.swift */,
|
||||
DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */,
|
||||
297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */,
|
||||
4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */,
|
||||
7181BB1F3926B457445105E5 /* ScreenContext.swift */,
|
||||
AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */,
|
||||
3942F3843D0A13C145576760 /* SwiftTermBackendSession.swift */,
|
||||
567E85A2ED628460CEC760DB /* TerminalManager.swift */,
|
||||
165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */,
|
||||
49E1791BB45E1505500ACC67 /* TerminalSession.swift */,
|
||||
CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */,
|
||||
3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */,
|
||||
0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */,
|
||||
F3B957BD1F6120D2592613ED /* WorkspaceStore.swift */,
|
||||
7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
19C217A7F1FCA6AE26E2FC4E /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D58535EA23D3C2A5D0520DB3 /* Carbon.framework */,
|
||||
4DB445256B2E0228F7679EE7 /* GhosttyKit.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
618799FE544A4373B457DCDA /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5DDC7451F0E7ACFE0BEC5473 /* NSScreen+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7043235A31A4023478DA1302 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1375B54FF94D210B46F18540 /* ghostty */,
|
||||
3A03006228968FE12FCBF588 /* terminfo */,
|
||||
84740FA9CF6A18B35EC82623 /* CommandNotch */,
|
||||
A2F9603AB9C86C4EA62FFA59 /* CommandNotchTests */,
|
||||
E6C841E864A0CC68B9B05BAC /* CommandNotchUITests */,
|
||||
19C217A7F1FCA6AE26E2FC4E /* Frameworks */,
|
||||
B269158E04E8E603B61448F0 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
84740FA9CF6A18B35EC82623 /* CommandNotch */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E7A8DCFA77626F9999B432FE /* AppDelegate.swift */,
|
||||
5159CB9DBE2BAA0D2E201C39 /* CommandNotchApp.swift */,
|
||||
A64A11F27E65B342B991629A /* ContentView.swift */,
|
||||
D3C008AD1EFEF08E3417396F /* Components */,
|
||||
618799FE544A4373B457DCDA /* Extensions */,
|
||||
D87DBBE2E8779343A36F3490 /* Managers */,
|
||||
00BC30DD7FA5C3C26404733B /* Models */,
|
||||
C2F0251EB52960C9F437154D /* Resources */,
|
||||
DEE792769214DF028395EA86 /* Views */,
|
||||
);
|
||||
path = CommandNotch;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A2F9603AB9C86C4EA62FFA59 /* CommandNotchTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3F5FF5623898FA150C3B70D4 /* AppSettingsControllerTests.swift */,
|
||||
39CA6F2C2CA32D0AA58F6C43 /* AppSettingsStoreTests.swift */,
|
||||
FF6D136A0B3FC79DDE12A826 /* NotchOrchestratorTests.swift */,
|
||||
A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */,
|
||||
EEC7F7D8D15A1BC4EE43DDDB /* ScreenRegistryTests.swift */,
|
||||
C0D19729317029008D81F361 /* TerminalCommandArrowBehaviorTests.swift */,
|
||||
D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */,
|
||||
40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */,
|
||||
7315EA485A52A8DC42DF925E /* TerminalScrollWheelRouterTests.swift */,
|
||||
D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */,
|
||||
591FCE91AF83A8A8E44E1625 /* WorkspaceRegistryTests.swift */,
|
||||
4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */,
|
||||
);
|
||||
path = CommandNotchTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B269158E04E8E603B61448F0 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5AA68DC0DD3FE07B56EB6EC /* CommandNotch.app */,
|
||||
35CBC14E11EBD8486457CE91 /* CommandNotchTests.xctest */,
|
||||
496267F03E261FEC9EBD5A9D /* CommandNotchUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C2F0251EB52960C9F437154D /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6ADD641A088A5005DD0EB4A6 /* Assets.xcassets */,
|
||||
8796768C84519077354A95C7 /* CommandNotch.entitlements */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D3C008AD1EFEF08E3417396F /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */,
|
||||
982BA8965FE92A59D66F378B /* MouseAwareTerminalView.swift */,
|
||||
3F57837A7115DEEE11E14B40 /* NotchShape.swift */,
|
||||
EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */,
|
||||
9C55F29B779DA0E5C5FC8627 /* SwiftTermView.swift */,
|
||||
72A1D3D12BAC593838B3125C /* TabBar.swift */,
|
||||
F22AA47452CF798A977A6F47 /* TerminalCommandArrowBehavior.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D87DBBE2E8779343A36F3490 /* Managers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EEB7FE2BDE5C25B7E599F340 /* HotkeyManager.swift */,
|
||||
8210D7783A614FD7190F5DDD /* LaunchAtLoginHelper.swift */,
|
||||
CB593A2546BF2C0BE8E40387 /* PopoutWindowController.swift */,
|
||||
8CB8AF76C0F728897A26D7EF /* ScreenManager.swift */,
|
||||
728B3125F7F7FDB7313D2DC6 /* SettingsWindowController.swift */,
|
||||
F9220E02B6D470DD05CA540C /* WindowCoordinator.swift */,
|
||||
);
|
||||
path = Managers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DEE792769214DF028395EA86 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
48198AFE5473B0F7AECAB3FB /* AboutSettingsView.swift */,
|
||||
FE99DBAB656EA80A117D2EE1 /* AnimationSettingsView.swift */,
|
||||
8B3C8C11834931A9D53BC2B6 /* AppearanceSettingsView.swift */,
|
||||
27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */,
|
||||
D03D042117E59DCA9D553844 /* HotkeySettingsView.swift */,
|
||||
900F0476BE9E3600FBD371BB /* SettingsBindings.swift */,
|
||||
3125FD3DC55420122CF85D80 /* SettingsView.swift */,
|
||||
E37A6DCD9C5DE1FE11C4C1CD /* TerminalSettingsView.swift */,
|
||||
F70A31EFACF23DD9262A040E /* WorkspacesSettingsView.swift */,
|
||||
DF0FFBC96F2446687D6474F4 /* WorkspaceSwitcherView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E6C841E864A0CC68B9B05BAC /* CommandNotchUITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
726B935606FD961FD7E8C2BE /* CommandNotchUITests.swift */,
|
||||
);
|
||||
path = CommandNotchUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
036FDAECD12C0A679DA1F5D6 /* CommandNotchTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 983C618ACECB88BD023F005E /* Build configuration list for PBXNativeTarget "CommandNotchTests" */;
|
||||
buildPhases = (
|
||||
4108B7D3B592DEBB95C689C4 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
316871D68B87C00F5A8FEECC /* PBXTargetDependency */,
|
||||
);
|
||||
name = CommandNotchTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = CommandNotchTests;
|
||||
productReference = 35CBC14E11EBD8486457CE91 /* CommandNotchTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
1C8D00CBB29219BD347E9CC4 /* CommandNotchUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CEAE976FCC3F5BD54E57E585 /* Build configuration list for PBXNativeTarget "CommandNotchUITests" */;
|
||||
buildPhases = (
|
||||
1215938A5211847A086CC444 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
F9E4A521E345300B08E257EA /* PBXTargetDependency */,
|
||||
);
|
||||
name = CommandNotchUITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = CommandNotchUITests;
|
||||
productReference = 496267F03E261FEC9EBD5A9D /* CommandNotchUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
D5585E5732CD067DF6EF0C69 /* CommandNotch */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = E599EC932C075AF0FD763A0E /* Build configuration list for PBXNativeTarget "CommandNotch" */;
|
||||
buildPhases = (
|
||||
D5B7874B63551D908A4B76C8 /* Sources */,
|
||||
3BD8FCDCDA6E37ED22A35CA5 /* Resources */,
|
||||
F3D057FF4247F13A1BBAE547 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = CommandNotch;
|
||||
packageProductDependencies = (
|
||||
C921E6435A64AA07A0FEA4D5 /* SwiftTerm */,
|
||||
);
|
||||
productName = CommandNotch;
|
||||
productReference = B5AA68DC0DD3FE07B56EB6EC /* CommandNotch.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
E99C2EDB39F7B64C1540BCA8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1600;
|
||||
TargetAttributes = {
|
||||
1C8D00CBB29219BD347E9CC4 = {
|
||||
DevelopmentTeam = G698BP272N;
|
||||
TestTargetID = D5585E5732CD067DF6EF0C69;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = C47A3896770C98F2A3E62B7A /* Build configuration list for PBXProject "CommandNotch" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 7043235A31A4023478DA1302;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
28377BE3F9997892D4929B6E /* XCRemoteSwiftPackageReference "SwiftTerm" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = B269158E04E8E603B61448F0 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
D5585E5732CD067DF6EF0C69 /* CommandNotch */,
|
||||
036FDAECD12C0A679DA1F5D6 /* CommandNotchTests */,
|
||||
1C8D00CBB29219BD347E9CC4 /* CommandNotchUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
3BD8FCDCDA6E37ED22A35CA5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7AB14019B5CC6ED84B96FA47 /* Assets.xcassets in Resources */,
|
||||
229567E62C617B045E92E0FE /* ghostty in Resources */,
|
||||
567914B84D0EEF6E6E9149A9 /* terminfo in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
1215938A5211847A086CC444 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
278010607B0D552DCC8996C5 /* CommandNotchUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
4108B7D3B592DEBB95C689C4 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F8F8DE2F26608259D72635B7 /* AppSettingsControllerTests.swift in Sources */,
|
||||
7D5F3C3B5E173B6F06901812 /* AppSettingsStoreTests.swift in Sources */,
|
||||
2089566A2BBAA65EA82119B3 /* NotchOrchestratorTests.swift in Sources */,
|
||||
D2468B19D6F0A2C1DFDFE2B7 /* ScreenContextTests.swift in Sources */,
|
||||
8A1B2A4CD61B0D9A4BAB075B /* ScreenRegistryTests.swift in Sources */,
|
||||
CFBBE994BA6BAD4658AAB9CB /* TerminalCommandArrowBehaviorTests.swift in Sources */,
|
||||
21373D6E9C2F34FD63CEC6A5 /* TerminalScrollCoordinatorTests.swift in Sources */,
|
||||
D6BBC4DE23DEA39D9713469A /* TerminalScrollWheelRouterTests.swift in Sources */,
|
||||
40183737D8C237022D0882FD /* TerminalScrollbackEstimatorTests.swift in Sources */,
|
||||
0F133E8A88D2E313D90C32AD /* WindowFrameCalculatorTests.swift in Sources */,
|
||||
154F363D434A26105C5999B5 /* WorkspaceRegistryTests.swift in Sources */,
|
||||
2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D5B7874B63551D908A4B76C8 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B605A9311557251A85183383 /* AboutSettingsView.swift in Sources */,
|
||||
88D87155D79C493D8956AA3B /* AnimationSettingsView.swift in Sources */,
|
||||
6DD4DAE72E5C3858B230D94C /* AppDelegate.swift in Sources */,
|
||||
7A7D3455D50D041CF698E786 /* AppSettings.swift in Sources */,
|
||||
B406183CC7B5DCE76989A066 /* AppSettingsController.swift in Sources */,
|
||||
4D335F67B71F7DD977B6AEF9 /* AppSettingsStore.swift in Sources */,
|
||||
0DE8E7F29FBAA7E2B613E221 /* AppearanceSettingsView.swift in Sources */,
|
||||
D088BC850F37E717844761C6 /* CommandNotchApp.swift in Sources */,
|
||||
C84FA3100884649FE92BF5DD /* ContentView.swift in Sources */,
|
||||
34AF69BF7AF8DC78ADE3774A /* GeneralSettingsView.swift in Sources */,
|
||||
FF6BB7E881BCBB4FD6CC4011 /* GhosttyBackendSession.swift in Sources */,
|
||||
9F535B9516A4AE9FB536B1BD /* HotkeyBinding.swift in Sources */,
|
||||
187F4B521BFC3BD29ADA79E3 /* HotkeyManager.swift in Sources */,
|
||||
E792411FA82E79E810F4B4C3 /* HotkeyRecorderView.swift in Sources */,
|
||||
6A18F6635B509FF58669F505 /* HotkeySettingsView.swift in Sources */,
|
||||
12F68EDA880030DAB644FF5F /* LaunchAtLoginHelper.swift in Sources */,
|
||||
4BA9D6274CFD6A87DC397B8E /* MouseAwareTerminalView.swift in Sources */,
|
||||
A74E14B8AD19C53820853D8E /* NSScreen+Extensions.swift in Sources */,
|
||||
EE72479BA5A25FF31BACCC50 /* NotchOrchestrator.swift in Sources */,
|
||||
3B69CB3CDEC2E5F2DCE600F9 /* NotchSettings.swift in Sources */,
|
||||
088FE22B3308AAB25027E086 /* NotchShape.swift in Sources */,
|
||||
23E2DDCF36D0DAB2EA72C39C /* NotchState.swift in Sources */,
|
||||
4E0AD14B7427532271E485AA /* NotchWindow.swift in Sources */,
|
||||
D4CEB4B895F75D91FA892A06 /* PopoutWindowController.swift in Sources */,
|
||||
787477BB6D2F8AC723EEDA7D /* ScreenContext.swift in Sources */,
|
||||
F8ED522FDC96B1F9AD8933F7 /* ScreenManager.swift in Sources */,
|
||||
E63FD5862C0EC54E284F6A0F /* ScreenRegistry.swift in Sources */,
|
||||
771088361F981A9AAE976F3C /* SettingsBindings.swift in Sources */,
|
||||
DAD3AB4A0DAADA32C02D959E /* SettingsView.swift in Sources */,
|
||||
5A3FDFCF30A1AAFE070290E9 /* SettingsWindowController.swift in Sources */,
|
||||
28F609FCF422E735F567EE32 /* SwiftTermBackendSession.swift in Sources */,
|
||||
5F3534F66A5DBE4E081AFFA6 /* SwiftTermView.swift in Sources */,
|
||||
88113BA9B217DA579C36BEBE /* TabBar.swift in Sources */,
|
||||
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */,
|
||||
6F249EDFA2D654457DF385F1 /* TerminalManager.swift in Sources */,
|
||||
DD116B0FCC341D66F1534EC4 /* TerminalScrollbackEstimator.swift in Sources */,
|
||||
7A69B5AAC686174BCA54D0F0 /* TerminalSession.swift in Sources */,
|
||||
65C7DB7296C6C6A77598A1F4 /* TerminalSettingsView.swift in Sources */,
|
||||
1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */,
|
||||
507A67E770DEFAF5BC321FCF /* WindowCoordinator.swift in Sources */,
|
||||
86BB4A41E8EC44A5F45CE995 /* WorkspaceController.swift in Sources */,
|
||||
4CFA0C79095ACE0A327A2469 /* WorkspaceRegistry.swift in Sources */,
|
||||
AE8215538D4B026A00BAD241 /* WorkspaceStore.swift in Sources */,
|
||||
2DF22798D3A7514E2A9183FC /* WorkspaceSummary.swift in Sources */,
|
||||
6624A60CB121A22A03365071 /* WorkspaceSwitcherView.swift in Sources */,
|
||||
26AE379040149CBE05B314BB /* WorkspacesSettingsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
316871D68B87C00F5A8FEECC /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D5585E5732CD067DF6EF0C69 /* CommandNotch */;
|
||||
targetProxy = 13E720E97D079D298D124BBE /* PBXContainerItemProxy */;
|
||||
};
|
||||
F9E4A521E345300B08E257EA /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D5585E5732CD067DF6EF0C69 /* CommandNotch */;
|
||||
targetProxy = 9D6387038E18398CA764147F /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
15F1D354AC7D5666A8317E25 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = CommandNotch/Resources/CommandNotch.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = G698BP272N;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"Vendor\"",
|
||||
);
|
||||
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-lstdc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
|
||||
PRODUCT_NAME = CommandNotch;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
4C713B8FE8B6293021AB974F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchTests;
|
||||
PRODUCT_NAME = CommandNotchTests;
|
||||
SDKROOT = macosx;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CommandNotch.app/Contents/MacOS/CommandNotch";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7D032F3A06E313F1F92D39EC /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.10;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
860E4EAD454534A04683E7DD /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = CommandNotch/Resources/CommandNotch.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = G698BP272N;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"Vendor\"",
|
||||
);
|
||||
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-lstdc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
|
||||
PRODUCT_NAME = CommandNotch;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
87EEF9DE40EE78121DC1E68B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.10;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C9CA7CD89BF1C9A9BC98C4CF /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = G698BP272N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchUITests;
|
||||
PRODUCT_NAME = CommandNotchUITests;
|
||||
SDKROOT = macosx;
|
||||
TEST_TARGET_NAME = CommandNotch;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
E52824EDC7F4752F43B6301A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchTests;
|
||||
PRODUCT_NAME = CommandNotchTests;
|
||||
SDKROOT = macosx;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CommandNotch.app/Contents/MacOS/CommandNotch";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F697F3FF95C1EB110FC25A5C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = G698BP272N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchUITests;
|
||||
PRODUCT_NAME = CommandNotchUITests;
|
||||
SDKROOT = macosx;
|
||||
TEST_TARGET_NAME = CommandNotch;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
983C618ACECB88BD023F005E /* Build configuration list for PBXNativeTarget "CommandNotchTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
E52824EDC7F4752F43B6301A /* Debug */,
|
||||
4C713B8FE8B6293021AB974F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
C47A3896770C98F2A3E62B7A /* Build configuration list for PBXProject "CommandNotch" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7D032F3A06E313F1F92D39EC /* Debug */,
|
||||
87EEF9DE40EE78121DC1E68B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
CEAE976FCC3F5BD54E57E585 /* Build configuration list for PBXNativeTarget "CommandNotchUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C9CA7CD89BF1C9A9BC98C4CF /* Debug */,
|
||||
F697F3FF95C1EB110FC25A5C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
E599EC932C075AF0FD763A0E /* Build configuration list for PBXNativeTarget "CommandNotch" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
15F1D354AC7D5666A8317E25 /* Debug */,
|
||||
860E4EAD454534A04683E7DD /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
28377BE3F9997892D4929B6E /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.2.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
C921E6435A64AA07A0FEA4D5 /* SwiftTerm */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 28377BE3F9997892D4929B6E /* XCRemoteSwiftPackageReference "SwiftTerm" */;
|
||||
productName = SwiftTerm;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = E99C2EDB39F7B64C1540BCA8 /* Project object */;
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -15,7 +14,7 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
@@ -27,8 +26,39 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "036FDAECD12C0A679DA1F5D6"
|
||||
BuildableName = "CommandNotchTests.xctest"
|
||||
BlueprintName = "CommandNotchTests"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1C8D00CBB29219BD347E9CC4"
|
||||
BuildableName = "CommandNotchUITests.xctest"
|
||||
BlueprintName = "CommandNotchUITests"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -44,7 +74,7 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
@@ -61,7 +91,7 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -15,7 +14,7 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
@@ -27,14 +26,45 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "036FDAECD12C0A679DA1F5D6"
|
||||
BuildableName = "CommandNotchTests.xctest"
|
||||
BlueprintName = "CommandNotchTests"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1C8D00CBB29219BD347E9CC4"
|
||||
BuildableName = "CommandNotchUITests.xctest"
|
||||
BlueprintName = "CommandNotchUITests"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "1"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
@@ -44,7 +74,7 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
@@ -61,7 +91,7 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||
BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
|
||||
BuildableName = "CommandNotch.app"
|
||||
BlueprintName = "CommandNotch"
|
||||
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||
@@ -5,23 +5,9 @@
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>CommandNotch.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<dict/>
|
||||
<key>Release-CommandNotch.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>1485207FA11756EC2DF4F08B</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<dict/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
128
CommandNotch/CommandNotch/AppDelegate.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
import AppKit
|
||||
import Combine
|
||||
|
||||
/// Application delegate that bootstraps the notch overlay system.
|
||||
@MainActor
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private enum UITestLaunchArgument {
|
||||
static let regularActivation = "--uitest-regular-activation"
|
||||
static let showSettings = "--uitest-show-settings"
|
||||
static let openNotch = "--uitest-open-notch"
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let settingsController = AppSettingsController.shared
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NotchSettings.registerDefaults()
|
||||
|
||||
if isRunningUITests {
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
} else {
|
||||
NSApp.setActivationPolicy(.accessory)
|
||||
}
|
||||
|
||||
// Sync the launch-at-login toggle with the actual system state
|
||||
// in case the user toggled it from System Settings.
|
||||
settingsController.update {
|
||||
$0.display.launchAtLogin = LaunchAtLoginHelper.isEnabled
|
||||
}
|
||||
|
||||
ScreenManager.shared.start()
|
||||
observeDisplayPreference()
|
||||
observeSizePreferences()
|
||||
observeFontSizeChanges()
|
||||
observeTerminalThemeChanges()
|
||||
observeTerminalScrollbackChanges()
|
||||
applyUITestLaunchBehaviorIfNeeded()
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
ScreenManager.shared.stop()
|
||||
}
|
||||
|
||||
// MARK: - Preference observers
|
||||
|
||||
/// Only rebuild windows when the display-count preference changes.
|
||||
private func observeDisplayPreference() {
|
||||
settingsController.$settings
|
||||
.map(\.display.showOnAllDisplays)
|
||||
.removeDuplicates()
|
||||
.dropFirst()
|
||||
.sink { _ in
|
||||
ScreenManager.shared.rebuildWindows()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// Reposition (not rebuild) when any sizing preference changes.
|
||||
private func observeSizePreferences() {
|
||||
settingsController.$settings
|
||||
.map(\.display.layoutSignature)
|
||||
.removeDuplicates()
|
||||
.dropFirst()
|
||||
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
|
||||
.sink { _ in
|
||||
ScreenManager.shared.repositionWindows()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// Live-update terminal font size across all sessions.
|
||||
private func observeFontSizeChanges() {
|
||||
settingsController.$settings
|
||||
.map(\.terminal.fontSize)
|
||||
.removeDuplicates()
|
||||
.sink { newSize in
|
||||
guard newSize > 0 else { return }
|
||||
WorkspaceRegistry.shared.updateAllWorkspacesFontSizes(CGFloat(newSize))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// Live-update terminal colors across all sessions.
|
||||
private func observeTerminalThemeChanges() {
|
||||
settingsController.$settings
|
||||
.map(\.terminal.themeRawValue)
|
||||
.removeDuplicates()
|
||||
.sink { newTheme in
|
||||
WorkspaceRegistry.shared.updateAllWorkspacesThemes(TerminalTheme.resolve(newTheme))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func observeTerminalScrollbackChanges() {
|
||||
settingsController.$settings
|
||||
.map(\.terminal.scrollbackLines)
|
||||
.removeDuplicates()
|
||||
.sink { scrollbackLines in
|
||||
WorkspaceRegistry.shared.updateAllWorkspacesScrollbackLines(scrollbackLines)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private var launchArguments: [String] {
|
||||
ProcessInfo.processInfo.arguments
|
||||
}
|
||||
|
||||
private var isRunningUITests: Bool {
|
||||
launchArguments.contains(UITestLaunchArgument.regularActivation)
|
||||
|| launchArguments.contains(UITestLaunchArgument.showSettings)
|
||||
|| launchArguments.contains(UITestLaunchArgument.openNotch)
|
||||
}
|
||||
|
||||
private func applyUITestLaunchBehaviorIfNeeded() {
|
||||
guard isRunningUITests else { return }
|
||||
|
||||
DispatchQueue.main.async { @MainActor in
|
||||
if self.launchArguments.contains(UITestLaunchArgument.showSettings) {
|
||||
SettingsWindowController.shared.showSettings()
|
||||
}
|
||||
|
||||
if self.launchArguments.contains(UITestLaunchArgument.openNotch),
|
||||
let screenID = ScreenRegistry.shared.activeScreenID() {
|
||||
ScreenManager.shared.openNotch(screenID: screenID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,19 @@ import SwiftUI
|
||||
struct CommandNotchApp: App {
|
||||
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
|
||||
@AppStorage(NotchSettings.Keys.showMenuBarIcon)
|
||||
private var showMenuBarIcon = NotchSettings.Defaults.showMenuBarIcon
|
||||
@StateObject private var settingsController = AppSettingsController.shared
|
||||
|
||||
var body: some Scene {
|
||||
MenuBarExtra("CommandNotch", systemImage: "terminal", isInserted: $showMenuBarIcon) {
|
||||
MenuBarExtra(
|
||||
"CommandNotch",
|
||||
systemImage: "terminal",
|
||||
isInserted: Binding(
|
||||
get: { settingsController.settings.display.showMenuBarIcon },
|
||||
set: { newValue in
|
||||
settingsController.update { $0.display.showMenuBarIcon = newValue }
|
||||
}
|
||||
)
|
||||
) {
|
||||
Button("Toggle Notch") {
|
||||
ScreenManager.shared.toggleNotchOnActiveScreen()
|
||||
}
|
||||
@@ -76,9 +76,7 @@ struct HotkeyRecorderField: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
||||
nsView.currentLabel = binding.displayString
|
||||
nsView.showRecording = isRecording
|
||||
nsView.needsDisplay = true
|
||||
nsView.update(currentLabel: binding.displayString, isRecording: isRecording)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +97,7 @@ struct OptionalHotkeyRecorderField: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
||||
nsView.currentLabel = binding?.displayString ?? "Not set"
|
||||
nsView.showRecording = isRecording
|
||||
nsView.needsDisplay = true
|
||||
nsView.update(currentLabel: binding?.displayString ?? "Not set", isRecording: isRecording)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +179,12 @@ class HotkeyNSView: NSView {
|
||||
updateLabelAppearance()
|
||||
}
|
||||
|
||||
func update(currentLabel: String, isRecording: Bool) {
|
||||
self.currentLabel = currentLabel
|
||||
showRecording = isRecording
|
||||
updateLabelAppearance()
|
||||
}
|
||||
|
||||
private func updateLabelAppearance() {
|
||||
label.stringValue = showRecording ? "Press keys..." : currentLabel
|
||||
label.textColor = showRecording ? .controlAccentColor : .labelColor
|
||||
@@ -0,0 +1,49 @@
|
||||
import AppKit
|
||||
import SwiftTerm
|
||||
|
||||
struct TerminalScrollWheelRouter {
|
||||
static func shouldSendMouseWheel(
|
||||
allowMouseReporting: Bool,
|
||||
mouseMode: Terminal.MouseMode,
|
||||
deltaY: Double
|
||||
) -> Bool {
|
||||
allowMouseReporting && mouseMode != .off && deltaY != 0
|
||||
}
|
||||
|
||||
static func velocity(for deltaY: Double) -> Int {
|
||||
let magnitude = Int(abs(deltaY))
|
||||
if magnitude > 9 {
|
||||
return 20
|
||||
}
|
||||
if magnitude > 5 {
|
||||
return 10
|
||||
}
|
||||
if magnitude > 1 {
|
||||
return 3
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
static func gridPosition(
|
||||
point: CGPoint,
|
||||
bounds: CGRect,
|
||||
cols: Int,
|
||||
rows: Int
|
||||
) -> (x: Int, y: Int, pixelX: Int, pixelY: Int) {
|
||||
let safeCols = max(cols, 1)
|
||||
let safeRows = max(rows, 1)
|
||||
let width = max(bounds.width, 1)
|
||||
let height = max(bounds.height, 1)
|
||||
let clampedX = min(max(point.x, 0), width)
|
||||
let clampedY = min(max(point.y, 0), height)
|
||||
let cellWidth = width / CGFloat(safeCols)
|
||||
let cellHeight = height / CGFloat(safeRows)
|
||||
|
||||
let column = min(max(Int(clampedX / cellWidth), 0), safeCols - 1)
|
||||
let row = min(max(Int((height - clampedY) / cellHeight), 0), safeRows - 1)
|
||||
let pixelX = min(max(Int(clampedX), 0), Int(width))
|
||||
let pixelY = min(max(Int(height - clampedY), 0), Int(height))
|
||||
|
||||
return (column, row, pixelX, pixelY)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import SwiftUI
|
||||
import SwiftTerm
|
||||
|
||||
/// NSViewRepresentable wrapper that embeds a SwiftTerm TerminalView.
|
||||
/// NSViewRepresentable wrapper that embeds the active terminal backend view.
|
||||
/// The container has a solid black background — matching the notch panel.
|
||||
/// All transparency is handled by the single `.opacity()` on ContentView.
|
||||
struct SwiftTermView: NSViewRepresentable {
|
||||
struct TerminalSessionView: NSViewRepresentable {
|
||||
|
||||
let session: TerminalSession
|
||||
|
||||
@@ -17,30 +16,30 @@ struct SwiftTermView: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) {
|
||||
let tv = session.terminalView
|
||||
let terminalView = session.view
|
||||
|
||||
if nsView.subviews.first !== tv {
|
||||
if nsView.subviews.first !== terminalView {
|
||||
nsView.subviews.forEach { $0.removeFromSuperview() }
|
||||
embedTerminalView(in: nsView)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let window = nsView.window, window.isKeyWindow {
|
||||
window.makeFirstResponder(tv)
|
||||
session.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func embedTerminalView(in container: NSView) {
|
||||
let tv = session.terminalView
|
||||
tv.removeFromSuperview()
|
||||
container.addSubview(tv)
|
||||
tv.translatesAutoresizingMaskIntoConstraints = false
|
||||
let terminalView = session.view
|
||||
terminalView.removeFromSuperview()
|
||||
container.addSubview(terminalView)
|
||||
terminalView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tv.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
tv.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||
tv.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
tv.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
terminalView.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
terminalView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||
terminalView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
terminalView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,30 @@ import SwiftUI
|
||||
/// the single `.opacity()` on ContentView handles transparency.
|
||||
struct TabBar: View {
|
||||
|
||||
@ObservedObject var terminalManager: TerminalManager
|
||||
@ObservedObject var workspace: WorkspaceController
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 2) {
|
||||
ForEach(Array(terminalManager.tabs.enumerated()), id: \.element.id) { index, tab in
|
||||
tabButton(for: tab, at: index)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
terminalManager.newTab()
|
||||
workspace.newTab()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
}
|
||||
.accessibilityLabel("New Tab")
|
||||
.accessibilityIdentifier("notch.new-tab")
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 2) {
|
||||
ForEach(Array(workspace.tabs.enumerated()), id: \.element.id) { index, tab in
|
||||
tabButton(for: tab, at: index)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
.frame(height: 28)
|
||||
.background(.black)
|
||||
@@ -36,7 +36,7 @@ struct TabBar: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func tabButton(for tab: TerminalSession, at index: Int) -> some View {
|
||||
let isActive = index == terminalManager.activeTabIndex
|
||||
let isActive = index == workspace.activeTabIndex
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text(abbreviateTitle(tab.title))
|
||||
@@ -44,9 +44,9 @@ struct TabBar: View {
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(isActive ? .white : .white.opacity(0.5))
|
||||
|
||||
if isActive && terminalManager.tabs.count > 1 {
|
||||
if isActive && workspace.tabs.count > 1 {
|
||||
Button {
|
||||
terminalManager.closeTab(at: index)
|
||||
workspace.closeTab(at: index)
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
@@ -63,7 +63,7 @@ struct TabBar: View {
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
terminalManager.switchToTab(at: index)
|
||||
workspace.switchToTab(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import AppKit
|
||||
import Carbon.HIToolbox
|
||||
import SwiftTerm
|
||||
|
||||
enum TerminalCommandArrowBehavior {
|
||||
private static let relevantModifiers: NSEvent.ModifierFlags = [.command, .control, .option, .shift]
|
||||
private static let lineKill: [UInt8] = [0x15]
|
||||
private static let clearScreen: [UInt8] = [0x0c]
|
||||
|
||||
static func sequence(
|
||||
for modifierFlags: NSEvent.ModifierFlags,
|
||||
keyCode: UInt16,
|
||||
applicationCursor: Bool
|
||||
) -> [UInt8]? {
|
||||
let flags = modifierFlags.intersection(relevantModifiers)
|
||||
guard flags == [.command] else { return nil }
|
||||
|
||||
switch Int(keyCode) {
|
||||
case kVK_LeftArrow:
|
||||
return applicationCursor ? EscapeSequences.moveHomeApp : EscapeSequences.moveHomeNormal
|
||||
case kVK_RightArrow:
|
||||
return applicationCursor ? EscapeSequences.moveEndApp : EscapeSequences.moveEndNormal
|
||||
case kVK_Delete:
|
||||
return lineKill
|
||||
case kVK_ANSI_L:
|
||||
return clearScreen
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import SwiftUI
|
||||
import SwiftTerm
|
||||
|
||||
/// Main view rendered inside each NotchWindow.
|
||||
///
|
||||
@@ -9,25 +8,11 @@ import SwiftTerm
|
||||
/// layering, no mismatched areas.
|
||||
struct ContentView: View {
|
||||
|
||||
@ObservedObject var vm: NotchViewModel
|
||||
@ObservedObject var terminalManager: TerminalManager
|
||||
@ObservedObject var screen: ScreenContext
|
||||
let orchestrator: NotchOrchestrator
|
||||
@ObservedObject private var settingsController = AppSettingsController.shared
|
||||
@ObservedObject private var screenRegistry = ScreenRegistry.shared
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
@AppStorage(NotchSettings.Keys.openNotchOnHover) private var openNotchOnHover = NotchSettings.Defaults.openNotchOnHover
|
||||
@AppStorage(NotchSettings.Keys.minimumHoverDuration) private var minimumHoverDuration = NotchSettings.Defaults.minimumHoverDuration
|
||||
|
||||
@AppStorage(NotchSettings.Keys.enableShadow) private var enableShadow = NotchSettings.Defaults.enableShadow
|
||||
@AppStorage(NotchSettings.Keys.shadowRadius) private var shadowRadius = NotchSettings.Defaults.shadowRadius
|
||||
@AppStorage(NotchSettings.Keys.shadowOpacity) private var shadowOpacity = NotchSettings.Defaults.shadowOpacity
|
||||
@AppStorage(NotchSettings.Keys.cornerRadiusScaling) private var cornerRadiusScaling = NotchSettings.Defaults.cornerRadiusScaling
|
||||
@AppStorage(NotchSettings.Keys.notchOpacity) private var notchOpacity = NotchSettings.Defaults.notchOpacity
|
||||
@AppStorage(NotchSettings.Keys.blurRadius) private var blurRadius = NotchSettings.Defaults.blurRadius
|
||||
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverSpringResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverSpringDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||
|
||||
@State private var hoverTask: Task<Void, Never>?
|
||||
@State private var resizeStartSize: CGSize?
|
||||
@State private var resizeStartMouseLocation: CGPoint?
|
||||
|
||||
@@ -36,18 +21,51 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private var currentShape: NotchShape {
|
||||
vm.notchState == .open
|
||||
screen.notchState == .open
|
||||
? (cornerRadiusScaling ? .opened : NotchShape(topCornerRadius: 0, bottomCornerRadius: 14))
|
||||
: .closed
|
||||
}
|
||||
|
||||
private var enableShadow: Bool {
|
||||
settingsController.settings.appearance.enableShadow
|
||||
}
|
||||
|
||||
private var shadowRadius: Double {
|
||||
settingsController.settings.appearance.shadowRadius
|
||||
}
|
||||
|
||||
private var shadowOpacity: Double {
|
||||
settingsController.settings.appearance.shadowOpacity
|
||||
}
|
||||
|
||||
private var cornerRadiusScaling: Bool {
|
||||
settingsController.settings.appearance.cornerRadiusScaling
|
||||
}
|
||||
|
||||
private var notchOpacity: Double {
|
||||
settingsController.settings.appearance.notchOpacity
|
||||
}
|
||||
|
||||
private var blurRadius: Double {
|
||||
settingsController.settings.appearance.blurRadius
|
||||
}
|
||||
|
||||
private var hoverSpringResponse: Double {
|
||||
settingsController.settings.animation.hoverSpringResponse
|
||||
}
|
||||
|
||||
private var hoverSpringDamping: Double {
|
||||
settingsController.settings.animation.hoverSpringDamping
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
notchBody
|
||||
.accessibilityIdentifier("notch.container")
|
||||
.frame(
|
||||
width: vm.notchSize.width,
|
||||
height: vm.notchState == .open ? vm.notchSize.height : vm.closedNotchSize.height,
|
||||
width: screen.notchSize.width,
|
||||
height: screen.notchState == .open ? screen.notchSize.height : screen.closedNotchSize.height,
|
||||
alignment: .top
|
||||
)
|
||||
.background(.black)
|
||||
@@ -56,7 +74,7 @@ struct ContentView: View {
|
||||
Rectangle().fill(.black).frame(height: 1)
|
||||
}
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
if vm.notchState == .open {
|
||||
if screen.notchState == .open {
|
||||
resizeHandle
|
||||
}
|
||||
}
|
||||
@@ -68,22 +86,15 @@ struct ContentView: View {
|
||||
// so this one modifier makes it all uniformly transparent.
|
||||
.opacity(notchOpacity)
|
||||
.blur(radius: blurRadius)
|
||||
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchState)
|
||||
.animation(sizeAnimation, value: vm.notchSize.width)
|
||||
.animation(sizeAnimation, value: vm.notchSize.height)
|
||||
.animation(screen.notchState == .open ? screen.openAnimation : screen.closeAnimation, value: screen.notchState)
|
||||
.animation(sizeAnimation, value: screen.notchSize.width)
|
||||
.animation(sizeAnimation, value: screen.notchSize.height)
|
||||
.onHover { handleHover($0) }
|
||||
.onChange(of: vm.isCloseTransitionActive) { _, isClosing in
|
||||
if isClosing {
|
||||
hoverTask?.cancel()
|
||||
} else {
|
||||
scheduleHoverOpenIfNeeded()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
hoverTask?.cancel()
|
||||
resizeStartSize = nil
|
||||
resizeStartMouseLocation = nil
|
||||
vm.endInteractiveResize()
|
||||
screen.endInteractiveResize()
|
||||
orchestrator.handleHoverChange(false, for: screen.id)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
@@ -93,18 +104,20 @@ struct ContentView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var notchBody: some View {
|
||||
if vm.notchState == .open {
|
||||
openContent
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
closedContent
|
||||
WorkspaceScopedView(screen: screen, screenRegistry: screenRegistry) { workspace in
|
||||
if screen.notchState == .open {
|
||||
openContent(workspace: workspace)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
closedContent(workspace: workspace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var closedContent: some View {
|
||||
private func closedContent(workspace: WorkspaceController) -> some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(abbreviate(terminalManager.activeTitle))
|
||||
Text(abbreviate(workspace.activeTitle))
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
.lineLimit(1)
|
||||
@@ -128,15 +141,15 @@ struct ContentView: View {
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
if resizeStartSize == nil {
|
||||
resizeStartSize = vm.notchSize
|
||||
resizeStartSize = screen.notchSize
|
||||
resizeStartMouseLocation = NSEvent.mouseLocation
|
||||
vm.beginInteractiveResize()
|
||||
screen.beginInteractiveResize()
|
||||
}
|
||||
|
||||
guard let startSize = resizeStartSize,
|
||||
let startMouseLocation = resizeStartMouseLocation else { return }
|
||||
let currentMouseLocation = NSEvent.mouseLocation
|
||||
vm.resizeOpenNotch(
|
||||
screen.resizeOpenNotch(
|
||||
to: CGSize(
|
||||
width: startSize.width + ((currentMouseLocation.x - startMouseLocation.x) * 2),
|
||||
height: startSize.height + (startMouseLocation.y - currentMouseLocation.y)
|
||||
@@ -146,24 +159,25 @@ struct ContentView: View {
|
||||
.onEnded { _ in
|
||||
resizeStartSize = nil
|
||||
resizeStartMouseLocation = nil
|
||||
vm.endInteractiveResize()
|
||||
screen.endInteractiveResize()
|
||||
}
|
||||
}
|
||||
|
||||
private var sizeAnimation: Animation? {
|
||||
guard !vm.isUserResizing, !vm.isPresetResizing else { return nil }
|
||||
return vm.notchState == .open ? vm.openAnimation : vm.closeAnimation
|
||||
guard !screen.isUserResizing, !screen.isPresetResizing else { return nil }
|
||||
return screen.notchState == .open ? screen.openAnimation : screen.closeAnimation
|
||||
}
|
||||
|
||||
/// Open layout: VStack with toolbar row on top, terminal in the middle,
|
||||
/// tab bar at the bottom. Every section has a black background.
|
||||
private var openContent: some View {
|
||||
private func openContent(workspace: WorkspaceController) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
// Toolbar row — right-aligned, solid black
|
||||
HStack {
|
||||
WorkspaceSwitcherView(screen: screen, orchestrator: orchestrator)
|
||||
Spacer()
|
||||
toolbarButton(icon: "arrow.up.forward.square", help: "Detach tab") {
|
||||
if let session = terminalManager.detachActiveTab() {
|
||||
if let session = workspace.detachActiveTab() {
|
||||
PopoutWindowController.shared.popout(session: session)
|
||||
}
|
||||
}
|
||||
@@ -172,20 +186,21 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
.padding(.top, 6)
|
||||
.padding(.leading, 10)
|
||||
.padding(.trailing, 10)
|
||||
.padding(.bottom, 2)
|
||||
.background(.black)
|
||||
|
||||
// Terminal — fills remaining space
|
||||
if let session = terminalManager.activeTab {
|
||||
SwiftTermView(session: session)
|
||||
if let session = workspace.activeTab {
|
||||
TerminalSessionView(session: session)
|
||||
.id(session.id)
|
||||
.padding(.leading, 10)
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
|
||||
// Tab bar
|
||||
TabBar(terminalManager: terminalManager)
|
||||
TabBar(workspace: workspace)
|
||||
}
|
||||
.background(.black)
|
||||
}
|
||||
@@ -199,38 +214,16 @@ struct ContentView: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(help)
|
||||
.accessibilityIdentifier("notch.toolbar.\(icon)")
|
||||
.help(help)
|
||||
}
|
||||
|
||||
// MARK: - Hover
|
||||
|
||||
private func handleHover(_ hovering: Bool) {
|
||||
if hovering {
|
||||
withAnimation(hoverAnimation) { vm.isHovering = true }
|
||||
scheduleHoverOpenIfNeeded()
|
||||
} else {
|
||||
hoverTask?.cancel()
|
||||
withAnimation(hoverAnimation) { vm.isHovering = false }
|
||||
vm.clearHoverOpenSuppression()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleHoverOpenIfNeeded() {
|
||||
hoverTask?.cancel()
|
||||
guard openNotchOnHover,
|
||||
vm.notchState == .closed,
|
||||
!vm.isCloseTransitionActive,
|
||||
!vm.suppressHoverOpenUntilHoverExit,
|
||||
vm.isHovering else { return }
|
||||
|
||||
hoverTask = Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: UInt64(minimumHoverDuration * 1_000_000_000))
|
||||
guard !Task.isCancelled,
|
||||
vm.isHovering,
|
||||
vm.notchState == .closed,
|
||||
!vm.isCloseTransitionActive,
|
||||
!vm.suppressHoverOpenUntilHoverExit else { return }
|
||||
vm.requestOpen?()
|
||||
withAnimation(hoverAnimation) {
|
||||
orchestrator.handleHoverChange(hovering, for: screen.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,3 +244,33 @@ private struct ResizeHandleShape: Shape {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
private struct WorkspaceScopedView<Content: View>: View {
|
||||
@ObservedObject var screen: ScreenContext
|
||||
@ObservedObject var screenRegistry: ScreenRegistry
|
||||
let content: (WorkspaceController) -> Content
|
||||
|
||||
init(
|
||||
screen: ScreenContext,
|
||||
screenRegistry: ScreenRegistry,
|
||||
@ViewBuilder content: @escaping (WorkspaceController) -> Content
|
||||
) {
|
||||
self.screen = screen
|
||||
self.screenRegistry = screenRegistry
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WorkspaceObservedView(workspace: screenRegistry.workspaceController(for: screen.id), content: content)
|
||||
.id(screen.workspaceID)
|
||||
}
|
||||
}
|
||||
|
||||
private struct WorkspaceObservedView<Content: View>: View {
|
||||
@ObservedObject var workspace: WorkspaceController
|
||||
let content: (WorkspaceController) -> Content
|
||||
|
||||
var body: some View {
|
||||
content(workspace)
|
||||
}
|
||||
}
|
||||
@@ -27,18 +27,16 @@ extension NSScreen {
|
||||
|
||||
/// Computes the closed-state notch size for this screen,
|
||||
/// respecting the user's height mode and custom height preferences.
|
||||
func closedNotchSize() -> CGSize {
|
||||
let height = closedNotchHeight()
|
||||
func closedNotchSize(using settings: AppSettings.DisplaySettings) -> CGSize {
|
||||
let height = closedNotchHeight(using: settings)
|
||||
let width = closedNotchWidth()
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
/// Height of the closed notch bar, determined by the user's chosen mode.
|
||||
private func closedNotchHeight() -> CGFloat {
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
private func closedNotchHeight(using settings: AppSettings.DisplaySettings) -> CGFloat {
|
||||
if hasNotch {
|
||||
let mode = NotchHeightMode(rawValue: defaults.integer(forKey: NotchSettings.Keys.notchHeightMode))
|
||||
let mode = NotchHeightMode(rawValue: settings.notchHeightMode)
|
||||
?? .matchRealNotchSize
|
||||
switch mode {
|
||||
case .matchRealNotchSize:
|
||||
@@ -46,16 +44,16 @@ extension NSScreen {
|
||||
case .matchMenuBar:
|
||||
return menuBarHeight()
|
||||
case .custom:
|
||||
return defaults.double(forKey: NotchSettings.Keys.notchHeight)
|
||||
return settings.notchHeight
|
||||
}
|
||||
} else {
|
||||
let mode = NonNotchHeightMode(rawValue: defaults.integer(forKey: NotchSettings.Keys.nonNotchHeightMode))
|
||||
let mode = NonNotchHeightMode(rawValue: settings.nonNotchHeightMode)
|
||||
?? .matchMenuBar
|
||||
switch mode {
|
||||
case .matchMenuBar:
|
||||
return menuBarHeight()
|
||||
case .custom:
|
||||
return defaults.double(forKey: NotchSettings.Keys.nonNotchHeight)
|
||||
return settings.nonNotchHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import AppKit
|
||||
import Carbon.HIToolbox
|
||||
import Combine
|
||||
|
||||
/// Manages global and local hotkeys.
|
||||
///
|
||||
/// The toggle hotkey uses Carbon's `RegisterEventHotKey` which works
|
||||
/// system-wide without Accessibility permission. Tab-level hotkeys
|
||||
/// system-wide without Accessibility permission. Notch-scoped hotkeys
|
||||
/// use a local `NSEvent` monitor (only fires when our app is active).
|
||||
@MainActor
|
||||
class HotkeyManager {
|
||||
|
||||
static let shared = HotkeyManager()
|
||||
@@ -17,47 +19,59 @@ class HotkeyManager {
|
||||
var onCloseTab: (() -> Void)?
|
||||
var onNextTab: (() -> Void)?
|
||||
var onPreviousTab: (() -> Void)?
|
||||
var onNextWorkspace: (() -> Void)?
|
||||
var onPreviousWorkspace: (() -> Void)?
|
||||
var onDetachTab: (() -> Void)?
|
||||
var onApplySizePreset: ((TerminalSizePreset) -> Void)?
|
||||
var onSwitchToTab: ((Int) -> Void)?
|
||||
var onSwitchToWorkspace: ((WorkspaceID) -> Void)?
|
||||
|
||||
/// Tab-level hotkeys only fire when the notch is open.
|
||||
/// Notch-scoped hotkeys only fire when the notch is open.
|
||||
var isNotchOpen: Bool = false
|
||||
|
||||
private var hotKeyRef: EventHotKeyRef?
|
||||
private var eventHandlerRef: EventHandlerRef?
|
||||
private var localMonitor: Any?
|
||||
private var defaultsObserver: NSObjectProtocol?
|
||||
private let settingsProvider: TerminalSessionConfigurationProviding
|
||||
private let workspaceRegistry: WorkspaceRegistry
|
||||
private var settingsCancellable: AnyCancellable?
|
||||
|
||||
private init() {}
|
||||
init(
|
||||
settingsProvider: TerminalSessionConfigurationProviding? = nil,
|
||||
workspaceRegistry: WorkspaceRegistry? = nil
|
||||
) {
|
||||
self.settingsProvider = settingsProvider ?? AppSettingsController.shared
|
||||
self.workspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
|
||||
}
|
||||
|
||||
// MARK: - Resolved bindings (live from UserDefaults)
|
||||
// MARK: - Resolved bindings from typed runtime settings
|
||||
|
||||
private var toggleBinding: HotkeyBinding {
|
||||
binding(for: NotchSettings.Keys.hotkeyToggle) ?? .cmdReturn
|
||||
settingsProvider.hotkeySettings.toggle
|
||||
}
|
||||
private var newTabBinding: HotkeyBinding {
|
||||
binding(for: NotchSettings.Keys.hotkeyNewTab) ?? .cmdT
|
||||
settingsProvider.hotkeySettings.newTab
|
||||
}
|
||||
private var closeTabBinding: HotkeyBinding {
|
||||
binding(for: NotchSettings.Keys.hotkeyCloseTab) ?? .cmdW
|
||||
settingsProvider.hotkeySettings.closeTab
|
||||
}
|
||||
private var nextTabBinding: HotkeyBinding {
|
||||
binding(for: NotchSettings.Keys.hotkeyNextTab) ?? .cmdShiftRB
|
||||
settingsProvider.hotkeySettings.nextTab
|
||||
}
|
||||
private var prevTabBinding: HotkeyBinding {
|
||||
binding(for: NotchSettings.Keys.hotkeyPreviousTab) ?? .cmdShiftLB
|
||||
settingsProvider.hotkeySettings.previousTab
|
||||
}
|
||||
private var nextWorkspaceBinding: HotkeyBinding {
|
||||
settingsProvider.hotkeySettings.nextWorkspace
|
||||
}
|
||||
private var previousWorkspaceBinding: HotkeyBinding {
|
||||
settingsProvider.hotkeySettings.previousWorkspace
|
||||
}
|
||||
private var detachBinding: HotkeyBinding {
|
||||
binding(for: NotchSettings.Keys.hotkeyDetachTab) ?? .cmdD
|
||||
settingsProvider.hotkeySettings.detachTab
|
||||
}
|
||||
private var sizePresets: [TerminalSizePreset] {
|
||||
TerminalSizePresetStore.load()
|
||||
}
|
||||
|
||||
private func binding(for key: String) -> HotkeyBinding? {
|
||||
guard let json = UserDefaults.standard.string(forKey: key) else { return nil }
|
||||
return HotkeyBinding.fromJSON(json)
|
||||
settingsProvider.terminalSizePresets
|
||||
}
|
||||
|
||||
// MARK: - Start / Stop
|
||||
@@ -73,10 +87,7 @@ class HotkeyManager {
|
||||
unregisterToggleHotkey()
|
||||
removeCarbonHandler()
|
||||
removeLocalMonitor()
|
||||
if let obs = defaultsObserver {
|
||||
NotificationCenter.default.removeObserver(obs)
|
||||
defaultsObserver = nil
|
||||
}
|
||||
settingsCancellable = nil
|
||||
}
|
||||
|
||||
// MARK: - Carbon global hotkey (toggle)
|
||||
@@ -130,7 +141,7 @@ class HotkeyManager {
|
||||
|
||||
let binding = toggleBinding
|
||||
let carbonMods = carbonModifiers(from: binding.modifiers)
|
||||
var hotKeyID = EventHotKeyID(
|
||||
let hotKeyID = EventHotKeyID(
|
||||
signature: OSType(0x444E5452), // "DNTR"
|
||||
id: 1
|
||||
)
|
||||
@@ -163,18 +174,20 @@ class HotkeyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-register the toggle hotkey whenever the user changes it in settings.
|
||||
/// Re-register the toggle hotkey whenever the typed settings change.
|
||||
private func observeToggleHotkeyChanges() {
|
||||
defaultsObserver = NotificationCenter.default.addObserver(
|
||||
forName: UserDefaults.didChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.registerToggleHotkey()
|
||||
}
|
||||
guard let settingsProvider = settingsProvider as? AppSettingsController else { return }
|
||||
|
||||
settingsCancellable = settingsProvider.$settings
|
||||
.map(\.hotkeys.toggle)
|
||||
.removeDuplicates()
|
||||
.dropFirst()
|
||||
.sink { [weak self] _ in
|
||||
self?.registerToggleHotkey()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Local monitor (tab-level hotkeys, only when our app is active)
|
||||
// MARK: - Local monitor (notch-level hotkeys, only when our app is active)
|
||||
|
||||
private func installLocalMonitor() {
|
||||
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
||||
@@ -190,9 +203,9 @@ class HotkeyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles tab-level hotkeys. Returns true if the event was consumed.
|
||||
/// Handles notch-scoped hotkeys. Returns true if the event was consumed.
|
||||
private func handleLocalKeyEvent(_ event: NSEvent) -> Bool {
|
||||
// Tab hotkeys only when the notch is open and focused
|
||||
// Local shortcuts only fire when the notch is open and focused.
|
||||
guard isNotchOpen else { return false }
|
||||
|
||||
if newTabBinding.matches(event) {
|
||||
@@ -211,10 +224,25 @@ class HotkeyManager {
|
||||
onPreviousTab?()
|
||||
return true
|
||||
}
|
||||
if nextWorkspaceBinding.matches(event) {
|
||||
onNextWorkspace?()
|
||||
return true
|
||||
}
|
||||
if previousWorkspaceBinding.matches(event) {
|
||||
onPreviousWorkspace?()
|
||||
return true
|
||||
}
|
||||
if detachBinding.matches(event) {
|
||||
onDetachTab?()
|
||||
return true
|
||||
}
|
||||
for summary in workspaceRegistry.workspaceSummaries {
|
||||
guard let binding = summary.hotkey else { continue }
|
||||
if binding.matches(event) {
|
||||
onSwitchToWorkspace?(summary.id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
for preset in sizePresets {
|
||||
guard let binding = preset.hotkey else { continue }
|
||||
if binding.matches(event) {
|
||||
@@ -41,7 +41,7 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
||||
win.isReleasedWhenClosed = false
|
||||
|
||||
let hostingView = NSHostingView(
|
||||
rootView: SwiftTermView(session: session)
|
||||
rootView: TerminalSessionView(session: session)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black)
|
||||
.preferredColorScheme(.dark)
|
||||
@@ -67,10 +67,10 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
guard let window = notification.object as? NSWindow,
|
||||
let entry = windows.first(where: { $0.value === window }),
|
||||
let terminalView = sessions[entry.key]?.terminalView,
|
||||
let terminalView = sessions[entry.key]?.view,
|
||||
terminalView.window === window else { return }
|
||||
|
||||
window.makeFirstResponder(terminalView)
|
||||
sessions[entry.key]?.focus()
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
297
CommandNotch/CommandNotch/Managers/ScreenManager.swift
Normal file
@@ -0,0 +1,297 @@
|
||||
import AppKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
/// Coordinates screen/workspace state with notch lifecycle and
|
||||
/// delegates raw window work to `WindowCoordinator`.
|
||||
@MainActor
|
||||
final class ScreenManager: ObservableObject {
|
||||
static let shared = ScreenManager()
|
||||
|
||||
private let screenRegistry = ScreenRegistry.shared
|
||||
private let workspaceRegistry = WorkspaceRegistry.shared
|
||||
private let windowCoordinator = WindowCoordinator()
|
||||
private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self)
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private init() {}
|
||||
|
||||
private var showOnAllDisplays: Bool {
|
||||
AppSettingsController.shared.settings.display.showOnAllDisplays
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
func start() {
|
||||
screenRegistry.refreshConnectedScreens()
|
||||
observeScreenChanges()
|
||||
rebuildWindows()
|
||||
setupHotkeys()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
cleanupAllWindows()
|
||||
cancellables.removeAll()
|
||||
HotkeyManager.shared.stop()
|
||||
}
|
||||
|
||||
// MARK: - Hotkey wiring
|
||||
|
||||
private func setupHotkeys() {
|
||||
let hotkeyManager = HotkeyManager.shared
|
||||
|
||||
hotkeyManager.onToggle = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.orchestrator.toggleOnActiveScreen() }
|
||||
}
|
||||
hotkeyManager.onNewTab = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.activeWorkspace().newTab() }
|
||||
}
|
||||
hotkeyManager.onCloseTab = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.activeWorkspace().closeActiveTab() }
|
||||
}
|
||||
hotkeyManager.onNextTab = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.activeWorkspace().nextTab() }
|
||||
}
|
||||
hotkeyManager.onPreviousTab = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.activeWorkspace().previousTab() }
|
||||
}
|
||||
hotkeyManager.onNextWorkspace = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.switchWorkspace(offset: 1) }
|
||||
}
|
||||
hotkeyManager.onPreviousWorkspace = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.switchWorkspace(offset: -1) }
|
||||
}
|
||||
hotkeyManager.onDetachTab = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.detachActiveTab() }
|
||||
}
|
||||
hotkeyManager.onApplySizePreset = { [weak self] preset in
|
||||
MainActor.assumeIsolated { self?.applySizePreset(preset) }
|
||||
}
|
||||
hotkeyManager.onSwitchToTab = { [weak self] index in
|
||||
MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) }
|
||||
}
|
||||
hotkeyManager.onSwitchToWorkspace = { [weak self] workspaceID in
|
||||
MainActor.assumeIsolated { self?.switchActiveScreen(to: workspaceID) }
|
||||
}
|
||||
|
||||
hotkeyManager.start()
|
||||
}
|
||||
|
||||
// MARK: - Toggle
|
||||
|
||||
func toggleNotchOnActiveScreen() {
|
||||
orchestrator.toggleOnActiveScreen()
|
||||
}
|
||||
|
||||
// MARK: - Open / Close
|
||||
|
||||
func openNotch(screenID: ScreenID) {
|
||||
orchestrator.open(screenID: screenID)
|
||||
}
|
||||
|
||||
func closeNotch(screenID: ScreenID) {
|
||||
orchestrator.close(screenID: screenID)
|
||||
}
|
||||
|
||||
private func detachActiveTab() {
|
||||
if let session = activeWorkspace().detachActiveTab() {
|
||||
DispatchQueue.main.async {
|
||||
PopoutWindowController.shared.popout(session: session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func switchWorkspace(offset: Int) {
|
||||
guard let screenID = screenRegistry.activeScreenID() else { return }
|
||||
let currentWorkspaceID = screenRegistry.screenContext(for: screenID)?.workspaceID ?? workspaceRegistry.defaultWorkspaceID
|
||||
let nextWorkspaceID = offset >= 0
|
||||
? workspaceRegistry.nextWorkspaceID(after: currentWorkspaceID)
|
||||
: workspaceRegistry.previousWorkspaceID(before: currentWorkspaceID)
|
||||
|
||||
guard let nextWorkspaceID else { return }
|
||||
switchScreen(screenID, to: nextWorkspaceID)
|
||||
}
|
||||
|
||||
private func switchActiveScreen(to workspaceID: WorkspaceID) {
|
||||
guard let screenID = screenRegistry.activeScreenID() else { return }
|
||||
switchScreen(screenID, to: workspaceID)
|
||||
}
|
||||
|
||||
private func switchScreen(_ screenID: ScreenID, to workspaceID: WorkspaceID) {
|
||||
screenRegistry.assignWorkspace(workspaceID, to: screenID)
|
||||
|
||||
guard let context = screenRegistry.screenContext(for: screenID),
|
||||
context.notchState == .open else {
|
||||
return
|
||||
}
|
||||
|
||||
orchestrator.open(screenID: screenID)
|
||||
}
|
||||
|
||||
func applySizePreset(_ preset: TerminalSizePreset) {
|
||||
guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else {
|
||||
AppSettingsController.shared.update {
|
||||
$0.display.openWidth = preset.width
|
||||
$0.display.openHeight = preset.height
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let startSize = context.notchSize
|
||||
let targetSize = context.setStoredOpenSize(preset.size)
|
||||
windowCoordinator.animatePresetResize(
|
||||
for: context.id,
|
||||
context: context,
|
||||
from: startSize,
|
||||
to: targetSize,
|
||||
duration: context.openAnimationDuration
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Window creation
|
||||
|
||||
func rebuildWindows() {
|
||||
cleanupAllWindows()
|
||||
screenRegistry.refreshConnectedScreens()
|
||||
|
||||
for screen in visibleScreens() {
|
||||
createWindow(for: screen)
|
||||
}
|
||||
}
|
||||
|
||||
private func createWindow(for screen: NSScreen) {
|
||||
let screenID = screen.displayUUID
|
||||
guard let context = screenRegistry.screenContext(for: screenID) else { return }
|
||||
|
||||
context.requestOpen = { [weak self] in
|
||||
self?.orchestrator.open(screenID: screenID)
|
||||
}
|
||||
context.requestClose = { [weak self] in
|
||||
self?.orchestrator.close(screenID: screenID)
|
||||
}
|
||||
context.requestWindowResize = { [weak self] in
|
||||
guard let self,
|
||||
let context = self.screenRegistry.screenContext(for: screenID) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.windowCoordinator.updateWindowFrame(
|
||||
for: screenID,
|
||||
context: context,
|
||||
centerHorizontally: true
|
||||
)
|
||||
}
|
||||
context.requestTerminalFocus = { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
self.windowCoordinator.focusActiveTerminal(for: screenID) { [weak self] in
|
||||
self?.screenRegistry.workspaceController(for: screenID).activeTab?.view
|
||||
}
|
||||
}
|
||||
|
||||
let hostingView = NSHostingView(
|
||||
rootView: ContentView(
|
||||
screen: context,
|
||||
orchestrator: orchestrator
|
||||
)
|
||||
.preferredColorScheme(.dark)
|
||||
)
|
||||
|
||||
windowCoordinator.createWindow(
|
||||
on: screen,
|
||||
context: context,
|
||||
contentView: hostingView,
|
||||
onResignKey: { [weak self] in
|
||||
guard !context.suppressCloseOnFocusLoss else { return }
|
||||
self?.orchestrator.close(screenID: screenID)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Repositioning
|
||||
|
||||
func repositionWindows() {
|
||||
screenRegistry.refreshConnectedScreens()
|
||||
|
||||
for context in screenRegistry.allScreens() {
|
||||
context.refreshClosedSize()
|
||||
windowCoordinator.repositionWindow(
|
||||
for: context.id,
|
||||
context: context,
|
||||
centerHorizontally: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
private func cleanupAllWindows() {
|
||||
orchestrator.cancelAllPendingWork()
|
||||
windowCoordinator.cleanupAllWindows()
|
||||
}
|
||||
|
||||
// MARK: - Screen observation
|
||||
|
||||
private func observeScreenChanges() {
|
||||
NotificationCenter.default.publisher(for: NSApplication.didChangeScreenParametersNotification)
|
||||
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.handleScreenConfigurationChange()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func handleScreenConfigurationChange() {
|
||||
screenRegistry.refreshConnectedScreens()
|
||||
|
||||
let currentScreenIDs = Set(visibleScreens().map(\.displayUUID))
|
||||
let knownScreenIDs = windowCoordinator.windowScreenIDs()
|
||||
|
||||
if currentScreenIDs != knownScreenIDs {
|
||||
rebuildWindows()
|
||||
} else {
|
||||
repositionWindows()
|
||||
}
|
||||
}
|
||||
|
||||
private func activeWorkspace() -> WorkspaceController {
|
||||
guard let screenID = screenRegistry.activeScreenID() else {
|
||||
return WorkspaceRegistry.shared.defaultWorkspaceController
|
||||
}
|
||||
|
||||
return screenRegistry.workspaceController(for: screenID)
|
||||
}
|
||||
|
||||
private func visibleScreens() -> [NSScreen] {
|
||||
if showOnAllDisplays {
|
||||
return NSScreen.screens
|
||||
}
|
||||
|
||||
return [NSScreen.main].compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
extension ScreenManager: NotchPresentationHost {
|
||||
func canPresentNotch(for screenID: ScreenID) -> Bool {
|
||||
windowCoordinator.hasWindow(for: screenID)
|
||||
}
|
||||
|
||||
func performOpenPresentation(for screenID: ScreenID) {
|
||||
guard screenRegistry.screenContext(for: screenID) != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
windowCoordinator.presentOpen(for: screenID) { [weak self] in
|
||||
self?.screenRegistry.workspaceController(for: screenID).activeTab?.view
|
||||
}
|
||||
}
|
||||
|
||||
func performClosePresentation(for screenID: ScreenID) {
|
||||
guard screenRegistry.screenContext(for: screenID) != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
windowCoordinator.presentClose(for: screenID)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
defer: false
|
||||
)
|
||||
win.title = "CommandNotch Settings"
|
||||
win.identifier = NSUserInterfaceItemIdentifier("settings.window")
|
||||
win.contentView = hostingView
|
||||
win.center()
|
||||
win.delegate = self
|
||||
291
CommandNotch/CommandNotch/Managers/WindowCoordinator.swift
Normal file
@@ -0,0 +1,291 @@
|
||||
import AppKit
|
||||
import QuartzCore
|
||||
import SwiftUI
|
||||
|
||||
struct WindowFrameCalculator {
|
||||
static let horizontalPadding: CGFloat = 40
|
||||
static let verticalPadding: CGFloat = 20
|
||||
|
||||
static func targetFrame(
|
||||
screenFrame: CGRect,
|
||||
currentWindowFrame: CGRect,
|
||||
notchState: NotchState,
|
||||
contentSize: CGSize,
|
||||
centerHorizontally: Bool
|
||||
) -> CGRect {
|
||||
let windowWidth = contentSize.width + horizontalPadding
|
||||
let windowHeight = contentSize.height + verticalPadding
|
||||
let centeredX = screenFrame.origin.x + ((screenFrame.width - windowWidth) / 2)
|
||||
|
||||
let x: CGFloat
|
||||
if centerHorizontally || notchState == .closed {
|
||||
x = centeredX
|
||||
} else {
|
||||
x = min(
|
||||
max(currentWindowFrame.minX, screenFrame.minX),
|
||||
screenFrame.maxX - windowWidth
|
||||
)
|
||||
}
|
||||
|
||||
return CGRect(
|
||||
x: x,
|
||||
y: screenFrame.origin.y + screenFrame.height - windowHeight,
|
||||
width: windowWidth,
|
||||
height: windowHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class WindowCoordinator {
|
||||
private let focusRetryDelay: TimeInterval
|
||||
private let presetResizeFrameInterval: TimeInterval
|
||||
private let screenLookup: @MainActor (ScreenID) -> NSScreen?
|
||||
private let applicationActivator: @MainActor () -> Void
|
||||
private let hotkeyOpenStateHandler: @MainActor (Bool) -> Void
|
||||
|
||||
private(set) var windows: [ScreenID: NotchWindow] = [:]
|
||||
private var presetResizeTimers: [ScreenID: Timer] = [:]
|
||||
|
||||
init(
|
||||
focusRetryDelay: TimeInterval = 0.01,
|
||||
presetResizeFrameInterval: TimeInterval = 1.0 / 60.0,
|
||||
screenLookup: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
|
||||
NSScreen.screens.first { $0.displayUUID == screenID }
|
||||
},
|
||||
applicationActivator: @escaping @MainActor () -> Void = {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
},
|
||||
hotkeyOpenStateHandler: @escaping @MainActor (Bool) -> Void = { isOpen in
|
||||
HotkeyManager.shared.isNotchOpen = isOpen
|
||||
}
|
||||
) {
|
||||
self.focusRetryDelay = focusRetryDelay
|
||||
self.presetResizeFrameInterval = presetResizeFrameInterval
|
||||
self.screenLookup = screenLookup
|
||||
self.applicationActivator = applicationActivator
|
||||
self.hotkeyOpenStateHandler = hotkeyOpenStateHandler
|
||||
}
|
||||
|
||||
func hasWindow(for screenID: ScreenID) -> Bool {
|
||||
windows[screenID] != nil
|
||||
}
|
||||
|
||||
func windowScreenIDs() -> Set<ScreenID> {
|
||||
Set(windows.keys)
|
||||
}
|
||||
|
||||
func createWindow(
|
||||
on screen: NSScreen,
|
||||
context: ScreenContext,
|
||||
contentView: NSView,
|
||||
onResignKey: @escaping () -> Void
|
||||
) {
|
||||
let initialFrame = WindowFrameCalculator.targetFrame(
|
||||
screenFrame: screen.frame,
|
||||
currentWindowFrame: .zero,
|
||||
notchState: context.notchState,
|
||||
contentSize: context.openNotchSize,
|
||||
centerHorizontally: true
|
||||
)
|
||||
|
||||
let window = NotchWindow(
|
||||
contentRect: initialFrame,
|
||||
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.onResignKey = onResignKey
|
||||
|
||||
let containerView = NSView(frame: NSRect(origin: .zero, size: initialFrame.size))
|
||||
containerView.autoresizesSubviews = true
|
||||
containerView.wantsLayer = true
|
||||
containerView.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
|
||||
contentView.frame = containerView.bounds
|
||||
contentView.autoresizingMask = [.width, .height]
|
||||
containerView.addSubview(contentView)
|
||||
window.contentView = containerView
|
||||
|
||||
windows[context.id] = window
|
||||
|
||||
updateWindowFrame(for: context.id, context: context, centerHorizontally: true)
|
||||
window.orderFrontRegardless()
|
||||
}
|
||||
|
||||
func repositionWindow(for screenID: ScreenID, context: ScreenContext, centerHorizontally: Bool = false) {
|
||||
updateWindowFrame(for: screenID, context: context, centerHorizontally: centerHorizontally)
|
||||
}
|
||||
|
||||
func updateWindowFrame(
|
||||
for screenID: ScreenID,
|
||||
context: ScreenContext,
|
||||
contentSize: CGSize? = nil,
|
||||
centerHorizontally: Bool = false
|
||||
) {
|
||||
guard let screen = screenLookup(screenID),
|
||||
let window = windows[screenID] else {
|
||||
return
|
||||
}
|
||||
|
||||
let frame = WindowFrameCalculator.targetFrame(
|
||||
screenFrame: screen.frame,
|
||||
currentWindowFrame: window.frame,
|
||||
notchState: context.notchState,
|
||||
contentSize: resolvedContentSize(for: context, override: contentSize),
|
||||
centerHorizontally: centerHorizontally
|
||||
)
|
||||
|
||||
guard !window.frame.equalTo(frame) else { return }
|
||||
window.setFrame(frame, display: false)
|
||||
}
|
||||
|
||||
func animatePresetResize(
|
||||
for screenID: ScreenID,
|
||||
context: ScreenContext,
|
||||
from startSize: CGSize,
|
||||
to targetSize: CGSize,
|
||||
duration: TimeInterval
|
||||
) {
|
||||
cancelPresetResize(for: screenID)
|
||||
|
||||
guard startSize != targetSize else {
|
||||
context.notchSize = targetSize
|
||||
updateWindowFrame(for: screenID, context: context, contentSize: targetSize, centerHorizontally: true)
|
||||
return
|
||||
}
|
||||
|
||||
context.isPresetResizing = true
|
||||
let startTime = CACurrentMediaTime()
|
||||
let frameInterval = max(duration, presetResizeFrameInterval)
|
||||
|
||||
let timer = Timer(timeInterval: presetResizeFrameInterval, repeats: true) { [weak self] timer in
|
||||
MainActor.assumeIsolated {
|
||||
guard let self else {
|
||||
timer.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
let elapsed = CACurrentMediaTime() - startTime
|
||||
let progress = min(1, elapsed / frameInterval)
|
||||
let easedProgress = 0.5 - (cos(.pi * progress) / 2)
|
||||
let size = CGSize(
|
||||
width: startSize.width + ((targetSize.width - startSize.width) * easedProgress),
|
||||
height: startSize.height + ((targetSize.height - startSize.height) * easedProgress)
|
||||
)
|
||||
|
||||
context.notchSize = size
|
||||
self.updateWindowFrame(for: screenID, context: context, contentSize: size, centerHorizontally: true)
|
||||
|
||||
if progress >= 1 {
|
||||
context.notchSize = targetSize
|
||||
context.isPresetResizing = false
|
||||
self.updateWindowFrame(for: screenID, context: context, contentSize: targetSize, centerHorizontally: true)
|
||||
self.presetResizeTimers[screenID] = nil
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
presetResizeTimers[screenID] = timer
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
timer.fire()
|
||||
}
|
||||
|
||||
func presentOpen(
|
||||
for screenID: ScreenID,
|
||||
terminalViewProvider: @escaping @MainActor () -> NSView?
|
||||
) {
|
||||
guard let window = windows[screenID] else { return }
|
||||
|
||||
window.isNotchOpen = true
|
||||
updateHotkeyOpenState()
|
||||
applicationActivator()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
focusActiveTerminal(
|
||||
in: screenID,
|
||||
attemptsRemaining: 12,
|
||||
terminalViewProvider: terminalViewProvider
|
||||
)
|
||||
}
|
||||
|
||||
func focusActiveTerminal(
|
||||
for screenID: ScreenID,
|
||||
terminalViewProvider: @escaping @MainActor () -> NSView?
|
||||
) {
|
||||
focusActiveTerminal(
|
||||
in: screenID,
|
||||
attemptsRemaining: 12,
|
||||
terminalViewProvider: terminalViewProvider
|
||||
)
|
||||
}
|
||||
|
||||
func presentClose(for screenID: ScreenID) {
|
||||
guard let window = windows[screenID] else { return }
|
||||
|
||||
window.isNotchOpen = false
|
||||
updateHotkeyOpenState()
|
||||
}
|
||||
|
||||
func cleanupAllWindows() {
|
||||
for timer in presetResizeTimers.values {
|
||||
timer.invalidate()
|
||||
}
|
||||
presetResizeTimers.removeAll()
|
||||
|
||||
for window in windows.values {
|
||||
window.orderOut(nil)
|
||||
window.close()
|
||||
}
|
||||
|
||||
windows.removeAll()
|
||||
updateHotkeyOpenState()
|
||||
}
|
||||
|
||||
private func focusActiveTerminal(
|
||||
in screenID: ScreenID,
|
||||
attemptsRemaining: Int,
|
||||
terminalViewProvider: @escaping @MainActor () -> NSView?
|
||||
) {
|
||||
guard let window = windows[screenID],
|
||||
let terminalView = terminalViewProvider() else {
|
||||
return
|
||||
}
|
||||
|
||||
if terminalView.window === window {
|
||||
window.makeFirstResponder(terminalView)
|
||||
return
|
||||
}
|
||||
|
||||
guard attemptsRemaining > 0 else { return }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + focusRetryDelay) { [weak self] in
|
||||
Task { @MainActor in
|
||||
self?.focusActiveTerminal(
|
||||
in: screenID,
|
||||
attemptsRemaining: attemptsRemaining - 1,
|
||||
terminalViewProvider: terminalViewProvider
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelPresetResize(for screenID: ScreenID) {
|
||||
presetResizeTimers[screenID]?.invalidate()
|
||||
presetResizeTimers[screenID] = nil
|
||||
}
|
||||
|
||||
private func resolvedContentSize(for context: ScreenContext, override: CGSize?) -> CGSize {
|
||||
if let override {
|
||||
return override
|
||||
}
|
||||
|
||||
return context.notchState == .open ? context.notchSize : context.openNotchSize
|
||||
}
|
||||
|
||||
private func updateHotkeyOpenState() {
|
||||
hotkeyOpenStateHandler(windows.values.contains(where: \.isNotchOpen))
|
||||
}
|
||||
}
|
||||
259
CommandNotch/CommandNotch/Models/AppSettings.swift
Normal file
@@ -0,0 +1,259 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
struct AppSettings: Equatable, Codable {
|
||||
var display: DisplaySettings
|
||||
var behavior: BehaviorSettings
|
||||
var appearance: AppearanceSettings
|
||||
var animation: AnimationSettings
|
||||
var terminal: TerminalSettings
|
||||
var hotkeys: HotkeySettings
|
||||
|
||||
static let `default` = AppSettings(
|
||||
display: DisplaySettings(
|
||||
showOnAllDisplays: NotchSettings.Defaults.showOnAllDisplays,
|
||||
showMenuBarIcon: NotchSettings.Defaults.showMenuBarIcon,
|
||||
launchAtLogin: NotchSettings.Defaults.launchAtLogin,
|
||||
notchHeightMode: NotchSettings.Defaults.notchHeightMode,
|
||||
notchHeight: NotchSettings.Defaults.notchHeight,
|
||||
nonNotchHeightMode: NotchSettings.Defaults.nonNotchHeightMode,
|
||||
nonNotchHeight: NotchSettings.Defaults.nonNotchHeight,
|
||||
openWidth: NotchSettings.Defaults.openWidth,
|
||||
openHeight: NotchSettings.Defaults.openHeight
|
||||
),
|
||||
behavior: BehaviorSettings(
|
||||
openNotchOnHover: NotchSettings.Defaults.openNotchOnHover,
|
||||
minimumHoverDuration: NotchSettings.Defaults.minimumHoverDuration,
|
||||
enableGestures: NotchSettings.Defaults.enableGestures,
|
||||
gestureSensitivity: NotchSettings.Defaults.gestureSensitivity
|
||||
),
|
||||
appearance: AppearanceSettings(
|
||||
enableShadow: NotchSettings.Defaults.enableShadow,
|
||||
shadowRadius: NotchSettings.Defaults.shadowRadius,
|
||||
shadowOpacity: NotchSettings.Defaults.shadowOpacity,
|
||||
cornerRadiusScaling: NotchSettings.Defaults.cornerRadiusScaling,
|
||||
notchOpacity: NotchSettings.Defaults.notchOpacity,
|
||||
blurRadius: NotchSettings.Defaults.blurRadius
|
||||
),
|
||||
animation: AnimationSettings(
|
||||
openSpringResponse: NotchSettings.Defaults.openSpringResponse,
|
||||
openSpringDamping: NotchSettings.Defaults.openSpringDamping,
|
||||
closeSpringResponse: NotchSettings.Defaults.closeSpringResponse,
|
||||
closeSpringDamping: NotchSettings.Defaults.closeSpringDamping,
|
||||
hoverSpringResponse: NotchSettings.Defaults.hoverSpringResponse,
|
||||
hoverSpringDamping: NotchSettings.Defaults.hoverSpringDamping,
|
||||
resizeAnimationDuration: NotchSettings.Defaults.resizeAnimationDuration
|
||||
),
|
||||
terminal: TerminalSettings(
|
||||
fontSize: NotchSettings.Defaults.terminalFontSize,
|
||||
shellPath: NotchSettings.Defaults.terminalShell,
|
||||
themeRawValue: NotchSettings.Defaults.terminalTheme,
|
||||
scrollbackLines: NotchSettings.Defaults.terminalScrollbackLines,
|
||||
backendRawValue: NotchSettings.Defaults.terminalBackend,
|
||||
termTypeRawValue: NotchSettings.Defaults.terminalTermType,
|
||||
sizePresetsJSON: NotchSettings.Defaults.terminalSizePresets
|
||||
),
|
||||
hotkeys: HotkeySettings(
|
||||
toggle: .cmdReturn,
|
||||
newTab: .cmdT,
|
||||
closeTab: .cmdW,
|
||||
nextTab: .cmdShiftRB,
|
||||
previousTab: .cmdShiftLB,
|
||||
nextWorkspace: .cmdShiftDown,
|
||||
previousWorkspace: .cmdShiftUp,
|
||||
detachTab: .cmdD
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
extension AppSettings {
|
||||
struct DisplaySettings: Equatable, Codable {
|
||||
var showOnAllDisplays: Bool
|
||||
var showMenuBarIcon: Bool
|
||||
var launchAtLogin: Bool
|
||||
var notchHeightMode: Int
|
||||
var notchHeight: Double
|
||||
var nonNotchHeightMode: Int
|
||||
var nonNotchHeight: Double
|
||||
var openWidth: Double
|
||||
var openHeight: Double
|
||||
}
|
||||
|
||||
struct BehaviorSettings: Equatable, Codable {
|
||||
var openNotchOnHover: Bool
|
||||
var minimumHoverDuration: Double
|
||||
var enableGestures: Bool
|
||||
var gestureSensitivity: Double
|
||||
}
|
||||
|
||||
struct AppearanceSettings: Equatable, Codable {
|
||||
var enableShadow: Bool
|
||||
var shadowRadius: Double
|
||||
var shadowOpacity: Double
|
||||
var cornerRadiusScaling: Bool
|
||||
var notchOpacity: Double
|
||||
var blurRadius: Double
|
||||
}
|
||||
|
||||
struct AnimationSettings: Equatable, Codable {
|
||||
var openSpringResponse: Double
|
||||
var openSpringDamping: Double
|
||||
var closeSpringResponse: Double
|
||||
var closeSpringDamping: Double
|
||||
var hoverSpringResponse: Double
|
||||
var hoverSpringDamping: Double
|
||||
var resizeAnimationDuration: Double
|
||||
}
|
||||
|
||||
struct TerminalSettings: Equatable, Codable {
|
||||
var fontSize: Double
|
||||
var shellPath: String
|
||||
var themeRawValue: String
|
||||
var scrollbackLines: Int
|
||||
var backendRawValue: String
|
||||
var termTypeRawValue: String
|
||||
var sizePresetsJSON: String
|
||||
|
||||
var theme: TerminalTheme {
|
||||
TerminalTheme.resolve(themeRawValue)
|
||||
}
|
||||
|
||||
var backend: TerminalBackendPreference {
|
||||
TerminalBackendPreference.resolve(backendRawValue)
|
||||
}
|
||||
|
||||
var termType: TerminalTermTypePreference {
|
||||
TerminalTermTypePreference.resolve(termTypeRawValue)
|
||||
}
|
||||
|
||||
var sizePresets: [TerminalSizePreset] {
|
||||
TerminalSizePresetStore.decodePresets(from: sizePresetsJSON) ?? TerminalSizePresetStore.loadDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
struct HotkeySettings: Equatable, Codable {
|
||||
var toggle: HotkeyBinding
|
||||
var newTab: HotkeyBinding
|
||||
var closeTab: HotkeyBinding
|
||||
var nextTab: HotkeyBinding
|
||||
var previousTab: HotkeyBinding
|
||||
var nextWorkspace: HotkeyBinding
|
||||
var previousWorkspace: HotkeyBinding
|
||||
var detachTab: HotkeyBinding
|
||||
}
|
||||
}
|
||||
|
||||
extension AppSettings.DisplaySettings {
|
||||
struct LayoutSignature: Equatable {
|
||||
var notchHeightMode: Int
|
||||
var notchHeight: Double
|
||||
var nonNotchHeightMode: Int
|
||||
var nonNotchHeight: Double
|
||||
var openWidth: Double
|
||||
var openHeight: Double
|
||||
}
|
||||
|
||||
var layoutSignature: LayoutSignature {
|
||||
LayoutSignature(
|
||||
notchHeightMode: notchHeightMode,
|
||||
notchHeight: notchHeight,
|
||||
nonNotchHeightMode: nonNotchHeightMode,
|
||||
nonNotchHeight: nonNotchHeight,
|
||||
openWidth: openWidth,
|
||||
openHeight: openHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalSessionConfiguration: Equatable {
|
||||
var fontSize: CGFloat
|
||||
var theme: TerminalTheme
|
||||
var shellPath: String
|
||||
var scrollbackLines: Int
|
||||
var backendPreference: TerminalBackendPreference
|
||||
var termTypePreference: TerminalTermTypePreference
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol TerminalSessionConfigurationProviding: AnyObject {
|
||||
var terminalSessionConfiguration: TerminalSessionConfiguration { get }
|
||||
var hotkeySettings: AppSettings.HotkeySettings { get }
|
||||
var terminalSizePresets: [TerminalSizePreset] { get }
|
||||
}
|
||||
|
||||
enum TerminalBackendPreference: String, CaseIterable, Codable, Identifiable {
|
||||
case ghostty
|
||||
case swiftTerm = "swiftterm"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .ghostty:
|
||||
return "Ghostty"
|
||||
case .swiftTerm:
|
||||
return "SwiftTerm"
|
||||
}
|
||||
}
|
||||
|
||||
var detail: String {
|
||||
switch self {
|
||||
case .ghostty:
|
||||
return "Higher-fidelity terminal emulation with Ghostty's renderer and input pipeline."
|
||||
case .swiftTerm:
|
||||
return "Legacy backend with simpler integration and a smaller compatibility surface."
|
||||
}
|
||||
}
|
||||
|
||||
static func resolve(_ rawValue: String) -> TerminalBackendPreference {
|
||||
TerminalBackendPreference(rawValue: rawValue) ?? .ghostty
|
||||
}
|
||||
}
|
||||
|
||||
enum TerminalTermTypePreference: String, CaseIterable, Codable, Identifiable {
|
||||
case automatic
|
||||
case xterm256color = "xterm-256color"
|
||||
case xtermGhostty = "xterm-ghostty"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .automatic:
|
||||
return "Automatic"
|
||||
case .xterm256color:
|
||||
return "xterm-256color"
|
||||
case .xtermGhostty:
|
||||
return "xterm-ghostty"
|
||||
}
|
||||
}
|
||||
|
||||
var detail: String {
|
||||
switch self {
|
||||
case .automatic:
|
||||
return "Uses a backend-specific default TERM value."
|
||||
case .xterm256color:
|
||||
return "Safest choice for SSH sessions and remote hosts with older terminfo setups."
|
||||
case .xtermGhostty:
|
||||
return "Advertises Ghostty's terminfo entry for maximum local capability."
|
||||
}
|
||||
}
|
||||
|
||||
func resolvedTermValue(for backend: TerminalBackendPreference) -> String {
|
||||
switch self {
|
||||
case .automatic:
|
||||
switch backend {
|
||||
case .ghostty:
|
||||
return Self.xtermGhostty.rawValue
|
||||
case .swiftTerm:
|
||||
return Self.xterm256color.rawValue
|
||||
}
|
||||
case .xterm256color, .xtermGhostty:
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
static func resolve(_ rawValue: String) -> TerminalTermTypePreference {
|
||||
TerminalTermTypePreference(rawValue: rawValue) ?? .automatic
|
||||
}
|
||||
}
|
||||
77
CommandNotch/CommandNotch/Models/AppSettingsController.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
final class AppSettingsController: ObservableObject, TerminalSessionConfigurationProviding {
|
||||
static let shared = AppSettingsController(
|
||||
store: UserDefaultsAppSettingsStore(),
|
||||
observeExternalChanges: true
|
||||
)
|
||||
|
||||
@Published private(set) var settings: AppSettings
|
||||
|
||||
private let store: any AppSettingsStoreType
|
||||
private let notificationCenter: NotificationCenter
|
||||
private var defaultsObserver: NSObjectProtocol?
|
||||
|
||||
init(
|
||||
store: any AppSettingsStoreType,
|
||||
observeExternalChanges: Bool = false,
|
||||
notificationCenter: NotificationCenter = .default
|
||||
) {
|
||||
self.store = store
|
||||
self.notificationCenter = notificationCenter
|
||||
self.settings = store.load()
|
||||
|
||||
if observeExternalChanges {
|
||||
defaultsObserver = notificationCenter.addObserver(
|
||||
forName: UserDefaults.didChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let defaultsObserver {
|
||||
notificationCenter.removeObserver(defaultsObserver)
|
||||
}
|
||||
}
|
||||
|
||||
var terminalSessionConfiguration: TerminalSessionConfiguration {
|
||||
TerminalSessionConfiguration(
|
||||
fontSize: CGFloat(settings.terminal.fontSize),
|
||||
theme: settings.terminal.theme,
|
||||
shellPath: settings.terminal.shellPath,
|
||||
scrollbackLines: settings.terminal.scrollbackLines,
|
||||
backendPreference: settings.terminal.backend,
|
||||
termTypePreference: settings.terminal.termType
|
||||
)
|
||||
}
|
||||
|
||||
var hotkeySettings: AppSettings.HotkeySettings {
|
||||
settings.hotkeys
|
||||
}
|
||||
|
||||
var terminalSizePresets: [TerminalSizePreset] {
|
||||
settings.terminal.sizePresets
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
let loaded = store.load()
|
||||
guard loaded != settings else { return }
|
||||
settings = loaded
|
||||
}
|
||||
|
||||
func update(_ mutate: (inout AppSettings) -> Void) {
|
||||
var updated = settings
|
||||
mutate(&updated)
|
||||
guard updated != settings else { return }
|
||||
settings = updated
|
||||
store.save(updated)
|
||||
}
|
||||
}
|
||||
146
CommandNotch/CommandNotch/Models/AppSettingsStore.swift
Normal file
@@ -0,0 +1,146 @@
|
||||
import Foundation
|
||||
|
||||
protocol AppSettingsStoreType {
|
||||
func load() -> AppSettings
|
||||
func save(_ settings: AppSettings)
|
||||
}
|
||||
|
||||
struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
|
||||
private let defaults: UserDefaults
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
func load() -> AppSettings {
|
||||
AppSettings(
|
||||
display: .init(
|
||||
showOnAllDisplays: bool(NotchSettings.Keys.showOnAllDisplays, default: NotchSettings.Defaults.showOnAllDisplays),
|
||||
showMenuBarIcon: bool(NotchSettings.Keys.showMenuBarIcon, default: NotchSettings.Defaults.showMenuBarIcon),
|
||||
launchAtLogin: bool(NotchSettings.Keys.launchAtLogin, default: NotchSettings.Defaults.launchAtLogin),
|
||||
notchHeightMode: integer(NotchSettings.Keys.notchHeightMode, default: NotchSettings.Defaults.notchHeightMode),
|
||||
notchHeight: double(NotchSettings.Keys.notchHeight, default: NotchSettings.Defaults.notchHeight),
|
||||
nonNotchHeightMode: integer(NotchSettings.Keys.nonNotchHeightMode, default: NotchSettings.Defaults.nonNotchHeightMode),
|
||||
nonNotchHeight: double(NotchSettings.Keys.nonNotchHeight, default: NotchSettings.Defaults.nonNotchHeight),
|
||||
openWidth: double(NotchSettings.Keys.openWidth, default: NotchSettings.Defaults.openWidth),
|
||||
openHeight: double(NotchSettings.Keys.openHeight, default: NotchSettings.Defaults.openHeight)
|
||||
),
|
||||
behavior: .init(
|
||||
openNotchOnHover: bool(NotchSettings.Keys.openNotchOnHover, default: NotchSettings.Defaults.openNotchOnHover),
|
||||
minimumHoverDuration: double(NotchSettings.Keys.minimumHoverDuration, default: NotchSettings.Defaults.minimumHoverDuration),
|
||||
enableGestures: bool(NotchSettings.Keys.enableGestures, default: NotchSettings.Defaults.enableGestures),
|
||||
gestureSensitivity: double(NotchSettings.Keys.gestureSensitivity, default: NotchSettings.Defaults.gestureSensitivity)
|
||||
),
|
||||
appearance: .init(
|
||||
enableShadow: bool(NotchSettings.Keys.enableShadow, default: NotchSettings.Defaults.enableShadow),
|
||||
shadowRadius: double(NotchSettings.Keys.shadowRadius, default: NotchSettings.Defaults.shadowRadius),
|
||||
shadowOpacity: double(NotchSettings.Keys.shadowOpacity, default: NotchSettings.Defaults.shadowOpacity),
|
||||
cornerRadiusScaling: bool(NotchSettings.Keys.cornerRadiusScaling, default: NotchSettings.Defaults.cornerRadiusScaling),
|
||||
notchOpacity: double(NotchSettings.Keys.notchOpacity, default: NotchSettings.Defaults.notchOpacity),
|
||||
blurRadius: double(NotchSettings.Keys.blurRadius, default: NotchSettings.Defaults.blurRadius)
|
||||
),
|
||||
animation: .init(
|
||||
openSpringResponse: double(NotchSettings.Keys.openSpringResponse, default: NotchSettings.Defaults.openSpringResponse),
|
||||
openSpringDamping: double(NotchSettings.Keys.openSpringDamping, default: NotchSettings.Defaults.openSpringDamping),
|
||||
closeSpringResponse: double(NotchSettings.Keys.closeSpringResponse, default: NotchSettings.Defaults.closeSpringResponse),
|
||||
closeSpringDamping: double(NotchSettings.Keys.closeSpringDamping, default: NotchSettings.Defaults.closeSpringDamping),
|
||||
hoverSpringResponse: double(NotchSettings.Keys.hoverSpringResponse, default: NotchSettings.Defaults.hoverSpringResponse),
|
||||
hoverSpringDamping: double(NotchSettings.Keys.hoverSpringDamping, default: NotchSettings.Defaults.hoverSpringDamping),
|
||||
resizeAnimationDuration: double(NotchSettings.Keys.resizeAnimationDuration, default: NotchSettings.Defaults.resizeAnimationDuration)
|
||||
),
|
||||
terminal: .init(
|
||||
fontSize: double(NotchSettings.Keys.terminalFontSize, default: NotchSettings.Defaults.terminalFontSize),
|
||||
shellPath: string(NotchSettings.Keys.terminalShell, default: NotchSettings.Defaults.terminalShell),
|
||||
themeRawValue: string(NotchSettings.Keys.terminalTheme, default: NotchSettings.Defaults.terminalTheme),
|
||||
scrollbackLines: integer(NotchSettings.Keys.terminalScrollbackLines, default: NotchSettings.Defaults.terminalScrollbackLines),
|
||||
backendRawValue: string(NotchSettings.Keys.terminalBackend, default: NotchSettings.Defaults.terminalBackend),
|
||||
termTypeRawValue: string(NotchSettings.Keys.terminalTermType, default: NotchSettings.Defaults.terminalTermType),
|
||||
sizePresetsJSON: string(NotchSettings.Keys.terminalSizePresets, default: NotchSettings.Defaults.terminalSizePresets)
|
||||
),
|
||||
hotkeys: .init(
|
||||
toggle: hotkey(NotchSettings.Keys.hotkeyToggle, default: .cmdReturn),
|
||||
newTab: hotkey(NotchSettings.Keys.hotkeyNewTab, default: .cmdT),
|
||||
closeTab: hotkey(NotchSettings.Keys.hotkeyCloseTab, default: .cmdW),
|
||||
nextTab: hotkey(NotchSettings.Keys.hotkeyNextTab, default: .cmdShiftRB),
|
||||
previousTab: hotkey(NotchSettings.Keys.hotkeyPreviousTab, default: .cmdShiftLB),
|
||||
nextWorkspace: hotkey(NotchSettings.Keys.hotkeyNextWorkspace, default: .cmdShiftDown),
|
||||
previousWorkspace: hotkey(NotchSettings.Keys.hotkeyPreviousWorkspace, default: .cmdShiftUp),
|
||||
detachTab: hotkey(NotchSettings.Keys.hotkeyDetachTab, default: .cmdD)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func save(_ settings: AppSettings) {
|
||||
defaults.set(settings.display.showOnAllDisplays, forKey: NotchSettings.Keys.showOnAllDisplays)
|
||||
defaults.set(settings.display.showMenuBarIcon, forKey: NotchSettings.Keys.showMenuBarIcon)
|
||||
defaults.set(settings.display.launchAtLogin, forKey: NotchSettings.Keys.launchAtLogin)
|
||||
defaults.set(settings.display.notchHeightMode, forKey: NotchSettings.Keys.notchHeightMode)
|
||||
defaults.set(settings.display.notchHeight, forKey: NotchSettings.Keys.notchHeight)
|
||||
defaults.set(settings.display.nonNotchHeightMode, forKey: NotchSettings.Keys.nonNotchHeightMode)
|
||||
defaults.set(settings.display.nonNotchHeight, forKey: NotchSettings.Keys.nonNotchHeight)
|
||||
defaults.set(settings.display.openWidth, forKey: NotchSettings.Keys.openWidth)
|
||||
defaults.set(settings.display.openHeight, forKey: NotchSettings.Keys.openHeight)
|
||||
|
||||
defaults.set(settings.behavior.openNotchOnHover, forKey: NotchSettings.Keys.openNotchOnHover)
|
||||
defaults.set(settings.behavior.minimumHoverDuration, forKey: NotchSettings.Keys.minimumHoverDuration)
|
||||
defaults.set(settings.behavior.enableGestures, forKey: NotchSettings.Keys.enableGestures)
|
||||
defaults.set(settings.behavior.gestureSensitivity, forKey: NotchSettings.Keys.gestureSensitivity)
|
||||
|
||||
defaults.set(settings.appearance.enableShadow, forKey: NotchSettings.Keys.enableShadow)
|
||||
defaults.set(settings.appearance.shadowRadius, forKey: NotchSettings.Keys.shadowRadius)
|
||||
defaults.set(settings.appearance.shadowOpacity, forKey: NotchSettings.Keys.shadowOpacity)
|
||||
defaults.set(settings.appearance.cornerRadiusScaling, forKey: NotchSettings.Keys.cornerRadiusScaling)
|
||||
defaults.set(settings.appearance.notchOpacity, forKey: NotchSettings.Keys.notchOpacity)
|
||||
defaults.set(settings.appearance.blurRadius, forKey: NotchSettings.Keys.blurRadius)
|
||||
|
||||
defaults.set(settings.animation.openSpringResponse, forKey: NotchSettings.Keys.openSpringResponse)
|
||||
defaults.set(settings.animation.openSpringDamping, forKey: NotchSettings.Keys.openSpringDamping)
|
||||
defaults.set(settings.animation.closeSpringResponse, forKey: NotchSettings.Keys.closeSpringResponse)
|
||||
defaults.set(settings.animation.closeSpringDamping, forKey: NotchSettings.Keys.closeSpringDamping)
|
||||
defaults.set(settings.animation.hoverSpringResponse, forKey: NotchSettings.Keys.hoverSpringResponse)
|
||||
defaults.set(settings.animation.hoverSpringDamping, forKey: NotchSettings.Keys.hoverSpringDamping)
|
||||
defaults.set(settings.animation.resizeAnimationDuration, forKey: NotchSettings.Keys.resizeAnimationDuration)
|
||||
|
||||
defaults.set(settings.terminal.fontSize, forKey: NotchSettings.Keys.terminalFontSize)
|
||||
defaults.set(settings.terminal.shellPath, forKey: NotchSettings.Keys.terminalShell)
|
||||
defaults.set(settings.terminal.themeRawValue, forKey: NotchSettings.Keys.terminalTheme)
|
||||
defaults.set(settings.terminal.scrollbackLines, forKey: NotchSettings.Keys.terminalScrollbackLines)
|
||||
defaults.set(settings.terminal.backendRawValue, forKey: NotchSettings.Keys.terminalBackend)
|
||||
defaults.set(settings.terminal.termTypeRawValue, forKey: NotchSettings.Keys.terminalTermType)
|
||||
defaults.set(settings.terminal.sizePresetsJSON, forKey: NotchSettings.Keys.terminalSizePresets)
|
||||
|
||||
defaults.set(settings.hotkeys.toggle.toJSON(), forKey: NotchSettings.Keys.hotkeyToggle)
|
||||
defaults.set(settings.hotkeys.newTab.toJSON(), forKey: NotchSettings.Keys.hotkeyNewTab)
|
||||
defaults.set(settings.hotkeys.closeTab.toJSON(), forKey: NotchSettings.Keys.hotkeyCloseTab)
|
||||
defaults.set(settings.hotkeys.nextTab.toJSON(), forKey: NotchSettings.Keys.hotkeyNextTab)
|
||||
defaults.set(settings.hotkeys.previousTab.toJSON(), forKey: NotchSettings.Keys.hotkeyPreviousTab)
|
||||
defaults.set(settings.hotkeys.nextWorkspace.toJSON(), forKey: NotchSettings.Keys.hotkeyNextWorkspace)
|
||||
defaults.set(settings.hotkeys.previousWorkspace.toJSON(), forKey: NotchSettings.Keys.hotkeyPreviousWorkspace)
|
||||
defaults.set(settings.hotkeys.detachTab.toJSON(), forKey: NotchSettings.Keys.hotkeyDetachTab)
|
||||
}
|
||||
|
||||
private func bool(_ key: String, default defaultValue: Bool) -> Bool {
|
||||
guard defaults.object(forKey: key) != nil else { return defaultValue }
|
||||
return defaults.bool(forKey: key)
|
||||
}
|
||||
|
||||
private func double(_ key: String, default defaultValue: Double) -> Double {
|
||||
guard defaults.object(forKey: key) != nil else { return defaultValue }
|
||||
return defaults.double(forKey: key)
|
||||
}
|
||||
|
||||
private func integer(_ key: String, default defaultValue: Int) -> Int {
|
||||
guard defaults.object(forKey: key) != nil else { return defaultValue }
|
||||
return defaults.integer(forKey: key)
|
||||
}
|
||||
|
||||
private func string(_ key: String, default defaultValue: String) -> String {
|
||||
defaults.string(forKey: key) ?? defaultValue
|
||||
}
|
||||
|
||||
private func hotkey(_ key: String, default defaultValue: HotkeyBinding) -> HotkeyBinding {
|
||||
guard let json = defaults.string(forKey: key),
|
||||
let binding = HotkeyBinding.fromJSON(json) else { return defaultValue }
|
||||
return binding
|
||||
}
|
||||
}
|
||||
1377
CommandNotch/CommandNotch/Models/GhosttyBackendSession.swift
Normal file
@@ -88,6 +88,8 @@ struct HotkeyBinding: Codable, Equatable, Hashable {
|
||||
static let cmdW = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 13)
|
||||
static let cmdShiftRB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30) // ]
|
||||
static let cmdShiftLB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 33) // [
|
||||
static let cmdShiftDown = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 125)
|
||||
static let cmdShiftUp = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 126)
|
||||
static let cmdD = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)
|
||||
|
||||
static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? {
|
||||
189
CommandNotch/CommandNotch/Models/NotchOrchestrator.swift
Normal file
@@ -0,0 +1,189 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
protocol ScreenRegistryType: AnyObject {
|
||||
func allScreens() -> [ScreenContext]
|
||||
func screenContext(for id: ScreenID) -> ScreenContext?
|
||||
func activeScreenID() -> ScreenID?
|
||||
func presentingScreenID(for workspaceID: WorkspaceID) -> ScreenID?
|
||||
@discardableResult
|
||||
func claimWorkspacePresentation(for screenID: ScreenID) -> ScreenID?
|
||||
func releaseWorkspacePresentation(for screenID: ScreenID)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol NotchPresentationHost: AnyObject {
|
||||
func canPresentNotch(for screenID: ScreenID) -> Bool
|
||||
func performOpenPresentation(for screenID: ScreenID)
|
||||
func performClosePresentation(for screenID: ScreenID)
|
||||
}
|
||||
|
||||
protocol SchedulerType {
|
||||
@MainActor
|
||||
func schedule(after interval: TimeInterval, action: @escaping @MainActor () -> Void) -> AnyCancellable
|
||||
}
|
||||
|
||||
struct TaskScheduler: SchedulerType {
|
||||
@MainActor
|
||||
func schedule(after interval: TimeInterval, action: @escaping @MainActor () -> Void) -> AnyCancellable {
|
||||
let task = Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
|
||||
guard !Task.isCancelled else { return }
|
||||
action()
|
||||
}
|
||||
|
||||
return AnyCancellable {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class NotchOrchestrator {
|
||||
private let screenRegistry: any ScreenRegistryType
|
||||
private weak var host: (any NotchPresentationHost)?
|
||||
private let settingsController: AppSettingsController
|
||||
private let scheduler: any SchedulerType
|
||||
|
||||
private var hoverOpenTasks: [ScreenID: AnyCancellable] = [:]
|
||||
private var closeTransitionTasks: [ScreenID: AnyCancellable] = [:]
|
||||
|
||||
init(
|
||||
screenRegistry: any ScreenRegistryType,
|
||||
host: any NotchPresentationHost,
|
||||
settingsController: AppSettingsController? = nil,
|
||||
scheduler: (any SchedulerType)? = nil
|
||||
) {
|
||||
self.screenRegistry = screenRegistry
|
||||
self.host = host
|
||||
self.settingsController = settingsController ?? AppSettingsController.shared
|
||||
self.scheduler = scheduler ?? TaskScheduler()
|
||||
}
|
||||
|
||||
func toggleOnActiveScreen() {
|
||||
guard let screenID = screenRegistry.activeScreenID(),
|
||||
host?.canPresentNotch(for: screenID) == true,
|
||||
let context = screenRegistry.screenContext(for: screenID) else {
|
||||
return
|
||||
}
|
||||
|
||||
if context.notchState == .open {
|
||||
close(screenID: screenID)
|
||||
} else {
|
||||
open(screenID: screenID)
|
||||
}
|
||||
}
|
||||
|
||||
func open(screenID: ScreenID) {
|
||||
guard host?.canPresentNotch(for: screenID) == true,
|
||||
let context = screenRegistry.screenContext(for: screenID) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let presentingScreenID = screenRegistry.presentingScreenID(for: context.workspaceID),
|
||||
presentingScreenID != screenID {
|
||||
close(screenID: presentingScreenID)
|
||||
}
|
||||
|
||||
cancelHoverOpen(for: screenID)
|
||||
cancelCloseTransition(for: screenID)
|
||||
context.cancelCloseTransition()
|
||||
|
||||
withAnimation(context.openAnimation) {
|
||||
context.open()
|
||||
}
|
||||
|
||||
_ = screenRegistry.claimWorkspacePresentation(for: screenID)
|
||||
host?.performOpenPresentation(for: screenID)
|
||||
}
|
||||
|
||||
func close(screenID: ScreenID) {
|
||||
guard let context = screenRegistry.screenContext(for: screenID) else { return }
|
||||
|
||||
cancelHoverOpen(for: screenID)
|
||||
cancelCloseTransition(for: screenID)
|
||||
context.beginCloseTransition()
|
||||
|
||||
closeTransitionTasks[screenID] = scheduler.schedule(after: context.closeInteractionLockDuration) { [weak self] in
|
||||
self?.finishCloseTransition(for: screenID)
|
||||
}
|
||||
|
||||
withAnimation(context.closeAnimation) {
|
||||
context.close()
|
||||
}
|
||||
|
||||
screenRegistry.releaseWorkspacePresentation(for: screenID)
|
||||
host?.performClosePresentation(for: screenID)
|
||||
}
|
||||
|
||||
func handleHoverChange(_ hovering: Bool, for screenID: ScreenID) {
|
||||
guard let context = screenRegistry.screenContext(for: screenID) else { return }
|
||||
|
||||
context.isHovering = hovering
|
||||
|
||||
if hovering {
|
||||
scheduleHoverOpenIfNeeded(for: screenID)
|
||||
} else {
|
||||
cancelHoverOpen(for: screenID)
|
||||
context.clearHoverOpenSuppression()
|
||||
}
|
||||
}
|
||||
|
||||
func cancelAllPendingWork() {
|
||||
for task in hoverOpenTasks.values {
|
||||
task.cancel()
|
||||
}
|
||||
for task in closeTransitionTasks.values {
|
||||
task.cancel()
|
||||
}
|
||||
|
||||
hoverOpenTasks.removeAll()
|
||||
closeTransitionTasks.removeAll()
|
||||
}
|
||||
|
||||
private func scheduleHoverOpenIfNeeded(for screenID: ScreenID) {
|
||||
cancelHoverOpen(for: screenID)
|
||||
|
||||
guard let context = screenRegistry.screenContext(for: screenID) else { return }
|
||||
guard settingsController.settings.behavior.openNotchOnHover,
|
||||
context.notchState == .closed,
|
||||
!context.isCloseTransitionActive,
|
||||
!context.suppressHoverOpenUntilHoverExit,
|
||||
context.isHovering else {
|
||||
return
|
||||
}
|
||||
|
||||
hoverOpenTasks[screenID] = scheduler.schedule(after: settingsController.settings.behavior.minimumHoverDuration) { [weak self] in
|
||||
guard let self,
|
||||
let context = self.screenRegistry.screenContext(for: screenID),
|
||||
context.isHovering,
|
||||
context.notchState == .closed,
|
||||
!context.isCloseTransitionActive,
|
||||
!context.suppressHoverOpenUntilHoverExit else {
|
||||
return
|
||||
}
|
||||
|
||||
self.hoverOpenTasks[screenID] = nil
|
||||
self.open(screenID: screenID)
|
||||
}
|
||||
}
|
||||
|
||||
private func finishCloseTransition(for screenID: ScreenID) {
|
||||
closeTransitionTasks[screenID] = nil
|
||||
guard let context = screenRegistry.screenContext(for: screenID) else { return }
|
||||
|
||||
context.endCloseTransition()
|
||||
scheduleHoverOpenIfNeeded(for: screenID)
|
||||
}
|
||||
|
||||
private func cancelHoverOpen(for screenID: ScreenID) {
|
||||
hoverOpenTasks[screenID]?.cancel()
|
||||
hoverOpenTasks[screenID] = nil
|
||||
}
|
||||
|
||||
private func cancelCloseTransition(for screenID: ScreenID) {
|
||||
closeTransitionTasks[screenID]?.cancel()
|
||||
closeTransitionTasks[screenID] = nil
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,12 @@ enum NotchSettings {
|
||||
static let terminalFontSize = "terminalFontSize"
|
||||
static let terminalShell = "terminalShell"
|
||||
static let terminalTheme = "terminalTheme"
|
||||
static let terminalScrollbackLines = "terminalScrollbackLines"
|
||||
static let terminalBackend = "terminalBackend"
|
||||
static let terminalTermType = "terminalTermType"
|
||||
static let terminalSizePresets = "terminalSizePresets"
|
||||
static let workspaceSummaries = "workspaceSummaries"
|
||||
static let screenAssignments = "screenAssignments"
|
||||
|
||||
// Hotkeys — each stores a HotkeyBinding JSON string
|
||||
static let hotkeyToggle = "hotkey_toggle"
|
||||
@@ -55,6 +60,8 @@ enum NotchSettings {
|
||||
static let hotkeyCloseTab = "hotkey_closeTab"
|
||||
static let hotkeyNextTab = "hotkey_nextTab"
|
||||
static let hotkeyPreviousTab = "hotkey_previousTab"
|
||||
static let hotkeyNextWorkspace = "hotkey_nextWorkspace"
|
||||
static let hotkeyPreviousWorkspace = "hotkey_previousWorkspace"
|
||||
static let hotkeyDetachTab = "hotkey_detachTab"
|
||||
}
|
||||
|
||||
@@ -94,6 +101,9 @@ enum NotchSettings {
|
||||
static let terminalFontSize: Double = 13
|
||||
static let terminalShell: String = ""
|
||||
static let terminalTheme: String = TerminalTheme.terminalApp.rawValue
|
||||
static let terminalScrollbackLines: Int = 500
|
||||
static let terminalBackend: String = TerminalBackendPreference.ghostty.rawValue
|
||||
static let terminalTermType: String = TerminalTermTypePreference.automatic.rawValue
|
||||
static let terminalSizePresets: String = TerminalSizePresetStore.defaultPresetsJSON()
|
||||
|
||||
// Default hotkey bindings as JSON
|
||||
@@ -102,6 +112,8 @@ enum NotchSettings {
|
||||
static let hotkeyCloseTab: String = HotkeyBinding.cmdW.toJSON()
|
||||
static let hotkeyNextTab: String = HotkeyBinding.cmdShiftRB.toJSON()
|
||||
static let hotkeyPreviousTab: String = HotkeyBinding.cmdShiftLB.toJSON()
|
||||
static let hotkeyNextWorkspace: String = HotkeyBinding.cmdShiftDown.toJSON()
|
||||
static let hotkeyPreviousWorkspace: String = HotkeyBinding.cmdShiftUp.toJSON()
|
||||
static let hotkeyDetachTab: String = HotkeyBinding.cmdD.toJSON()
|
||||
}
|
||||
|
||||
@@ -142,6 +154,9 @@ enum NotchSettings {
|
||||
Keys.terminalFontSize: Defaults.terminalFontSize,
|
||||
Keys.terminalShell: Defaults.terminalShell,
|
||||
Keys.terminalTheme: Defaults.terminalTheme,
|
||||
Keys.terminalScrollbackLines: Defaults.terminalScrollbackLines,
|
||||
Keys.terminalBackend: Defaults.terminalBackend,
|
||||
Keys.terminalTermType: Defaults.terminalTermType,
|
||||
Keys.terminalSizePresets: Defaults.terminalSizePresets,
|
||||
|
||||
Keys.hotkeyToggle: Defaults.hotkeyToggle,
|
||||
@@ -149,6 +164,8 @@ enum NotchSettings {
|
||||
Keys.hotkeyCloseTab: Defaults.hotkeyCloseTab,
|
||||
Keys.hotkeyNextTab: Defaults.hotkeyNextTab,
|
||||
Keys.hotkeyPreviousTab: Defaults.hotkeyPreviousTab,
|
||||
Keys.hotkeyNextWorkspace: Defaults.hotkeyNextWorkspace,
|
||||
Keys.hotkeyPreviousWorkspace: Defaults.hotkeyPreviousWorkspace,
|
||||
Keys.hotkeyDetachTab: Defaults.hotkeyDetachTab,
|
||||
])
|
||||
}
|
||||
@@ -212,17 +229,14 @@ enum TerminalSizePresetStore {
|
||||
static func load() -> [TerminalSizePreset] {
|
||||
let defaults = UserDefaults.standard
|
||||
guard let json = defaults.string(forKey: NotchSettings.Keys.terminalSizePresets),
|
||||
let data = json.data(using: .utf8),
|
||||
let presets = try? JSONDecoder().decode([TerminalSizePreset].self, from: data) else {
|
||||
let presets = decodePresets(from: json) else {
|
||||
return defaultPresets()
|
||||
}
|
||||
return presets
|
||||
}
|
||||
|
||||
static func save(_ presets: [TerminalSizePreset]) {
|
||||
guard let data = try? JSONEncoder().encode(presets),
|
||||
let json = String(data: data, encoding: .utf8) else { return }
|
||||
UserDefaults.standard.set(json, forKey: NotchSettings.Keys.terminalSizePresets)
|
||||
UserDefaults.standard.set(encodePresets(presets), forKey: NotchSettings.Keys.terminalSizePresets)
|
||||
}
|
||||
|
||||
static func reset() {
|
||||
@@ -234,11 +248,7 @@ enum TerminalSizePresetStore {
|
||||
}
|
||||
|
||||
static func defaultPresetsJSON() -> String {
|
||||
guard let data = try? JSONEncoder().encode(defaultPresets()),
|
||||
let json = String(data: data, encoding: .utf8) else {
|
||||
return "[]"
|
||||
}
|
||||
return json
|
||||
encodePresets(defaultPresets())
|
||||
}
|
||||
|
||||
static func suggestedHotkey(for presets: [TerminalSizePreset]) -> HotkeyBinding? {
|
||||
@@ -259,4 +269,17 @@ enum TerminalSizePresetStore {
|
||||
TerminalSizePreset(name: "Large", width: 900, height: 500, hotkey: HotkeyBinding.cmdShiftDigit(3)),
|
||||
]
|
||||
}
|
||||
|
||||
static func decodePresets(from json: String) -> [TerminalSizePreset]? {
|
||||
guard let data = json.data(using: .utf8) else { return nil }
|
||||
return try? JSONDecoder().decode([TerminalSizePreset].self, from: data)
|
||||
}
|
||||
|
||||
static func encodePresets(_ presets: [TerminalSizePreset]) -> String {
|
||||
guard let data = try? JSONEncoder().encode(presets),
|
||||
let json = String(data: data, encoding: .utf8) else {
|
||||
return "[]"
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
222
CommandNotch/CommandNotch/Models/ScreenContext.swift
Normal file
@@ -0,0 +1,222 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
typealias ScreenID = String
|
||||
|
||||
/// Observable screen-local UI state for one physical display.
|
||||
@MainActor
|
||||
final class ScreenContext: ObservableObject, Identifiable {
|
||||
private static let minimumOpenWidth: CGFloat = 320
|
||||
private static let minimumOpenHeight: CGFloat = 140
|
||||
private static let windowHorizontalPadding: CGFloat = 40
|
||||
private static let windowVerticalPadding: CGFloat = 20
|
||||
|
||||
let id: ScreenID
|
||||
|
||||
@Published var workspaceID: WorkspaceID
|
||||
@Published var notchState: NotchState = .closed
|
||||
@Published var notchSize: CGSize
|
||||
@Published var closedNotchSize: CGSize
|
||||
@Published var isHovering = false
|
||||
@Published var isCloseTransitionActive = false
|
||||
@Published var suppressHoverOpenUntilHoverExit = false
|
||||
@Published var isUserResizing = false
|
||||
@Published var isPresetResizing = false
|
||||
@Published private(set) var suppressCloseOnFocusLoss = false
|
||||
|
||||
var requestOpen: (() -> Void)?
|
||||
var requestClose: (() -> Void)?
|
||||
var requestWindowResize: (() -> Void)?
|
||||
var requestTerminalFocus: (() -> Void)?
|
||||
|
||||
private let settingsController: AppSettingsController
|
||||
private let screenProvider: @MainActor (ScreenID) -> NSScreen?
|
||||
|
||||
init(
|
||||
id: ScreenID,
|
||||
workspaceID: WorkspaceID,
|
||||
settingsController: AppSettingsController? = nil,
|
||||
screenProvider: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
|
||||
NSScreen.screens.first { $0.displayUUID == screenID }
|
||||
}
|
||||
) {
|
||||
self.id = id
|
||||
self.workspaceID = workspaceID
|
||||
self.settingsController = settingsController ?? AppSettingsController.shared
|
||||
self.screenProvider = screenProvider
|
||||
|
||||
let closed = Self.resolveClosedNotchSize(
|
||||
for: id,
|
||||
using: self.settingsController.settings.display,
|
||||
screenProvider: screenProvider
|
||||
)
|
||||
self.closedNotchSize = closed
|
||||
self.notchSize = closed
|
||||
}
|
||||
|
||||
var openAnimation: Animation {
|
||||
let animation = settingsController.settings.animation
|
||||
return .spring(
|
||||
response: animation.openSpringResponse,
|
||||
dampingFraction: animation.openSpringDamping
|
||||
)
|
||||
}
|
||||
|
||||
var closeAnimation: Animation {
|
||||
let animation = settingsController.settings.animation
|
||||
return .spring(
|
||||
response: animation.closeSpringResponse,
|
||||
dampingFraction: animation.closeSpringDamping
|
||||
)
|
||||
}
|
||||
|
||||
var openAnimationDuration: TimeInterval {
|
||||
max(0.05, settingsController.settings.animation.resizeAnimationDuration)
|
||||
}
|
||||
|
||||
func open() {
|
||||
notchSize = openNotchSize
|
||||
notchState = .open
|
||||
}
|
||||
|
||||
func close() {
|
||||
refreshClosedSize()
|
||||
notchSize = closedNotchSize
|
||||
notchState = .closed
|
||||
}
|
||||
|
||||
func updateWorkspace(id: WorkspaceID) {
|
||||
guard workspaceID != id else { return }
|
||||
workspaceID = id
|
||||
}
|
||||
|
||||
func refreshClosedSize() {
|
||||
closedNotchSize = Self.resolveClosedNotchSize(
|
||||
for: id,
|
||||
using: settingsController.settings.display,
|
||||
screenProvider: screenProvider
|
||||
)
|
||||
}
|
||||
|
||||
var openNotchSize: CGSize {
|
||||
let display = settingsController.settings.display
|
||||
return clampedOpenSize(
|
||||
CGSize(width: display.openWidth, height: display.openHeight)
|
||||
)
|
||||
}
|
||||
|
||||
func beginInteractiveResize() {
|
||||
isUserResizing = true
|
||||
}
|
||||
|
||||
func resizeOpenNotch(to proposedSize: CGSize) {
|
||||
let clampedSize = clampedOpenSize(proposedSize)
|
||||
if notchState == .open {
|
||||
notchSize = clampedSize
|
||||
}
|
||||
requestWindowResize?()
|
||||
}
|
||||
|
||||
func endInteractiveResize() {
|
||||
if notchState == .open {
|
||||
settingsController.update {
|
||||
$0.display.openWidth = notchSize.width
|
||||
$0.display.openHeight = notchSize.height
|
||||
}
|
||||
}
|
||||
isUserResizing = false
|
||||
}
|
||||
|
||||
func applySizePreset(_ preset: TerminalSizePreset, notifyWindowResize: Bool = true) {
|
||||
setOpenSize(preset.size, notifyWindowResize: notifyWindowResize)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setStoredOpenSize(_ proposedSize: CGSize) -> CGSize {
|
||||
let clampedSize = clampedOpenSize(proposedSize)
|
||||
settingsController.update {
|
||||
$0.display.openWidth = clampedSize.width
|
||||
$0.display.openHeight = clampedSize.height
|
||||
}
|
||||
return clampedSize
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize {
|
||||
let clampedSize = setStoredOpenSize(proposedSize)
|
||||
if notchState == .open {
|
||||
notchSize = clampedSize
|
||||
}
|
||||
if notifyWindowResize {
|
||||
requestWindowResize?()
|
||||
}
|
||||
return clampedSize
|
||||
}
|
||||
|
||||
private func clampedOpenSize(_ size: CGSize) -> CGSize {
|
||||
CGSize(
|
||||
width: size.width.clamped(to: Self.minimumOpenWidth...maximumAllowedWidth),
|
||||
height: size.height.clamped(to: Self.minimumOpenHeight...maximumAllowedHeight)
|
||||
)
|
||||
}
|
||||
|
||||
private var maximumAllowedWidth: CGFloat {
|
||||
guard let screen = resolvedScreen() ?? NSScreen.main else {
|
||||
return Self.minimumOpenWidth
|
||||
}
|
||||
return max(Self.minimumOpenWidth, screen.frame.width - Self.windowHorizontalPadding)
|
||||
}
|
||||
|
||||
private var maximumAllowedHeight: CGFloat {
|
||||
guard let screen = resolvedScreen() ?? NSScreen.main else {
|
||||
return Self.minimumOpenHeight
|
||||
}
|
||||
return max(Self.minimumOpenHeight, screen.frame.height - Self.windowVerticalPadding)
|
||||
}
|
||||
|
||||
var closeInteractionLockDuration: TimeInterval {
|
||||
max(settingsController.settings.animation.closeSpringResponse + 0.2, 0.35)
|
||||
}
|
||||
|
||||
func beginCloseTransition() {
|
||||
isCloseTransitionActive = true
|
||||
if isHovering {
|
||||
suppressHoverOpenUntilHoverExit = true
|
||||
}
|
||||
}
|
||||
|
||||
func cancelCloseTransition() {
|
||||
isCloseTransitionActive = false
|
||||
}
|
||||
|
||||
func endCloseTransition() {
|
||||
isCloseTransitionActive = false
|
||||
}
|
||||
|
||||
func clearHoverOpenSuppression() {
|
||||
suppressHoverOpenUntilHoverExit = false
|
||||
}
|
||||
|
||||
func setCloseOnFocusLossSuppressed(_ suppressed: Bool) {
|
||||
suppressCloseOnFocusLoss = suppressed
|
||||
}
|
||||
|
||||
private func resolvedScreen() -> NSScreen? {
|
||||
screenProvider(id)
|
||||
}
|
||||
|
||||
private static func resolveClosedNotchSize(
|
||||
for screenID: ScreenID,
|
||||
using settings: AppSettings.DisplaySettings,
|
||||
screenProvider: @escaping @MainActor (ScreenID) -> NSScreen?
|
||||
) -> CGSize {
|
||||
let screen = screenProvider(screenID) ?? NSScreen.main
|
||||
return screen?.closedNotchSize(using: settings) ?? CGSize(width: 220, height: 32)
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGFloat {
|
||||
func clamped(to range: ClosedRange<CGFloat>) -> CGFloat {
|
||||
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||||
}
|
||||
}
|
||||
268
CommandNotch/CommandNotch/Models/ScreenRegistry.swift
Normal file
@@ -0,0 +1,268 @@
|
||||
import AppKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct ConnectedScreenSummary: Identifiable, Equatable {
|
||||
let id: ScreenID
|
||||
let displayName: String
|
||||
let isActive: Bool
|
||||
let assignedWorkspaceID: WorkspaceID
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ScreenRegistry: ObservableObject {
|
||||
static let shared = ScreenRegistry(assignmentStore: UserDefaultsScreenAssignmentStore())
|
||||
|
||||
@Published private(set) var screenContexts: [ScreenContext] = []
|
||||
|
||||
private let workspaceRegistry: WorkspaceRegistry
|
||||
private let settingsController: AppSettingsController
|
||||
private let assignmentStore: any ScreenAssignmentStoreType
|
||||
private let connectedScreenIDsProvider: @MainActor () -> [ScreenID]
|
||||
private let activeScreenIDProvider: @MainActor () -> ScreenID?
|
||||
private let screenLookup: @MainActor (ScreenID) -> NSScreen?
|
||||
|
||||
private var contextsByID: [ScreenID: ScreenContext] = [:]
|
||||
private var preferredAssignments: [ScreenID: WorkspaceID]
|
||||
private var workspacePresenters: [WorkspaceID: ScreenID] = [:]
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(
|
||||
workspaceRegistry: WorkspaceRegistry? = nil,
|
||||
settingsController: AppSettingsController? = nil,
|
||||
assignmentStore: (any ScreenAssignmentStoreType)? = nil,
|
||||
initialAssignments: [ScreenID: WorkspaceID]? = nil,
|
||||
connectedScreenIDsProvider: @escaping @MainActor () -> [ScreenID] = {
|
||||
NSScreen.screens.map(\.displayUUID)
|
||||
},
|
||||
activeScreenIDProvider: @escaping @MainActor () -> ScreenID? = {
|
||||
let mouseLocation = NSEvent.mouseLocation
|
||||
return NSScreen.screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }?.displayUUID
|
||||
?? NSScreen.main?.displayUUID
|
||||
},
|
||||
screenLookup: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
|
||||
NSScreen.screens.first { $0.displayUUID == screenID }
|
||||
}
|
||||
) {
|
||||
let resolvedWorkspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
|
||||
let resolvedSettingsController = settingsController ?? AppSettingsController.shared
|
||||
let resolvedAssignmentStore = assignmentStore ?? UserDefaultsScreenAssignmentStore()
|
||||
|
||||
self.workspaceRegistry = resolvedWorkspaceRegistry
|
||||
self.settingsController = resolvedSettingsController
|
||||
self.assignmentStore = resolvedAssignmentStore
|
||||
self.preferredAssignments = initialAssignments ?? resolvedAssignmentStore.loadScreenAssignments()
|
||||
self.connectedScreenIDsProvider = connectedScreenIDsProvider
|
||||
self.activeScreenIDProvider = activeScreenIDProvider
|
||||
self.screenLookup = screenLookup
|
||||
|
||||
observeWorkspaceChanges()
|
||||
refreshConnectedScreens()
|
||||
}
|
||||
|
||||
func allScreens() -> [ScreenContext] {
|
||||
screenContexts
|
||||
}
|
||||
|
||||
func screenContext(for id: ScreenID) -> ScreenContext? {
|
||||
contextsByID[id]
|
||||
}
|
||||
|
||||
func workspaceController(for screenID: ScreenID) -> WorkspaceController {
|
||||
let workspaceID = contextsByID[screenID]?.workspaceID ?? workspaceRegistry.defaultWorkspaceID
|
||||
return workspaceRegistry.controller(for: workspaceID) ?? workspaceRegistry.defaultWorkspaceController
|
||||
}
|
||||
|
||||
func assignedScreenIDs(to workspaceID: WorkspaceID) -> [ScreenID] {
|
||||
preferredAssignments
|
||||
.filter { $0.value == workspaceID }
|
||||
.map(\.key)
|
||||
.sorted()
|
||||
}
|
||||
|
||||
func assignedScreenCount(to workspaceID: WorkspaceID) -> Int {
|
||||
assignedScreenIDs(to: workspaceID).count
|
||||
}
|
||||
|
||||
func connectedScreenSummaries() -> [ConnectedScreenSummary] {
|
||||
let activeScreenID = activeScreenID()
|
||||
|
||||
return screenContexts.enumerated().map { index, context in
|
||||
ConnectedScreenSummary(
|
||||
id: context.id,
|
||||
displayName: resolvedDisplayName(for: context.id, fallbackIndex: index),
|
||||
isActive: context.id == activeScreenID,
|
||||
assignedWorkspaceID: context.workspaceID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func assignWorkspace(_ workspaceID: WorkspaceID, to screenID: ScreenID) {
|
||||
guard workspaceRegistry.controller(for: workspaceID) != nil else { return }
|
||||
|
||||
let previousWorkspaceID = contextsByID[screenID]?.workspaceID ?? preferredAssignments[screenID]
|
||||
preferredAssignments[screenID] = workspaceID
|
||||
contextsByID[screenID]?.updateWorkspace(id: workspaceID)
|
||||
|
||||
if let previousWorkspaceID,
|
||||
previousWorkspaceID != workspaceID,
|
||||
workspacePresenters[previousWorkspaceID] == screenID {
|
||||
workspacePresenters.removeValue(forKey: previousWorkspaceID)
|
||||
}
|
||||
|
||||
persistAssignments()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func assignActiveScreen(to workspaceID: WorkspaceID) -> ScreenID? {
|
||||
guard let screenID = activeScreenID() else { return nil }
|
||||
assignWorkspace(workspaceID, to: screenID)
|
||||
return screenID
|
||||
}
|
||||
|
||||
func presentingScreenID(for workspaceID: WorkspaceID) -> ScreenID? {
|
||||
guard let screenID = workspacePresenters[workspaceID] else { return nil }
|
||||
guard preferredAssignments[screenID] == workspaceID else {
|
||||
workspacePresenters.removeValue(forKey: workspaceID)
|
||||
return nil
|
||||
}
|
||||
return screenID
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func claimWorkspacePresentation(for screenID: ScreenID) -> ScreenID? {
|
||||
guard let workspaceID = contextsByID[screenID]?.workspaceID ?? preferredAssignments[screenID] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let previousPresenter = workspacePresenters[workspaceID]
|
||||
workspacePresenters[workspaceID] = screenID
|
||||
return previousPresenter == screenID ? nil : previousPresenter
|
||||
}
|
||||
|
||||
func releaseWorkspacePresentation(for screenID: ScreenID) {
|
||||
workspacePresenters = workspacePresenters.filter { $0.value != screenID }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func deleteWorkspace(
|
||||
_ workspaceID: WorkspaceID,
|
||||
preferredFallback preferredFallbackID: WorkspaceID? = nil
|
||||
) -> WorkspaceID? {
|
||||
guard workspaceRegistry.canDeleteWorkspace(id: workspaceID) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let fallbackWorkspaceID = workspaceRegistry.deletionFallbackWorkspaceID(
|
||||
forDeleting: workspaceID,
|
||||
preferredFallback: preferredFallbackID
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
workspacePresenters.removeValue(forKey: workspaceID)
|
||||
|
||||
for (screenID, assignedWorkspaceID) in preferredAssignments where assignedWorkspaceID == workspaceID {
|
||||
preferredAssignments[screenID] = fallbackWorkspaceID
|
||||
}
|
||||
|
||||
for context in contextsByID.values where context.workspaceID == workspaceID {
|
||||
context.updateWorkspace(id: fallbackWorkspaceID)
|
||||
}
|
||||
|
||||
guard workspaceRegistry.deleteWorkspace(id: workspaceID) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
persistAssignments()
|
||||
return fallbackWorkspaceID
|
||||
}
|
||||
|
||||
func activeScreenID() -> ScreenID? {
|
||||
activeScreenIDProvider() ?? screenContexts.first?.id
|
||||
}
|
||||
|
||||
func refreshConnectedScreens() {
|
||||
let connectedScreenIDs = connectedScreenIDsProvider()
|
||||
let validWorkspaceIDs = Set(workspaceRegistry.allWorkspaceSummaries().map(\.id))
|
||||
let defaultWorkspaceID = workspaceRegistry.defaultWorkspaceID
|
||||
var nextContextsByID: [ScreenID: ScreenContext] = [:]
|
||||
var nextContexts: [ScreenContext] = []
|
||||
|
||||
for screenID in connectedScreenIDs {
|
||||
let workspaceID = resolvedWorkspaceID(
|
||||
for: screenID,
|
||||
validWorkspaceIDs: validWorkspaceIDs,
|
||||
defaultWorkspaceID: defaultWorkspaceID
|
||||
)
|
||||
|
||||
let context = contextsByID[screenID] ?? ScreenContext(
|
||||
id: screenID,
|
||||
workspaceID: workspaceID,
|
||||
settingsController: settingsController,
|
||||
screenProvider: screenLookup
|
||||
)
|
||||
|
||||
context.updateWorkspace(id: workspaceID)
|
||||
context.refreshClosedSize()
|
||||
|
||||
nextContextsByID[screenID] = context
|
||||
nextContexts.append(context)
|
||||
}
|
||||
|
||||
contextsByID = nextContextsByID
|
||||
screenContexts = nextContexts
|
||||
reconcileWorkspacePresenters()
|
||||
persistAssignments()
|
||||
}
|
||||
|
||||
private func resolvedWorkspaceID(
|
||||
for screenID: ScreenID,
|
||||
validWorkspaceIDs: Set<WorkspaceID>,
|
||||
defaultWorkspaceID: WorkspaceID
|
||||
) -> WorkspaceID {
|
||||
guard let preferredWorkspaceID = preferredAssignments[screenID],
|
||||
validWorkspaceIDs.contains(preferredWorkspaceID) else {
|
||||
preferredAssignments[screenID] = defaultWorkspaceID
|
||||
return defaultWorkspaceID
|
||||
}
|
||||
|
||||
return preferredWorkspaceID
|
||||
}
|
||||
|
||||
private func observeWorkspaceChanges() {
|
||||
workspaceRegistry.$workspaceSummaries
|
||||
.dropFirst()
|
||||
.sink { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.refreshConnectedScreens()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func persistAssignments() {
|
||||
assignmentStore.saveScreenAssignments(preferredAssignments)
|
||||
}
|
||||
|
||||
private func reconcileWorkspacePresenters() {
|
||||
let validScreenIDs = Set(contextsByID.keys)
|
||||
let validAssignments = preferredAssignments
|
||||
|
||||
workspacePresenters = workspacePresenters.filter { workspaceID, screenID in
|
||||
validScreenIDs.contains(screenID) && validAssignments[screenID] == workspaceID
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedDisplayName(for screenID: ScreenID, fallbackIndex: Int) -> String {
|
||||
let fallbackName = "Screen \(fallbackIndex + 1)"
|
||||
guard let screen = screenLookup(screenID) else {
|
||||
return fallbackName
|
||||
}
|
||||
|
||||
let localizedName = screen.localizedName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return localizedName.isEmpty ? fallbackName : localizedName
|
||||
}
|
||||
}
|
||||
|
||||
extension ScreenRegistry: ScreenRegistryType {}
|
||||
337
CommandNotch/CommandNotch/Models/SwiftTermBackendSession.swift
Normal file
@@ -0,0 +1,337 @@
|
||||
import AppKit
|
||||
import Darwin
|
||||
import SwiftTerm
|
||||
|
||||
/// SwiftTerm-backed terminal backend used by the app-owned `TerminalSession`.
|
||||
@MainActor
|
||||
final class SwiftTermBackendSession: NSObject, TerminalBackendSession, LocalProcessDelegate, @preconcurrency TerminalViewDelegate {
|
||||
let terminalView: TerminalView
|
||||
|
||||
var onTitleChange: ((String) -> Void)?
|
||||
var onCurrentDirectoryChange: ((String?) -> Void)?
|
||||
var onExit: (() -> Void)?
|
||||
|
||||
private var process: LocalProcess?
|
||||
private var keyEventMonitor: Any?
|
||||
private var scrollEventMonitor: Any?
|
||||
private var backgroundColor = NSColor.black
|
||||
private let configuredShellPath: String
|
||||
private let termTypePreference: TerminalTermTypePreference
|
||||
private let launchDirectory: String
|
||||
private let scrollCoordinator = TerminalScrollCoordinator()
|
||||
|
||||
init(
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
termTypePreference: TerminalTermTypePreference,
|
||||
initialDirectory: String
|
||||
) {
|
||||
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
|
||||
configuredShellPath = shellPath
|
||||
self.termTypePreference = termTypePreference
|
||||
launchDirectory = initialDirectory
|
||||
super.init()
|
||||
|
||||
terminalView.terminalDelegate = self
|
||||
installOsc52ClipboardHandler()
|
||||
updateFontSize(fontSize)
|
||||
updateTheme(theme)
|
||||
updateScrollbackLines(scrollbackLines)
|
||||
installCommandArrowMonitor()
|
||||
installScrollWheelMonitor()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let keyEventMonitor {
|
||||
NSEvent.removeMonitor(keyEventMonitor)
|
||||
}
|
||||
if let scrollEventMonitor {
|
||||
NSEvent.removeMonitor(scrollEventMonitor)
|
||||
}
|
||||
}
|
||||
|
||||
var view: NSView {
|
||||
terminalView
|
||||
}
|
||||
|
||||
func start() {
|
||||
guard process == nil else { return }
|
||||
|
||||
let shellPath = resolveShell()
|
||||
let shellName = (shellPath as NSString).lastPathComponent
|
||||
let loginExecName = "-\(shellName)"
|
||||
|
||||
let proc = LocalProcess(delegate: self)
|
||||
// Launch as a login shell so user startup files initialize PATH/tools.
|
||||
proc.startProcess(
|
||||
executable: shellPath,
|
||||
args: ["-l"],
|
||||
environment: terminalEnvironment(),
|
||||
execName: loginExecName,
|
||||
currentDirectory: launchDirectory
|
||||
)
|
||||
process = proc
|
||||
onTitleChange?(shellName)
|
||||
onCurrentDirectoryChange?(launchDirectory)
|
||||
}
|
||||
|
||||
func terminate() {
|
||||
process?.terminate()
|
||||
process = nil
|
||||
}
|
||||
|
||||
func focus() {
|
||||
guard let window = terminalView.window else { return }
|
||||
window.makeFirstResponder(terminalView)
|
||||
}
|
||||
|
||||
func updateFontSize(_ size: CGFloat) {
|
||||
terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: TerminalTheme) {
|
||||
backgroundColor = theme.backgroundColor
|
||||
terminalView.nativeBackgroundColor = backgroundColor
|
||||
terminalView.nativeForegroundColor = theme.foregroundColor
|
||||
terminalView.installColors(theme.ansiColors)
|
||||
}
|
||||
|
||||
func updateScrollbackLines(_ scrollbackLines: Int) {
|
||||
let sanitizedScrollbackLines = max(0, scrollbackLines)
|
||||
terminalView.getTerminal().changeHistorySize(sanitizedScrollbackLines)
|
||||
}
|
||||
|
||||
private func resolveShell() -> String {
|
||||
let custom = configuredShellPath.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !custom.isEmpty && FileManager.default.isExecutableFile(atPath: custom) {
|
||||
return custom
|
||||
}
|
||||
|
||||
let environmentShell = ProcessInfo.processInfo.environment["SHELL"]?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if FileManager.default.isExecutableFile(atPath: environmentShell) {
|
||||
return environmentShell
|
||||
}
|
||||
|
||||
return "/bin/zsh"
|
||||
}
|
||||
|
||||
private func terminalEnvironment() -> [String] {
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
environment["TERM"] = termTypePreference.resolvedTermValue(for: .swiftTerm)
|
||||
environment["COLORTERM"] = "truecolor"
|
||||
|
||||
return environment
|
||||
.sorted { $0.key < $1.key }
|
||||
.map { "\($0.key)=\($0.value)" }
|
||||
}
|
||||
|
||||
private func installOsc52ClipboardHandler() {
|
||||
let maxPayloadSize = 1_048_576 // 1 MB
|
||||
terminalView.getTerminal().registerOscHandler(code: 52) { data in
|
||||
guard data.count >= 2,
|
||||
data[data.startIndex] == UInt8(ascii: "c"),
|
||||
data[data.startIndex + 1] == UInt8(ascii: ";") else { return }
|
||||
|
||||
let base64 = Data(data[(data.startIndex + 2)...])
|
||||
guard let content = Data(base64Encoded: base64),
|
||||
content.count <= maxPayloadSize,
|
||||
let string = String(data: content, encoding: .utf8) else { return }
|
||||
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(string, forType: .string)
|
||||
}
|
||||
}
|
||||
|
||||
private func installCommandArrowMonitor() {
|
||||
keyEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
||||
guard let self else { return event }
|
||||
guard let window = self.terminalView.window else { return event }
|
||||
guard event.window === window else { return event }
|
||||
guard window.firstResponder === self.terminalView else { return event }
|
||||
|
||||
guard let sequence = TerminalCommandArrowBehavior.sequence(
|
||||
for: event.modifierFlags,
|
||||
keyCode: event.keyCode,
|
||||
applicationCursor: self.terminalView.getTerminal().applicationCursor
|
||||
) else {
|
||||
return event
|
||||
}
|
||||
|
||||
self.terminalView.send(data: sequence[...])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func installScrollWheelMonitor() {
|
||||
scrollEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { [weak self] event in
|
||||
guard let self else { return event }
|
||||
guard let window = self.terminalView.window else { return event }
|
||||
guard event.window === window else { return event }
|
||||
guard window.firstResponder === self.terminalView else { return event }
|
||||
|
||||
let terminal = self.terminalView.getTerminal()
|
||||
guard TerminalScrollWheelRouter.shouldSendMouseWheel(
|
||||
allowMouseReporting: self.terminalView.allowMouseReporting,
|
||||
mouseMode: terminal.mouseMode,
|
||||
deltaY: event.deltaY
|
||||
) else {
|
||||
return event
|
||||
}
|
||||
|
||||
let localPoint = self.terminalView.convert(event.locationInWindow, from: nil)
|
||||
let dims = terminal.getDims()
|
||||
let hit = TerminalScrollWheelRouter.gridPosition(
|
||||
point: localPoint,
|
||||
bounds: self.terminalView.bounds,
|
||||
cols: dims.cols,
|
||||
rows: dims.rows
|
||||
)
|
||||
let button = event.deltaY > 0 ? 4 : 5
|
||||
let flags = terminal.encodeButton(
|
||||
button: button,
|
||||
release: false,
|
||||
shift: event.modifierFlags.contains(.shift),
|
||||
meta: event.modifierFlags.contains(.option),
|
||||
control: event.modifierFlags.contains(.control)
|
||||
)
|
||||
|
||||
for _ in 0..<TerminalScrollWheelRouter.velocity(for: event.deltaY) {
|
||||
terminal.sendEvent(
|
||||
buttonFlags: flags,
|
||||
x: hit.x,
|
||||
y: hit.y,
|
||||
pixelX: hit.pixelX,
|
||||
pixelY: hit.pixelY
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LocalProcessDelegate
|
||||
|
||||
nonisolated func processTerminated(_ source: LocalProcess, exitCode: Int32?) {
|
||||
_ = source
|
||||
_ = exitCode
|
||||
|
||||
Task { @MainActor in
|
||||
self.process = nil
|
||||
self.resetTerminalModes()
|
||||
self.onExit?()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetTerminalModes() {
|
||||
let resetSequences: [[UInt8]] = [
|
||||
Array("\u{1b}[?9l".utf8),
|
||||
Array("\u{1b}[?1000l".utf8),
|
||||
Array("\u{1b}[?1002l".utf8),
|
||||
Array("\u{1b}[?1003l".utf8),
|
||||
Array("\u{1b}[?1006l".utf8),
|
||||
Array("\u{1b}[?1015l".utf8),
|
||||
Array("\u{1b}[?2004l".utf8),
|
||||
Array("\u{1b}[?1l".utf8),
|
||||
Array("\u{1b}[?1049l".utf8),
|
||||
Array("\u{1b}[?25h".utf8),
|
||||
]
|
||||
|
||||
for seq in resetSequences {
|
||||
terminalView.feed(byteArray: seq[...])
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func dataReceived(slice: ArraySlice<UInt8>) {
|
||||
let data = slice
|
||||
Task { @MainActor in
|
||||
if let restorePosition = self.scrollCoordinator.outputRestorePosition(canScroll: self.terminalView.canScroll) {
|
||||
self.scrollCoordinator.suppressTracking {
|
||||
self.terminalView.feed(byteArray: data)
|
||||
self.terminalView.scroll(toPosition: restorePosition)
|
||||
}
|
||||
} else {
|
||||
self.terminalView.feed(byteArray: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func getWindowSize() -> winsize {
|
||||
var ws = winsize()
|
||||
ws.ws_col = 80
|
||||
ws.ws_row = 24
|
||||
return ws
|
||||
}
|
||||
|
||||
// MARK: - TerminalViewDelegate
|
||||
|
||||
func send(source: TerminalView, data: ArraySlice<UInt8>) {
|
||||
_ = source
|
||||
|
||||
if scrollCoordinator.userDidStartTyping() {
|
||||
terminalView.scroll(toPosition: 1)
|
||||
}
|
||||
process?.send(data: data)
|
||||
}
|
||||
|
||||
func setTerminalTitle(source: TerminalView, title: String) {
|
||||
_ = source
|
||||
onTitleChange?(title)
|
||||
}
|
||||
|
||||
func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) {
|
||||
_ = source
|
||||
|
||||
guard newCols > 0, newRows > 0 else { return }
|
||||
guard let proc = process else { return }
|
||||
let fd = proc.childfd
|
||||
guard fd >= 0 else { return }
|
||||
|
||||
var ws = winsize()
|
||||
ws.ws_col = UInt16(newCols)
|
||||
ws.ws_row = UInt16(newRows)
|
||||
_ = ioctl(fd, TIOCSWINSZ, &ws)
|
||||
}
|
||||
|
||||
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
|
||||
_ = source
|
||||
onCurrentDirectoryChange?(directory)
|
||||
}
|
||||
|
||||
func scrolled(source: TerminalView, position: Double) {
|
||||
scrollCoordinator.terminalDidScroll(to: position, canScroll: source.canScroll)
|
||||
}
|
||||
|
||||
func rangeChanged(source: TerminalView, startY: Int, endY: Int) {
|
||||
_ = source
|
||||
_ = startY
|
||||
_ = endY
|
||||
}
|
||||
|
||||
func clipboardCopy(source: TerminalView, content: Data) {
|
||||
_ = source
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setData(content, forType: .string)
|
||||
}
|
||||
|
||||
func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
|
||||
_ = source
|
||||
_ = params
|
||||
|
||||
if let url = URL(string: link) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
func bell(source: TerminalView) {
|
||||
_ = source
|
||||
NSSound.beep()
|
||||
}
|
||||
|
||||
func iTermContent(source: TerminalView, content: ArraySlice<UInt8>) {
|
||||
_ = source
|
||||
_ = content
|
||||
}
|
||||
}
|
||||
75
CommandNotch/CommandNotch/Models/TerminalManager.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// Compatibility adapter for the legacy single-workspace architecture.
|
||||
/// New code should use `WorkspaceRegistry` + `WorkspaceController`.
|
||||
@MainActor
|
||||
class TerminalManager: ObservableObject {
|
||||
|
||||
static let shared = TerminalManager()
|
||||
|
||||
private var workspaceCancellable: AnyCancellable?
|
||||
|
||||
private init() {
|
||||
workspaceCancellable = WorkspaceRegistry.shared.defaultWorkspaceController.objectWillChange
|
||||
.sink { [weak self] _ in
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private var workspace: WorkspaceController {
|
||||
WorkspaceRegistry.shared.defaultWorkspaceController
|
||||
}
|
||||
|
||||
var tabs: [TerminalSession] {
|
||||
workspace.tabs
|
||||
}
|
||||
|
||||
var activeTabIndex: Int {
|
||||
workspace.activeTabIndex
|
||||
}
|
||||
|
||||
var activeTab: TerminalSession? {
|
||||
workspace.activeTab
|
||||
}
|
||||
|
||||
var activeTitle: String {
|
||||
workspace.activeTitle
|
||||
}
|
||||
|
||||
func newTab() {
|
||||
workspace.newTab()
|
||||
}
|
||||
|
||||
func closeActiveTab() {
|
||||
workspace.closeActiveTab()
|
||||
}
|
||||
|
||||
func closeTab(at index: Int) {
|
||||
workspace.closeTab(at: index)
|
||||
}
|
||||
|
||||
func switchToTab(at index: Int) {
|
||||
workspace.switchToTab(at: index)
|
||||
}
|
||||
|
||||
func nextTab() {
|
||||
workspace.nextTab()
|
||||
}
|
||||
|
||||
func previousTab() {
|
||||
workspace.previousTab()
|
||||
}
|
||||
|
||||
func detachActiveTab() -> TerminalSession? {
|
||||
workspace.detachActiveTab()
|
||||
}
|
||||
|
||||
func updateAllFontSizes(_ size: CGFloat) {
|
||||
workspace.updateAllFontSizes(size)
|
||||
}
|
||||
|
||||
func updateAllThemes(_ theme: TerminalTheme) {
|
||||
workspace.updateAllThemes(theme)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import AppKit
|
||||
import CoreText
|
||||
import SwiftTerm
|
||||
|
||||
enum TerminalScrollbackEstimator {
|
||||
private static let minimumColumns = 1
|
||||
private static let minimumRows = 1
|
||||
private static let defaultBytesPerLineOverhead = 256
|
||||
|
||||
struct Estimate: Equatable {
|
||||
let bytes: Int
|
||||
let columns: Int
|
||||
let rows: Int
|
||||
|
||||
var formattedBytes: String {
|
||||
ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .memory)
|
||||
}
|
||||
}
|
||||
|
||||
static func estimate(
|
||||
scrollbackLines: Int,
|
||||
fontSize: Double,
|
||||
openWidth: Double,
|
||||
openHeight: Double
|
||||
) -> Estimate {
|
||||
let safeScrollbackLines = max(0, scrollbackLines)
|
||||
let dimensions = terminalGridDimensions(
|
||||
fontSize: fontSize,
|
||||
openWidth: openWidth,
|
||||
openHeight: openHeight
|
||||
)
|
||||
let totalLines = safeScrollbackLines + dimensions.rows
|
||||
let bytesPerCell = MemoryLayout<CharData>.stride
|
||||
let bytesPerLine = (dimensions.columns * bytesPerCell) + defaultBytesPerLineOverhead
|
||||
let totalBytes = max(0, totalLines * bytesPerLine)
|
||||
|
||||
return Estimate(bytes: totalBytes, columns: dimensions.columns, rows: dimensions.rows)
|
||||
}
|
||||
|
||||
private static func terminalGridDimensions(
|
||||
fontSize: Double,
|
||||
openWidth: Double,
|
||||
openHeight: Double
|
||||
) -> (columns: Int, rows: Int) {
|
||||
let font = NSFont.monospacedSystemFont(ofSize: CGFloat(max(1, fontSize)), weight: .regular)
|
||||
let cellWidth = max(1, font.advancement(forGlyph: font.glyph(withName: "W")).width)
|
||||
let cellHeight = max(1, ceil(CTFontGetAscent(font) + CTFontGetDescent(font) + CTFontGetLeading(font)))
|
||||
|
||||
let columns = max(minimumColumns, Int(CGFloat(max(1, openWidth)) / cellWidth))
|
||||
let rows = max(minimumRows, Int(CGFloat(max(1, openHeight)) / cellHeight))
|
||||
|
||||
return (columns, rows)
|
||||
}
|
||||
}
|
||||
269
CommandNotch/CommandNotch/Models/TerminalSession.swift
Normal file
@@ -0,0 +1,269 @@
|
||||
import AppKit
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol TerminalBackendSession: AnyObject {
|
||||
var view: NSView { get }
|
||||
var onTitleChange: ((String) -> Void)? { get set }
|
||||
var onCurrentDirectoryChange: ((String?) -> Void)? { get set }
|
||||
var onExit: (() -> Void)? { get set }
|
||||
|
||||
func start()
|
||||
func terminate()
|
||||
func focus()
|
||||
func updateFontSize(_ size: CGFloat)
|
||||
func updateTheme(_ theme: TerminalTheme)
|
||||
func updateScrollbackLines(_ scrollbackLines: Int)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class TerminalSession: ObservableObject, Identifiable {
|
||||
let id = UUID()
|
||||
|
||||
@Published var title: String = "shell"
|
||||
@Published var isRunning: Bool = true
|
||||
@Published var currentDirectory: String?
|
||||
|
||||
private let backend: any TerminalBackendSession
|
||||
|
||||
init(
|
||||
backend: any TerminalBackendSession,
|
||||
initialDirectory: String? = nil,
|
||||
startImmediately: Bool = true
|
||||
) {
|
||||
self.backend = backend
|
||||
currentDirectory = Self.resolveInitialDirectory(initialDirectory)
|
||||
|
||||
backend.onTitleChange = { [weak self] title in
|
||||
self?.title = title.isEmpty ? "shell" : title
|
||||
}
|
||||
backend.onCurrentDirectoryChange = { [weak self] directory in
|
||||
guard let self,
|
||||
let normalizedDirectory = Self.normalizedDirectory(directory) else { return }
|
||||
self.currentDirectory = normalizedDirectory
|
||||
}
|
||||
backend.onExit = { [weak self] in
|
||||
self?.isRunning = false
|
||||
}
|
||||
|
||||
if startImmediately {
|
||||
start()
|
||||
} else {
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
backendPreference: TerminalBackendPreference,
|
||||
termTypePreference: TerminalTermTypePreference,
|
||||
initialDirectory: String? = nil,
|
||||
startImmediately: Bool = true
|
||||
) {
|
||||
let resolvedDirectory = Self.resolveInitialDirectory(initialDirectory)
|
||||
let backend = Self.makeBackend(
|
||||
fontSize: fontSize,
|
||||
theme: theme,
|
||||
shellPath: shellPath,
|
||||
scrollbackLines: scrollbackLines,
|
||||
backendPreference: backendPreference,
|
||||
termTypePreference: termTypePreference,
|
||||
initialDirectory: resolvedDirectory
|
||||
)
|
||||
|
||||
self.init(
|
||||
backend: backend,
|
||||
initialDirectory: resolvedDirectory,
|
||||
startImmediately: startImmediately
|
||||
)
|
||||
}
|
||||
|
||||
var view: NSView {
|
||||
backend.view
|
||||
}
|
||||
|
||||
func start() {
|
||||
isRunning = true
|
||||
backend.start()
|
||||
}
|
||||
|
||||
func focus() {
|
||||
backend.focus()
|
||||
}
|
||||
|
||||
func terminate() {
|
||||
backend.terminate()
|
||||
isRunning = false
|
||||
}
|
||||
|
||||
func updateFontSize(_ size: CGFloat) {
|
||||
backend.updateFontSize(size)
|
||||
}
|
||||
|
||||
func applyTheme(_ theme: TerminalTheme) {
|
||||
backend.updateTheme(theme)
|
||||
}
|
||||
|
||||
func updateScrollbackLines(_ scrollbackLines: Int) {
|
||||
backend.updateScrollbackLines(scrollbackLines)
|
||||
}
|
||||
|
||||
static func resolveInitialDirectory(_ directory: String?) -> String {
|
||||
normalizedDirectory(directory) ?? NSHomeDirectory()
|
||||
}
|
||||
|
||||
static func normalizedDirectory(_ directory: String?) -> String? {
|
||||
let trimmed = directory?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !trimmed.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let url = URL(string: trimmed), url.isFileURL {
|
||||
return url.path(percentEncoded: false)
|
||||
}
|
||||
|
||||
return (trimmed as NSString).expandingTildeInPath
|
||||
}
|
||||
|
||||
private static func makeBackend(
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
backendPreference: TerminalBackendPreference,
|
||||
termTypePreference: TerminalTermTypePreference,
|
||||
initialDirectory: String
|
||||
) -> any TerminalBackendSession {
|
||||
let resolvedBackendPreference = resolvedBackendPreference(configured: backendPreference)
|
||||
|
||||
if resolvedBackendPreference == .ghostty,
|
||||
let backend = GhosttyBackendSession.makeIfAvailable(
|
||||
fontSize: fontSize,
|
||||
theme: theme,
|
||||
shellPath: shellPath,
|
||||
scrollbackLines: scrollbackLines,
|
||||
termTypePreference: termTypePreference,
|
||||
initialDirectory: initialDirectory
|
||||
) {
|
||||
return backend
|
||||
}
|
||||
|
||||
return SwiftTermBackendSession(
|
||||
fontSize: fontSize,
|
||||
theme: theme,
|
||||
shellPath: shellPath,
|
||||
scrollbackLines: scrollbackLines,
|
||||
termTypePreference: termTypePreference,
|
||||
initialDirectory: initialDirectory
|
||||
)
|
||||
}
|
||||
|
||||
private static func resolvedBackendPreference(configured: TerminalBackendPreference) -> TerminalBackendPreference {
|
||||
let environmentOverride = ProcessInfo.processInfo.environment["COMMANDNOTCH_TERMINAL_BACKEND"]?.lowercased() ?? ""
|
||||
switch environmentOverride {
|
||||
case TerminalBackendPreference.swiftTerm.rawValue:
|
||||
return .swiftTerm
|
||||
case TerminalBackendPreference.ghostty.rawValue:
|
||||
return .ghostty
|
||||
default:
|
||||
return configured
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks whether the terminal viewport should follow live output or preserve
|
||||
/// the user's current scrollback position.
|
||||
final class TerminalScrollCoordinator {
|
||||
private let bottomThreshold: Double
|
||||
private var suppressScrollTracking = false
|
||||
|
||||
private(set) var followsOutput = true
|
||||
private(set) var preservedScrollPosition: Double = 1
|
||||
|
||||
init(bottomThreshold: Double = 0.999) {
|
||||
self.bottomThreshold = bottomThreshold
|
||||
}
|
||||
|
||||
func terminalDidScroll(to position: Double, canScroll: Bool) {
|
||||
guard !suppressScrollTracking else { return }
|
||||
|
||||
guard canScroll else {
|
||||
followsOutput = true
|
||||
preservedScrollPosition = 1
|
||||
return
|
||||
}
|
||||
|
||||
let clampedPosition = min(max(position, 0), 1)
|
||||
if clampedPosition >= bottomThreshold {
|
||||
followsOutput = true
|
||||
preservedScrollPosition = 1
|
||||
} else {
|
||||
followsOutput = false
|
||||
preservedScrollPosition = clampedPosition
|
||||
}
|
||||
}
|
||||
|
||||
func outputRestorePosition(canScroll: Bool) -> Double? {
|
||||
guard canScroll, !followsOutput else { return nil }
|
||||
return preservedScrollPosition
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func userDidStartTyping() -> Bool {
|
||||
let shouldJumpToBottom = !followsOutput
|
||||
followsOutput = true
|
||||
preservedScrollPosition = 1
|
||||
return shouldJumpToBottom
|
||||
}
|
||||
|
||||
func suppressTracking<T>(_ body: () -> T) -> T {
|
||||
suppressScrollTracking = true
|
||||
defer { suppressScrollTracking = false }
|
||||
return body()
|
||||
}
|
||||
}
|
||||
|
||||
/// Suppresses stale mouse-report forwarding when an interactive command exits
|
||||
/// without restoring terminal mouse modes cleanly.
|
||||
final class TerminalMouseCaptureCoordinator {
|
||||
private(set) var suppressesMouseReporting = false
|
||||
|
||||
@discardableResult
|
||||
func commandDidFinish(mouseCaptured: Bool) -> Bool {
|
||||
suppressesMouseReporting = mouseCaptured
|
||||
return mouseCaptured
|
||||
}
|
||||
|
||||
func shouldBypassEnhancedKeyboardInput(mouseCaptured: Bool) -> Bool {
|
||||
if !suppressesMouseReporting {
|
||||
return false
|
||||
}
|
||||
|
||||
if !mouseCaptured {
|
||||
suppressesMouseReporting = false
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func shouldForwardMouseInput(mouseCaptured: Bool) -> Bool {
|
||||
if !suppressesMouseReporting {
|
||||
return true
|
||||
}
|
||||
|
||||
if !mouseCaptured {
|
||||
suppressesMouseReporting = false
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func userDidSubmitCommand() {
|
||||
suppressesMouseReporting = false
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,21 @@ enum TerminalTheme: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundColor: NSColor {
|
||||
switch self {
|
||||
case .terminalApp:
|
||||
return Self.nsColor(0x000000)
|
||||
case .xterm:
|
||||
return Self.nsColor(0x000000)
|
||||
case .solarizedDark:
|
||||
return Self.nsColor(0x002B36)
|
||||
case .dracula:
|
||||
return Self.nsColor(0x282A36)
|
||||
case .nord:
|
||||
return Self.nsColor(0x2E3440)
|
||||
}
|
||||
}
|
||||
|
||||
var ansiColors: [Color] {
|
||||
switch self {
|
||||
case .terminalApp:
|
||||
208
CommandNotch/CommandNotch/Models/WorkspaceController.swift
Normal file
@@ -0,0 +1,208 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol TerminalSessionFactoryType {
|
||||
func makeSession(
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
backendPreference: TerminalBackendPreference,
|
||||
termTypePreference: TerminalTermTypePreference,
|
||||
initialDirectory: String?
|
||||
) -> TerminalSession
|
||||
}
|
||||
|
||||
struct LiveTerminalSessionFactory: TerminalSessionFactoryType {
|
||||
func makeSession(
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
backendPreference: TerminalBackendPreference,
|
||||
termTypePreference: TerminalTermTypePreference,
|
||||
initialDirectory: String?
|
||||
) -> TerminalSession {
|
||||
TerminalSession(
|
||||
fontSize: fontSize,
|
||||
theme: theme,
|
||||
shellPath: shellPath,
|
||||
scrollbackLines: scrollbackLines,
|
||||
backendPreference: backendPreference,
|
||||
termTypePreference: termTypePreference,
|
||||
initialDirectory: initialDirectory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class WorkspaceController: ObservableObject {
|
||||
let id: WorkspaceID
|
||||
let createdAt: Date
|
||||
|
||||
@Published private(set) var name: String
|
||||
@Published private(set) var hotkey: HotkeyBinding?
|
||||
@Published private(set) var tabs: [TerminalSession] = []
|
||||
@Published private(set) var activeTabIndex: Int = 0
|
||||
|
||||
private let sessionFactory: TerminalSessionFactoryType
|
||||
private let settingsProvider: TerminalSessionConfigurationProviding
|
||||
private var titleObservers: [UUID: AnyCancellable] = [:]
|
||||
|
||||
init(
|
||||
summary: WorkspaceSummary,
|
||||
sessionFactory: TerminalSessionFactoryType,
|
||||
settingsProvider: TerminalSessionConfigurationProviding,
|
||||
bootstrapDefaultTab: Bool = true
|
||||
) {
|
||||
self.id = summary.id
|
||||
self.name = summary.name
|
||||
self.createdAt = summary.createdAt
|
||||
self.hotkey = summary.hotkey
|
||||
self.sessionFactory = sessionFactory
|
||||
self.settingsProvider = settingsProvider
|
||||
|
||||
if bootstrapDefaultTab {
|
||||
newTab()
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(summary: WorkspaceSummary) {
|
||||
self.init(
|
||||
summary: summary,
|
||||
sessionFactory: LiveTerminalSessionFactory(),
|
||||
settingsProvider: AppSettingsController.shared
|
||||
)
|
||||
}
|
||||
|
||||
var summary: WorkspaceSummary {
|
||||
WorkspaceSummary(id: id, name: name, createdAt: createdAt, hotkey: hotkey)
|
||||
}
|
||||
|
||||
var state: WorkspaceState {
|
||||
WorkspaceState(
|
||||
id: id,
|
||||
name: name,
|
||||
tabs: tabs.map { WorkspaceTabState(id: $0.id, title: $0.title) },
|
||||
activeTabID: activeTab?.id
|
||||
)
|
||||
}
|
||||
|
||||
var activeTab: TerminalSession? {
|
||||
guard tabs.indices.contains(activeTabIndex) else { return nil }
|
||||
return tabs[activeTabIndex]
|
||||
}
|
||||
|
||||
var activeTitle: String {
|
||||
activeTab?.title ?? "shell"
|
||||
}
|
||||
|
||||
func rename(to updatedName: String) {
|
||||
let trimmed = updatedName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty, trimmed != name else { return }
|
||||
name = trimmed
|
||||
}
|
||||
|
||||
func updateHotkey(_ updatedHotkey: HotkeyBinding?) {
|
||||
guard hotkey != updatedHotkey else { return }
|
||||
hotkey = updatedHotkey
|
||||
}
|
||||
|
||||
func newTab() {
|
||||
let config = settingsProvider.terminalSessionConfiguration
|
||||
let session = sessionFactory.makeSession(
|
||||
fontSize: config.fontSize,
|
||||
theme: config.theme,
|
||||
shellPath: config.shellPath,
|
||||
scrollbackLines: config.scrollbackLines,
|
||||
backendPreference: config.backendPreference,
|
||||
termTypePreference: config.termTypePreference,
|
||||
initialDirectory: activeTab?.currentDirectory
|
||||
)
|
||||
|
||||
titleObservers[session.id] = session.$title
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
|
||||
tabs.append(session)
|
||||
activeTabIndex = tabs.count - 1
|
||||
}
|
||||
|
||||
func closeTab(at index: Int) {
|
||||
guard tabs.indices.contains(index) else { return }
|
||||
|
||||
let session = tabs.remove(at: index)
|
||||
titleObservers.removeValue(forKey: session.id)
|
||||
session.terminate()
|
||||
|
||||
if tabs.isEmpty {
|
||||
newTab()
|
||||
} else if activeTabIndex >= tabs.count {
|
||||
activeTabIndex = tabs.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
func closeActiveTab() {
|
||||
closeTab(at: activeTabIndex)
|
||||
}
|
||||
|
||||
func switchToTab(at index: Int) {
|
||||
guard tabs.indices.contains(index) else { return }
|
||||
activeTabIndex = index
|
||||
}
|
||||
|
||||
func switchToTab(id: UUID) {
|
||||
guard let index = tabs.firstIndex(where: { $0.id == id }) else { return }
|
||||
activeTabIndex = index
|
||||
}
|
||||
|
||||
func nextTab() {
|
||||
guard tabs.count > 1 else { return }
|
||||
activeTabIndex = (activeTabIndex + 1) % tabs.count
|
||||
}
|
||||
|
||||
func previousTab() {
|
||||
guard tabs.count > 1 else { return }
|
||||
activeTabIndex = (activeTabIndex - 1 + tabs.count) % tabs.count
|
||||
}
|
||||
|
||||
func detachTab(at index: Int) -> TerminalSession? {
|
||||
guard tabs.indices.contains(index) else { return nil }
|
||||
|
||||
let session = tabs.remove(at: index)
|
||||
titleObservers.removeValue(forKey: session.id)
|
||||
|
||||
if tabs.isEmpty {
|
||||
newTab()
|
||||
} else if activeTabIndex >= tabs.count {
|
||||
activeTabIndex = tabs.count - 1
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func detachActiveTab() -> TerminalSession? {
|
||||
detachTab(at: activeTabIndex)
|
||||
}
|
||||
|
||||
func updateAllFontSizes(_ size: CGFloat) {
|
||||
for tab in tabs {
|
||||
tab.updateFontSize(size)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAllThemes(_ theme: TerminalTheme) {
|
||||
for tab in tabs {
|
||||
tab.applyTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAllScrollbackLines(_ scrollbackLines: Int) {
|
||||
for tab in tabs {
|
||||
tab.updateScrollbackLines(scrollbackLines)
|
||||
}
|
||||
}
|
||||
}
|
||||
187
CommandNotch/CommandNotch/Models/WorkspaceRegistry.swift
Normal file
@@ -0,0 +1,187 @@
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class WorkspaceRegistry: ObservableObject {
|
||||
static let shared = WorkspaceRegistry(store: UserDefaultsWorkspaceStore())
|
||||
|
||||
@Published private(set) var workspaceSummaries: [WorkspaceSummary]
|
||||
|
||||
private let store: any WorkspaceStoreType
|
||||
private var controllers: [WorkspaceID: WorkspaceController] = [:]
|
||||
private let controllerFactory: @MainActor (WorkspaceSummary) -> WorkspaceController
|
||||
|
||||
init(
|
||||
initialWorkspaces: [WorkspaceSummary]? = nil,
|
||||
store: (any WorkspaceStoreType)? = nil,
|
||||
controllerFactory: @escaping @MainActor (WorkspaceSummary) -> WorkspaceController = { summary in
|
||||
WorkspaceController(summary: summary)
|
||||
}
|
||||
) {
|
||||
let resolvedStore = store ?? UserDefaultsWorkspaceStore()
|
||||
let resolvedWorkspaces = initialWorkspaces ?? resolvedStore.loadWorkspaceSummaries()
|
||||
|
||||
self.store = resolvedStore
|
||||
self.controllerFactory = controllerFactory
|
||||
self.workspaceSummaries = resolvedWorkspaces
|
||||
|
||||
for summary in resolvedWorkspaces {
|
||||
controllers[summary.id] = controllerFactory(summary)
|
||||
}
|
||||
|
||||
_ = ensureWorkspaceExists()
|
||||
}
|
||||
|
||||
var defaultWorkspaceID: WorkspaceID {
|
||||
ensureWorkspaceExists()
|
||||
}
|
||||
|
||||
var defaultWorkspaceController: WorkspaceController {
|
||||
let workspaceID = ensureWorkspaceExists()
|
||||
guard let controller = controllers[workspaceID] else {
|
||||
let summary = WorkspaceSummary(id: workspaceID, name: "Main")
|
||||
let controller = controllerFactory(summary)
|
||||
controllers[workspaceID] = controller
|
||||
return controller
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
func allWorkspaceSummaries() -> [WorkspaceSummary] {
|
||||
workspaceSummaries
|
||||
}
|
||||
|
||||
func summary(for id: WorkspaceID) -> WorkspaceSummary? {
|
||||
workspaceSummaries.first { $0.id == id }
|
||||
}
|
||||
|
||||
func controller(for id: WorkspaceID) -> WorkspaceController? {
|
||||
controllers[id]
|
||||
}
|
||||
|
||||
func canDeleteWorkspace(id: WorkspaceID) -> Bool {
|
||||
workspaceSummaries.count > 1 && workspaceSummaries.contains { $0.id == id }
|
||||
}
|
||||
|
||||
func deletionFallbackWorkspaceID(
|
||||
forDeleting id: WorkspaceID,
|
||||
preferredFallback preferredFallbackID: WorkspaceID? = nil
|
||||
) -> WorkspaceID? {
|
||||
let candidates = workspaceSummaries.filter { $0.id != id }
|
||||
|
||||
if let preferredFallbackID,
|
||||
candidates.contains(where: { $0.id == preferredFallbackID }) {
|
||||
return preferredFallbackID
|
||||
}
|
||||
|
||||
return candidates.first?.id
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func ensureWorkspaceExists() -> WorkspaceID {
|
||||
if let existing = workspaceSummaries.first {
|
||||
return existing.id
|
||||
}
|
||||
return createWorkspace(named: "Main")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func createWorkspace(named name: String? = nil) -> WorkspaceID {
|
||||
let workspaceName = resolvedWorkspaceName(from: name)
|
||||
let summary = WorkspaceSummary(name: workspaceName)
|
||||
workspaceSummaries.append(summary)
|
||||
controllers[summary.id] = controllerFactory(summary)
|
||||
persistWorkspaceSummaries()
|
||||
return summary.id
|
||||
}
|
||||
|
||||
func renameWorkspace(id: WorkspaceID, to name: String) {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else { return }
|
||||
|
||||
workspaceSummaries[index].name = trimmed
|
||||
controllers[id]?.rename(to: trimmed)
|
||||
persistWorkspaceSummaries()
|
||||
}
|
||||
|
||||
func updateWorkspaceHotkey(id: WorkspaceID, to hotkey: HotkeyBinding?) {
|
||||
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else { return }
|
||||
guard workspaceSummaries[index].hotkey != hotkey else { return }
|
||||
|
||||
workspaceSummaries[index].hotkey = hotkey
|
||||
controllers[id]?.updateHotkey(hotkey)
|
||||
persistWorkspaceSummaries()
|
||||
}
|
||||
|
||||
func nextWorkspaceID(after id: WorkspaceID) -> WorkspaceID? {
|
||||
guard !workspaceSummaries.isEmpty else { return nil }
|
||||
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else {
|
||||
return workspaceSummaries.first?.id
|
||||
}
|
||||
|
||||
let nextIndex = workspaceSummaries.index(after: index)
|
||||
return workspaceSummaries[nextIndex == workspaceSummaries.endIndex ? workspaceSummaries.startIndex : nextIndex].id
|
||||
}
|
||||
|
||||
func previousWorkspaceID(before id: WorkspaceID) -> WorkspaceID? {
|
||||
guard !workspaceSummaries.isEmpty else { return nil }
|
||||
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else {
|
||||
return workspaceSummaries.last?.id
|
||||
}
|
||||
|
||||
let previousIndex = index == workspaceSummaries.startIndex
|
||||
? workspaceSummaries.index(before: workspaceSummaries.endIndex)
|
||||
: workspaceSummaries.index(before: index)
|
||||
return workspaceSummaries[previousIndex].id
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func deleteWorkspace(id: WorkspaceID) -> Bool {
|
||||
guard canDeleteWorkspace(id: id) else { return false }
|
||||
workspaceSummaries.removeAll { $0.id == id }
|
||||
controllers.removeValue(forKey: id)
|
||||
_ = ensureWorkspaceExists()
|
||||
persistWorkspaceSummaries()
|
||||
return true
|
||||
}
|
||||
|
||||
func updateAllWorkspacesFontSizes(_ size: CGFloat) {
|
||||
for controller in controllers.values {
|
||||
controller.updateAllFontSizes(size)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAllWorkspacesThemes(_ theme: TerminalTheme) {
|
||||
for controller in controllers.values {
|
||||
controller.updateAllThemes(theme)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAllWorkspacesScrollbackLines(_ scrollbackLines: Int) {
|
||||
for controller in controllers.values {
|
||||
controller.updateAllScrollbackLines(scrollbackLines)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedWorkspaceName(from proposedName: String?) -> String {
|
||||
let trimmed = proposedName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
let existing = Set(workspaceSummaries.map(\.name))
|
||||
if !existing.contains("Main") {
|
||||
return "Main"
|
||||
}
|
||||
|
||||
var index = 2
|
||||
while existing.contains("Workspace \(index)") {
|
||||
index += 1
|
||||
}
|
||||
return "Workspace \(index)"
|
||||
}
|
||||
|
||||
private func persistWorkspaceSummaries() {
|
||||
store.saveWorkspaceSummaries(workspaceSummaries)
|
||||
}
|
||||
}
|
||||
59
CommandNotch/CommandNotch/Models/WorkspaceStore.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
|
||||
protocol WorkspaceStoreType {
|
||||
func loadWorkspaceSummaries() -> [WorkspaceSummary]
|
||||
func saveWorkspaceSummaries(_ summaries: [WorkspaceSummary])
|
||||
}
|
||||
|
||||
protocol ScreenAssignmentStoreType {
|
||||
func loadScreenAssignments() -> [ScreenID: WorkspaceID]
|
||||
func saveScreenAssignments(_ assignments: [ScreenID: WorkspaceID])
|
||||
}
|
||||
|
||||
struct UserDefaultsWorkspaceStore: WorkspaceStoreType {
|
||||
private let defaults: UserDefaults
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
func loadWorkspaceSummaries() -> [WorkspaceSummary] {
|
||||
guard let data = defaults.data(forKey: NotchSettings.Keys.workspaceSummaries),
|
||||
let summaries = try? decoder.decode([WorkspaceSummary].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
|
||||
return summaries
|
||||
}
|
||||
|
||||
func saveWorkspaceSummaries(_ summaries: [WorkspaceSummary]) {
|
||||
guard let data = try? encoder.encode(summaries) else { return }
|
||||
defaults.set(data, forKey: NotchSettings.Keys.workspaceSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
struct UserDefaultsScreenAssignmentStore: ScreenAssignmentStoreType {
|
||||
private let defaults: UserDefaults
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
func loadScreenAssignments() -> [ScreenID: WorkspaceID] {
|
||||
guard let data = defaults.data(forKey: NotchSettings.Keys.screenAssignments),
|
||||
let assignments = try? decoder.decode([ScreenID: WorkspaceID].self, from: data) else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
return assignments
|
||||
}
|
||||
|
||||
func saveScreenAssignments(_ assignments: [ScreenID: WorkspaceID]) {
|
||||
guard let data = try? encoder.encode(assignments) else { return }
|
||||
defaults.set(data, forKey: NotchSettings.Keys.screenAssignments)
|
||||
}
|
||||
}
|
||||
29
CommandNotch/CommandNotch/Models/WorkspaceSummary.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
typealias WorkspaceID = UUID
|
||||
|
||||
struct WorkspaceSummary: Identifiable, Equatable, Codable {
|
||||
var id: WorkspaceID
|
||||
var name: String
|
||||
var createdAt: Date
|
||||
var hotkey: HotkeyBinding?
|
||||
|
||||
init(id: WorkspaceID = UUID(), name: String, createdAt: Date = Date(), hotkey: HotkeyBinding? = nil) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.createdAt = createdAt
|
||||
self.hotkey = hotkey
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkspaceTabState: Identifiable, Equatable {
|
||||
var id: UUID
|
||||
var title: String
|
||||
}
|
||||
|
||||
struct WorkspaceState: Equatable {
|
||||
var id: WorkspaceID
|
||||
var name: String
|
||||
var tabs: [WorkspaceTabState]
|
||||
var activeTabID: UUID?
|
||||
}
|
||||
|
Before Width: | Height: | Size: 945 B After Width: | Height: | Size: 945 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 735 B |
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 958 B |
|
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 512 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,382 @@
|
||||
# bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions.
|
||||
# https://github.com/rcaloras/bash-preexec
|
||||
#
|
||||
#
|
||||
# 'preexec' functions are executed before each interactive command is
|
||||
# executed, with the interactive command as its argument. The 'precmd'
|
||||
# function is executed before each prompt is displayed.
|
||||
#
|
||||
# Author: Ryan Caloras (ryan@bashhub.com)
|
||||
# Forked from Original Author: Glyph Lefkowitz
|
||||
#
|
||||
# V0.6.0
|
||||
#
|
||||
|
||||
# General Usage:
|
||||
#
|
||||
# 1. Source this file at the end of your bash profile so as not to interfere
|
||||
# with anything else that's using PROMPT_COMMAND.
|
||||
#
|
||||
# 2. Add any precmd or preexec functions by appending them to their arrays:
|
||||
# e.g.
|
||||
# precmd_functions+=(my_precmd_function)
|
||||
# precmd_functions+=(some_other_precmd_function)
|
||||
#
|
||||
# preexec_functions+=(my_preexec_function)
|
||||
#
|
||||
# 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND
|
||||
# to use preexec and precmd instead. Preexisting usages will be
|
||||
# preserved, but doing so manually may be less surprising.
|
||||
#
|
||||
# Note: This module requires two Bash features which you must not otherwise be
|
||||
# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override
|
||||
# either of these after bash-preexec has been installed it will most likely break.
|
||||
|
||||
# Tell shellcheck what kind of file this is.
|
||||
# shellcheck shell=bash
|
||||
|
||||
# Make sure this is bash that's running and return otherwise.
|
||||
# Use POSIX syntax for this line:
|
||||
if [ -z "${BASH_VERSION-}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# We only support Bash 3.1+.
|
||||
# Note: BASH_VERSINFO is first available in Bash-2.0.
|
||||
if [[ -z "${BASH_VERSINFO-}" ]] || (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 1) )); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Avoid duplicate inclusion
|
||||
if [[ -n "${bash_preexec_imported:-}" || -n "${__bp_imported:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
bash_preexec_imported="defined"
|
||||
|
||||
# WARNING: This variable is no longer used and should not be relied upon.
|
||||
# Use ${bash_preexec_imported} instead.
|
||||
# shellcheck disable=SC2034
|
||||
__bp_imported="${bash_preexec_imported}"
|
||||
|
||||
# Should be available to each precmd and preexec
|
||||
# functions, should they want it. $? and $_ are available as $? and $_, but
|
||||
# $PIPESTATUS is available only in a copy, $BP_PIPESTATUS.
|
||||
# TODO: Figure out how to restore PIPESTATUS before each precmd or preexec
|
||||
# function.
|
||||
__bp_last_ret_value="$?"
|
||||
BP_PIPESTATUS=("${PIPESTATUS[@]}")
|
||||
__bp_last_argument_prev_command="$_"
|
||||
|
||||
__bp_inside_precmd=0
|
||||
__bp_inside_preexec=0
|
||||
|
||||
# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install
|
||||
__bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install'
|
||||
|
||||
# Fails if any of the given variables are readonly
|
||||
# Reference https://stackoverflow.com/a/4441178
|
||||
__bp_require_not_readonly() {
|
||||
local var
|
||||
for var; do
|
||||
if ! ( unset "$var" 2> /dev/null ); then
|
||||
echo "bash-preexec requires write access to ${var}" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Remove ignorespace and or replace ignoreboth from HISTCONTROL
|
||||
# so we can accurately invoke preexec with a command from our
|
||||
# history even if it starts with a space.
|
||||
__bp_adjust_histcontrol() {
|
||||
local histcontrol
|
||||
histcontrol="${HISTCONTROL:-}"
|
||||
histcontrol="${histcontrol//ignorespace}"
|
||||
# Replace ignoreboth with ignoredups
|
||||
if [[ "$histcontrol" == *"ignoreboth"* ]]; then
|
||||
histcontrol="ignoredups:${histcontrol//ignoreboth}"
|
||||
fi
|
||||
export HISTCONTROL="$histcontrol"
|
||||
}
|
||||
|
||||
# This variable describes whether we are currently in "interactive mode";
|
||||
# i.e. whether this shell has just executed a prompt and is waiting for user
|
||||
# input. It documents whether the current command invoked by the trace hook is
|
||||
# run interactively by the user; it's set immediately after the prompt hook,
|
||||
# and unset as soon as the trace hook is run.
|
||||
__bp_preexec_interactive_mode=""
|
||||
|
||||
# These arrays are used to add functions to be run before, or after, prompts.
|
||||
declare -a precmd_functions
|
||||
declare -a preexec_functions
|
||||
|
||||
# Trims leading and trailing whitespace from $2 and writes it to the variable
|
||||
# name passed as $1
|
||||
__bp_trim_whitespace() {
|
||||
local var=${1:?} text=${2:-}
|
||||
text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters
|
||||
text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters
|
||||
printf -v "$var" '%s' "$text"
|
||||
}
|
||||
|
||||
|
||||
# Trims whitespace and removes any leading or trailing semicolons from $2 and
|
||||
# writes the resulting string to the variable name passed as $1. Used for
|
||||
# manipulating substrings in PROMPT_COMMAND
|
||||
__bp_sanitize_string() {
|
||||
local var=${1:?} text=${2:-} sanitized
|
||||
__bp_trim_whitespace sanitized "$text"
|
||||
sanitized=${sanitized%;}
|
||||
sanitized=${sanitized#;}
|
||||
__bp_trim_whitespace sanitized "$sanitized"
|
||||
printf -v "$var" '%s' "$sanitized"
|
||||
}
|
||||
|
||||
# This function is installed as part of the PROMPT_COMMAND;
|
||||
# It sets a variable to indicate that the prompt was just displayed,
|
||||
# to allow the DEBUG trap to know that the next command is likely interactive.
|
||||
__bp_interactive_mode() {
|
||||
__bp_preexec_interactive_mode="on"
|
||||
}
|
||||
|
||||
|
||||
# This function is installed as part of the PROMPT_COMMAND.
|
||||
# It will invoke any functions defined in the precmd_functions array.
|
||||
__bp_precmd_invoke_cmd() {
|
||||
# Save the returned value from our last command, and from each process in
|
||||
# its pipeline. Note: this MUST be the first thing done in this function.
|
||||
# BP_PIPESTATUS may be unused, ignore
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
__bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}")
|
||||
|
||||
# Don't invoke precmds if we are inside an execution of an "original
|
||||
# prompt command" by another precmd execution loop. This avoids infinite
|
||||
# recursion.
|
||||
if (( __bp_inside_precmd > 0 )); then
|
||||
return
|
||||
fi
|
||||
local __bp_inside_precmd=1
|
||||
|
||||
# Invoke every function defined in our function array.
|
||||
local precmd_function
|
||||
for precmd_function in "${precmd_functions[@]}"; do
|
||||
|
||||
# Only execute this function if it actually exists.
|
||||
# Test existence of functions with: declare -[Ff]
|
||||
if type -t "$precmd_function" 1>/dev/null; then
|
||||
__bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command"
|
||||
# Quote our function invocation to prevent issues with IFS
|
||||
"$precmd_function"
|
||||
fi
|
||||
done
|
||||
|
||||
__bp_set_ret_value "$__bp_last_ret_value"
|
||||
}
|
||||
|
||||
# Sets a return value in $?. We may want to get access to the $? variable in our
|
||||
# precmd functions. This is available for instance in zsh. We can simulate it in bash
|
||||
# by setting the value here.
|
||||
__bp_set_ret_value() {
|
||||
return ${1:+"$1"}
|
||||
}
|
||||
|
||||
__bp_in_prompt_command() {
|
||||
|
||||
local prompt_command_array IFS=$'\n;'
|
||||
read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND[*]:-}"
|
||||
|
||||
local trimmed_arg
|
||||
__bp_trim_whitespace trimmed_arg "${1:-}"
|
||||
|
||||
local command trimmed_command
|
||||
for command in "${prompt_command_array[@]:-}"; do
|
||||
__bp_trim_whitespace trimmed_command "$command"
|
||||
if [[ "$trimmed_command" == "$trimmed_arg" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# This function is installed as the DEBUG trap. It is invoked before each
|
||||
# interactive prompt display. Its purpose is to inspect the current
|
||||
# environment to attempt to detect if the current command is being invoked
|
||||
# interactively, and invoke 'preexec' if so.
|
||||
__bp_preexec_invoke_exec() {
|
||||
|
||||
# Save the contents of $_ so that it can be restored later on.
|
||||
# https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702
|
||||
__bp_last_argument_prev_command="${1:-}"
|
||||
# Don't invoke preexecs if we are inside of another preexec.
|
||||
if (( __bp_inside_preexec > 0 )); then
|
||||
return
|
||||
fi
|
||||
local __bp_inside_preexec=1
|
||||
|
||||
# Checks if the file descriptor is not standard out (i.e. '1')
|
||||
# __bp_delay_install checks if we're in test. Needed for bats to run.
|
||||
# Prevents preexec from being invoked for functions in PS1
|
||||
if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "${COMP_POINT:-}" || -n "${READLINE_POINT:-}" ]]; then
|
||||
# We're in the middle of a completer or a keybinding set up by "bind
|
||||
# -x". This obviously can't be an interactively issued command.
|
||||
return
|
||||
fi
|
||||
if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then
|
||||
# We're doing something related to displaying the prompt. Let the
|
||||
# prompt set the title instead of me.
|
||||
return
|
||||
else
|
||||
# If we're in a subshell, then the prompt won't be re-displayed to put
|
||||
# us back into interactive mode, so let's not set the variable back.
|
||||
# In other words, if you have a subshell like
|
||||
# (sleep 1; sleep 2)
|
||||
# You want to see the 'sleep 2' as a set_command_title as well.
|
||||
if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then
|
||||
__bp_preexec_interactive_mode=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if __bp_in_prompt_command "${BASH_COMMAND:-}"; then
|
||||
# If we're executing something inside our prompt_command then we don't
|
||||
# want to call preexec. Bash prior to 3.1 can't detect this at all :/
|
||||
__bp_preexec_interactive_mode=""
|
||||
return
|
||||
fi
|
||||
|
||||
local this_command
|
||||
this_command=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1)
|
||||
this_command="${this_command#*[[:digit:]][* ] }"
|
||||
|
||||
# Sanity check to make sure we have something to invoke our function with.
|
||||
if [[ -z "$this_command" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Invoke every function defined in our function array.
|
||||
local preexec_function
|
||||
local preexec_function_ret_value
|
||||
local preexec_ret_value=0
|
||||
for preexec_function in "${preexec_functions[@]:-}"; do
|
||||
|
||||
# Only execute each function if it actually exists.
|
||||
# Test existence of function with: declare -[fF]
|
||||
if type -t "$preexec_function" 1>/dev/null; then
|
||||
__bp_set_ret_value "${__bp_last_ret_value:-}"
|
||||
# Quote our function invocation to prevent issues with IFS
|
||||
"$preexec_function" "$this_command"
|
||||
preexec_function_ret_value="$?"
|
||||
if [[ "$preexec_function_ret_value" != 0 ]]; then
|
||||
preexec_ret_value="$preexec_function_ret_value"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Restore the last argument of the last executed command, and set the return
|
||||
# value of the DEBUG trap to be the return code of the last preexec function
|
||||
# to return an error.
|
||||
# If `extdebug` is enabled a non-zero return value from any preexec function
|
||||
# will cause the user's command not to execute.
|
||||
# Run `shopt -s extdebug` to enable
|
||||
__bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command"
|
||||
}
|
||||
|
||||
__bp_install() {
|
||||
# Exit if we already have this installed.
|
||||
if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
trap '__bp_preexec_invoke_exec "$_"' DEBUG
|
||||
|
||||
# Preserve any prior DEBUG trap as a preexec function
|
||||
eval "local trap_argv=(${__bp_trap_string:-})"
|
||||
local prior_trap=${trap_argv[2]:-}
|
||||
unset __bp_trap_string
|
||||
if [[ -n "$prior_trap" ]]; then
|
||||
eval '__bp_original_debug_trap() {
|
||||
'"$prior_trap"'
|
||||
}'
|
||||
preexec_functions+=(__bp_original_debug_trap)
|
||||
fi
|
||||
|
||||
# Adjust our HISTCONTROL Variable if needed.
|
||||
#
|
||||
# GHOSTTY: Don't modify HISTCONTROL. This hack is only needed to improve the
|
||||
# accuracy of the command argument passed to the preexec functions, and we
|
||||
# don't use that argument in our bash shell integration script (and nor does
|
||||
# the __bp_original_debug_trap function above, which is the only other active
|
||||
# preexec function).
|
||||
#__bp_adjust_histcontrol
|
||||
|
||||
# Issue #25. Setting debug trap for subshells causes sessions to exit for
|
||||
# backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash.
|
||||
#
|
||||
# Disabling this by default. It can be enabled by setting this variable.
|
||||
if [[ -n "${__bp_enable_subshells:-}" ]]; then
|
||||
|
||||
# Set so debug trap will work be invoked in subshells.
|
||||
set -o functrace > /dev/null 2>&1
|
||||
shopt -s extdebug > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
local existing_prompt_command
|
||||
# Remove setting our trap install string and sanitize the existing prompt command string
|
||||
existing_prompt_command="${PROMPT_COMMAND:-}"
|
||||
# Edge case of appending to PROMPT_COMMAND
|
||||
existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op
|
||||
existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only
|
||||
existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only
|
||||
__bp_sanitize_string existing_prompt_command "$existing_prompt_command"
|
||||
if [[ "${existing_prompt_command:-:}" == ":" ]]; then
|
||||
existing_prompt_command=
|
||||
fi
|
||||
|
||||
# Install our hooks in PROMPT_COMMAND to allow our trap to know when we've
|
||||
# actually entered something.
|
||||
PROMPT_COMMAND='__bp_precmd_invoke_cmd'
|
||||
PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command}
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then
|
||||
PROMPT_COMMAND+=('__bp_interactive_mode')
|
||||
else
|
||||
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND+=$'\n__bp_interactive_mode'
|
||||
fi
|
||||
|
||||
# Add two functions to our arrays for convenience
|
||||
# of definition.
|
||||
precmd_functions+=(precmd)
|
||||
preexec_functions+=(preexec)
|
||||
|
||||
# Invoke our two functions manually that were added to $PROMPT_COMMAND
|
||||
__bp_precmd_invoke_cmd
|
||||
__bp_interactive_mode
|
||||
}
|
||||
|
||||
# Sets an installation string as part of our PROMPT_COMMAND to install
|
||||
# after our session has started. This allows bash-preexec to be included
|
||||
# at any point in our bash profile.
|
||||
__bp_install_after_session_init() {
|
||||
# bash-preexec needs to modify these variables in order to work correctly
|
||||
# if it can't, just stop the installation
|
||||
__bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return
|
||||
|
||||
local sanitized_prompt_command
|
||||
__bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}"
|
||||
if [[ -n "$sanitized_prompt_command" ]]; then
|
||||
# shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
|
||||
fi
|
||||
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND+=${__bp_install_string}
|
||||
}
|
||||
|
||||
# Run our install so long as we're not delaying it.
|
||||
if [[ -z "${__bp_delay_install:-}" ]]; then
|
||||
__bp_install_after_session_init
|
||||
fi
|
||||
@@ -0,0 +1,324 @@
|
||||
# Parts of this script are based on Kitty's bash integration. Kitty is
|
||||
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||
# The license header is reproduced below:
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# We need to be in interactive mode to proceed.
|
||||
if [[ "$-" != *i* ]]; then builtin return; fi
|
||||
|
||||
# When automatic shell integration is active, we were started in POSIX
|
||||
# mode and need to manually recreate the bash startup sequence.
|
||||
if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
# Store a temporary copy of our startup flags and unset these global
|
||||
# environment variables so we can safely handle reentrancy.
|
||||
builtin declare __ghostty_bash_flags="$GHOSTTY_BASH_INJECT"
|
||||
builtin unset ENV GHOSTTY_BASH_INJECT
|
||||
|
||||
# Restore an existing ENV that was replaced by the shell integration code.
|
||||
if [[ -n "$GHOSTTY_BASH_ENV" ]]; then
|
||||
builtin export ENV=$GHOSTTY_BASH_ENV
|
||||
builtin unset GHOSTTY_BASH_ENV
|
||||
fi
|
||||
|
||||
# Restore bash's default 'posix' behavior. Also reset 'inherit_errexit',
|
||||
# which doesn't happen as part of the 'posix' reset.
|
||||
builtin set +o posix
|
||||
builtin shopt -u inherit_errexit 2>/dev/null
|
||||
|
||||
# Unexport HISTFILE if it was set by the shell integration code.
|
||||
if [[ -n "$GHOSTTY_BASH_UNEXPORT_HISTFILE" ]]; then
|
||||
builtin export -n HISTFILE
|
||||
builtin unset GHOSTTY_BASH_UNEXPORT_HISTFILE
|
||||
fi
|
||||
|
||||
# Manually source the startup files. See INVOCATION in bash(1) and
|
||||
# run_startup_files() in shell.c in the Bash source code.
|
||||
if builtin shopt -q login_shell; then
|
||||
if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
|
||||
[ -r /etc/profile ] && builtin source "/etc/profile"
|
||||
for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
|
||||
[ -r "$__ghostty_rcfile" ] && {
|
||||
builtin source "$__ghostty_rcfile"
|
||||
break
|
||||
}
|
||||
done
|
||||
fi
|
||||
else
|
||||
if [[ $__ghostty_bash_flags != *"--norc"* ]]; then
|
||||
# The location of the system bashrc is determined at bash build
|
||||
# time via -DSYS_BASHRC and can therefore vary across distros:
|
||||
# Arch, Debian, Ubuntu use /etc/bash.bashrc
|
||||
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
|
||||
# Void Linux uses /etc/bash/bashrc
|
||||
# Nixos uses /etc/bashrc
|
||||
for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
|
||||
[ -r "$__ghostty_rcfile" ] && {
|
||||
builtin source "$__ghostty_rcfile"
|
||||
break
|
||||
}
|
||||
done
|
||||
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
|
||||
[ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
builtin unset __ghostty_rcfile
|
||||
builtin unset __ghostty_bash_flags
|
||||
builtin unset GHOSTTY_BASH_RCFILE
|
||||
fi
|
||||
|
||||
# Add Ghostty binary to PATH if the path feature is enabled
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* && -n "$GHOSTTY_BIN_DIR" ]]; then
|
||||
if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then
|
||||
export PATH="$PATH:$GHOSTTY_BIN_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sudo
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved.
|
||||
#
|
||||
# This approach supports wrapping a `sudo` alias, but the alias definition
|
||||
# must come _after_ this function is defined. Otherwise, the alias expansion
|
||||
# will take precedence over this function, and it won't be wrapped.
|
||||
function sudo {
|
||||
builtin local sudo_has_sudoedit_flags="no"
|
||||
for arg in "$@"; do
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
|
||||
sudo_has_sudoedit_flags="yes"
|
||||
builtin break
|
||||
fi
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
if [[ "$arg" != -* && "$arg" != *=* ]]; then
|
||||
builtin break
|
||||
fi
|
||||
done
|
||||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@"
|
||||
else
|
||||
builtin command sudo --preserve-env=TERMINFO "$@"
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# SSH Integration
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
function ssh() {
|
||||
builtin local ssh_term ssh_opts
|
||||
ssh_term="xterm-256color"
|
||||
ssh_opts=()
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then
|
||||
ssh_opts+=(-o "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
fi
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then
|
||||
builtin local ssh_user ssh_hostname
|
||||
|
||||
while IFS=' ' read -r ssh_key ssh_value; do
|
||||
case "$ssh_key" in
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
esac
|
||||
[[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break
|
||||
done < <(builtin command ssh -G "$@" 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_hostname" ]]; then
|
||||
builtin local ssh_target="${ssh_user}@${ssh_hostname}"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then
|
||||
ssh_term="xterm-ghostty"
|
||||
elif builtin command -v infocmp >/dev/null 2>&1; then
|
||||
builtin local ssh_terminfo ssh_cpath_dir ssh_cpath
|
||||
|
||||
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_terminfo" ]]; then
|
||||
builtin echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$"
|
||||
ssh_cpath="$ssh_cpath_dir/socket"
|
||||
|
||||
if builtin echo "$ssh_terminfo" | builtin command ssh -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null; then
|
||||
ssh_term="xterm-ghostty"
|
||||
ssh_opts+=(-o "ControlPath=$ssh_cpath")
|
||||
|
||||
# Cache successful installation
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true
|
||||
else
|
||||
builtin echo "Warning: Failed to install terminfo." >&2
|
||||
fi
|
||||
else
|
||||
builtin echo "Warning: Could not generate terminfo data." >&2
|
||||
fi
|
||||
else
|
||||
builtin echo "Warning: ghostty command not available for cache management." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" COLORTERM=truecolor builtin command ssh "${ssh_opts[@]}" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
# This is set to 1 when we're executing a command so that we don't
|
||||
# send prompt marks multiple times.
|
||||
_ghostty_executing=""
|
||||
_ghostty_last_reported_cwd=""
|
||||
|
||||
function __ghostty_precmd() {
|
||||
local ret="$?"
|
||||
if test "$_ghostty_executing" != "0"; then
|
||||
_GHOSTTY_SAVE_PS1="$PS1"
|
||||
_GHOSTTY_SAVE_PS2="$PS2"
|
||||
|
||||
# Use 133;P (not 133;A) inside PS1 to avoid fresh-line behavior on
|
||||
# readline redraws (e.g., vi mode switches, Ctrl-L). The initial
|
||||
# 133;A with fresh-line is emitted once via printf below.
|
||||
PS1='\[\e]133;P;k=i\a\]'$PS1'\[\e]133;B\a\]'
|
||||
PS2='\[\e]133;P;k=s\a\]'$PS2'\[\e]133;B\a\]'
|
||||
|
||||
# Bash doesn't redraw the leading lines in a multiline prompt so we mark
|
||||
# the start of each line (after each newline) as a secondary prompt. This
|
||||
# correctly handles multiline prompts by setting the first to primary and
|
||||
# the subsequent lines to secondary.
|
||||
#
|
||||
# We only replace the \n prompt escape, not literal newlines ($'\n'),
|
||||
# because literal newlines may appear inside $(...) command substitutions
|
||||
# where inserting escape sequences would break shell syntax.
|
||||
if [[ "$PS1" == *"\n"* ]]; then
|
||||
PS1="${PS1//\\n/\\n$'\\[\\e]133;P;k=s\\a\\]'}"
|
||||
fi
|
||||
|
||||
# Cursor
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
builtin local cursor=5 # blinking bar
|
||||
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && cursor=6 # steady bar
|
||||
|
||||
[[ "$PS1" != *"\[\e[${cursor} q\]"* ]] && PS1=$PS1"\[\e[${cursor} q\]"
|
||||
[[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset
|
||||
fi
|
||||
|
||||
# Title (working directory)
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
PS1=$PS1'\[\e]2;\w\a\]'
|
||||
fi
|
||||
fi
|
||||
|
||||
if test "$_ghostty_executing" != ""; then
|
||||
# End of current command. Report its status.
|
||||
builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
|
||||
fi
|
||||
|
||||
# Fresh line and start of prompt. When ble.sh is active, emit 133;P instead
|
||||
# of 133;A because ble.sh maintains its own cursor position tracking. 133;A's
|
||||
# cursor movement (CR+LF when not at column 0) is invisible to ble.sh and
|
||||
# desyncs its position state, causing display artifacts like duplicate
|
||||
# prompts. See: https://github.com/akinomyoga/ble.sh/issues/684
|
||||
if [[ -n "${BLE_VERSION-}" ]]; then
|
||||
builtin printf "\e]133;P;k=i\a"
|
||||
else
|
||||
builtin printf "\e]133;A;redraw=last;cl=line;aid=%s\a" "$BASHPID"
|
||||
fi
|
||||
|
||||
# unfortunately bash provides no hooks to detect cwd changes
|
||||
# in particular this means cwd reporting will not happen for a
|
||||
# command like cd /test && cat. PS0 is evaluated before cd is run.
|
||||
if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then
|
||||
_ghostty_last_reported_cwd="$PWD"
|
||||
builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
|
||||
fi
|
||||
|
||||
_ghostty_executing=0
|
||||
}
|
||||
|
||||
function __ghostty_preexec() {
|
||||
builtin local cmd="$1"
|
||||
|
||||
PS1="$_GHOSTTY_SAVE_PS1"
|
||||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
|
||||
# Title (current command)
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}"
|
||||
fi
|
||||
|
||||
# End of input, start of output.
|
||||
builtin printf "\e]133;C;\a"
|
||||
_ghostty_executing=1
|
||||
}
|
||||
|
||||
if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )); then
|
||||
__ghostty_preexec_hook() {
|
||||
builtin local cmd
|
||||
cmd=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1)
|
||||
cmd="${cmd#*[[:digit:]][* ] }" # remove leading history number
|
||||
[[ -n "$cmd" ]] && __ghostty_preexec "$cmd"
|
||||
}
|
||||
|
||||
__ghostty_hook() {
|
||||
builtin local ret=$?
|
||||
__ghostty_precmd "$ret"
|
||||
|
||||
# Append preexec hook to PS0 if not already present.
|
||||
# Use function substitution in 5.3+, otherwise command substitution.
|
||||
if [[ "$PS0" != *"__ghostty_preexec_hook"* ]]; then
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then
|
||||
# shellcheck disable=SC2016
|
||||
PS0+='${ __ghostty_preexec_hook; }'
|
||||
else
|
||||
# shellcheck disable=SC2016
|
||||
PS0+='$(__ghostty_preexec_hook >/dev/tty)'
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Append our hook to PROMPT_COMMAND, preserving its existing type.
|
||||
#
|
||||
# The 2>/dev/null suppresses "command not found" in subshells that inherit
|
||||
# PROMPT_COMMAND without the function definition. This also silences any
|
||||
# errors from inside __ghostty_hook itself, but those are all terminal escape
|
||||
# sequences and non-actionable.
|
||||
#
|
||||
# shellcheck disable=SC2128,SC2178,SC2179
|
||||
if [[ ";${PROMPT_COMMAND[*]:-};" != *";__ghostty_hook 2>/dev/null;"* ]]; then
|
||||
if [[ -z "${PROMPT_COMMAND[*]}" ]]; then
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then
|
||||
PROMPT_COMMAND=("__ghostty_hook 2>/dev/null")
|
||||
else
|
||||
PROMPT_COMMAND="__ghostty_hook 2>/dev/null"
|
||||
fi
|
||||
elif [[ $(builtin declare -p PROMPT_COMMAND 2>/dev/null) == "declare -a "* ]]; then
|
||||
PROMPT_COMMAND+=("__ghostty_hook 2>/dev/null")
|
||||
else
|
||||
[[ "${PROMPT_COMMAND}" =~ (\;[[:space:]]*|$'\n')$ ]] || PROMPT_COMMAND+=";"
|
||||
PROMPT_COMMAND+="__ghostty_hook 2>/dev/null"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh"
|
||||
preexec_functions+=(__ghostty_preexec)
|
||||
precmd_functions+=(__ghostty_precmd)
|
||||
fi
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
use platform
|
||||
use str
|
||||
|
||||
# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
if (and (has-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR) (has-env XDG_DATA_DIRS)) {
|
||||
set-env XDG_DATA_DIRS (str:replace $E:GHOSTTY_SHELL_INTEGRATION_XDG_DIR":" "" $E:XDG_DATA_DIRS)
|
||||
unset-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
}
|
||||
|
||||
# List of enabled shell integration features
|
||||
var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)]
|
||||
|
||||
# State tracking for semantic prompt sequences
|
||||
# Values: 'prompt-start', 'pre-exec', 'post-exec'
|
||||
fn set-prompt-state {|new| set-env __ghostty_prompt_state $new }
|
||||
|
||||
fn mark-prompt-start {
|
||||
if (not-eq $E:__ghostty_prompt_state 'prompt-start') {
|
||||
printf "\e]133;D;aid="$pid"\a"
|
||||
}
|
||||
set-prompt-state 'prompt-start'
|
||||
printf "\e]133;A;aid="$pid"\a"
|
||||
}
|
||||
|
||||
fn mark-output-start {|_|
|
||||
set-prompt-state 'pre-exec'
|
||||
printf "\e]133;C\a"
|
||||
}
|
||||
|
||||
fn mark-output-end {|cmd-info|
|
||||
set-prompt-state 'post-exec'
|
||||
|
||||
var exit-status = 0
|
||||
|
||||
# in case of error: retrieve exit status,
|
||||
# unless does not exist (= builtin function failure), then default to 1
|
||||
if (not-eq $nil $cmd-info[error]) {
|
||||
set exit-status = 1
|
||||
|
||||
if (has-key $cmd-info[error] reason) {
|
||||
if (has-key $cmd-info[error][reason] exit-status) {
|
||||
set exit-status = $cmd-info[error][reason][exit-status]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf "\e]133;D;"$exit-status";aid="$pid"\a"
|
||||
}
|
||||
|
||||
# NOTE: OSC 133;B (end of prompt, start of input) cannot be reliably
|
||||
# implemented at the script level in Elvish. The prompt function's output is
|
||||
# escaped, and writing to /dev/tty has timing issues because Elvish renders
|
||||
# its prompts on a background thread. Full semantic prompt support requires a
|
||||
# native implementation: https://github.com/elves/elvish/pull/1917
|
||||
|
||||
fn sudo-with-terminfo {|@args|
|
||||
var sudoedit = $false
|
||||
for arg $args {
|
||||
if (str:has-prefix $arg --) {
|
||||
if (eq $arg --edit) {
|
||||
set sudoedit = $true
|
||||
break
|
||||
}
|
||||
} elif (str:has-prefix $arg -) {
|
||||
if (str:contains (str:trim-prefix $arg -) e) {
|
||||
set sudoedit = $true
|
||||
break
|
||||
}
|
||||
} elif (not (str:contains $arg =)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (not $sudoedit) { set args = [ --preserve-env=TERMINFO $@args ] }
|
||||
(external sudo) $@args
|
||||
}
|
||||
|
||||
fn ssh-integration {|@args|
|
||||
var ssh-term = "xterm-256color"
|
||||
var ssh-opts = []
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if (has-value $features ssh-env) {
|
||||
set ssh-opts = (conj $ssh-opts ^
|
||||
-o "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
}
|
||||
|
||||
if (has-value $features ssh-terminfo) {
|
||||
var ssh-user = ""
|
||||
var ssh-hostname = ""
|
||||
|
||||
# Parse ssh config
|
||||
for line [((external ssh) -G $@args)] {
|
||||
var parts = [(str:fields $line)]
|
||||
if (> (count $parts) 1) {
|
||||
var ssh-key = $parts[0]
|
||||
var ssh-value = $parts[1]
|
||||
if (eq $ssh-key user) {
|
||||
set ssh-user = $ssh-value
|
||||
} elif (eq $ssh-key hostname) {
|
||||
set ssh-hostname = $ssh-value
|
||||
}
|
||||
if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (not-eq $ssh-hostname "") {
|
||||
var ghostty = $E:GHOSTTY_BIN_DIR/"ghostty"
|
||||
var ssh-target = $ssh-user"@"$ssh-hostname
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if (bool ?($ghostty +ssh-cache --host=$ssh-target)) {
|
||||
set ssh-term = "xterm-ghostty"
|
||||
} elif (has-external infocmp) {
|
||||
var ssh-terminfo = ((external infocmp) -0 -x xterm-ghostty 2>/dev/null | slurp)
|
||||
|
||||
if (not-eq $ssh-terminfo "") {
|
||||
echo "Setting up xterm-ghostty terminfo on "$ssh-hostname"..." >&2
|
||||
|
||||
use os
|
||||
var ssh-cpath-dir = (os:temp-dir "ghostty-ssh-"$ssh-user".*")
|
||||
var ssh-cpath = $ssh-cpath-dir"/socket"
|
||||
|
||||
if (bool ?(echo $ssh-terminfo | (external ssh) $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null)) {
|
||||
set ssh-term = "xterm-ghostty"
|
||||
set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath)
|
||||
|
||||
# Cache successful installation
|
||||
$ghostty +ssh-cache --add=$ssh-target >/dev/null
|
||||
} else {
|
||||
echo "Warning: Failed to install terminfo." >&2
|
||||
}
|
||||
} else {
|
||||
echo "Warning: Could not generate terminfo data." >&2
|
||||
}
|
||||
} else {
|
||||
echo "Warning: ghostty command not available for cache management." >&2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with [E:TERM = $ssh-term E:COLORTERM = truecolor] {
|
||||
(external ssh) $@ssh-opts $@args
|
||||
}
|
||||
}
|
||||
|
||||
defer {
|
||||
mark-prompt-start
|
||||
}
|
||||
|
||||
set edit:before-readline = (conj $edit:before-readline $mark-prompt-start~)
|
||||
set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
|
||||
set edit:after-command = (conj $edit:after-command $mark-output-end~)
|
||||
|
||||
if (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor") {
|
||||
var cursor = "5" # blinking bar
|
||||
if (has-value $features cursor:steady) {
|
||||
set cursor = "6" # steady bar
|
||||
}
|
||||
|
||||
fn beam { printf "\e["$cursor" q" }
|
||||
fn reset { printf "\e[0 q" }
|
||||
set edit:before-readline = (conj $edit:before-readline $beam~)
|
||||
set edit:after-readline = (conj $edit:after-readline {|_| reset })
|
||||
}
|
||||
if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) {
|
||||
if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) {
|
||||
set paths = [$@paths $E:GHOSTTY_BIN_DIR]
|
||||
}
|
||||
}
|
||||
if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) {
|
||||
edit:add-var sudo~ $sudo-with-terminfo~
|
||||
}
|
||||
if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) (has-external ssh)) {
|
||||
edit:add-var ssh~ $ssh-integration~
|
||||
}
|
||||
|
||||
# Report changes to the current directory.
|
||||
fn report-pwd { printf "\e]7;kitty-shell-cwd://%s%s\a" (platform:hostname) $pwd }
|
||||
set after-chdir = (conj $after-chdir {|_| report-pwd })
|
||||
report-pwd
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
# This shell script aims to be written in a way where it can't really fail
|
||||
# or all failure scenarios are handled, so that we never leave the shell in
|
||||
# a weird state. If you find a way to break this, please report a bug!
|
||||
|
||||
function ghostty_restore_xdg_data_dir -d "restore the original XDG_DATA_DIR value"
|
||||
# If we don't have our own data dir then we don't need to do anything.
|
||||
if not set -q GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
return
|
||||
end
|
||||
|
||||
# If the data dir isn't set at all then we don't need to do anything.
|
||||
if not set -q XDG_DATA_DIRS
|
||||
return
|
||||
end
|
||||
|
||||
# We need to do this so that XDG_DATA_DIRS turns into an array.
|
||||
set --function --path xdg_data_dirs "$XDG_DATA_DIRS"
|
||||
|
||||
# If our data dir is in the list then remove it.
|
||||
if set --function index (contains --index "$GHOSTTY_SHELL_INTEGRATION_XDG_DIR" $xdg_data_dirs)
|
||||
set --erase --function xdg_data_dirs[$index]
|
||||
end
|
||||
|
||||
# Re-export our data dir
|
||||
if set -q xdg_data_dirs[1]
|
||||
set --global --export --unpath XDG_DATA_DIRS "$xdg_data_dirs"
|
||||
else
|
||||
set --erase --global XDG_DATA_DIRS
|
||||
end
|
||||
|
||||
set --erase GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
end
|
||||
|
||||
function ghostty_exit -d "exit the shell integration setup"
|
||||
functions -e ghostty_restore_xdg_data_dir
|
||||
functions -e ghostty_exit
|
||||
exit 0
|
||||
end
|
||||
|
||||
# We always try to restore the XDG data dir
|
||||
ghostty_restore_xdg_data_dir
|
||||
|
||||
# If we aren't interactive or we've already run, don't run.
|
||||
status --is-interactive || ghostty_exit
|
||||
|
||||
# We do the full setup on the first prompt render. We do this so that other
|
||||
# shell integrations that setup the prompt and modify things are able to run
|
||||
# first. We want to run _last_.
|
||||
function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
functions -e __ghostty_setup
|
||||
|
||||
set --local features (string split , $GHOSTTY_SHELL_FEATURES)
|
||||
|
||||
# Parse the fish version for feature detection.
|
||||
# Default to 0.0 if version is unavailable or malformed.
|
||||
set -l fish_major 0
|
||||
set -l fish_minor 0
|
||||
if set -q version[1]
|
||||
set -l fish_ver (string match -r '(\d+)\.(\d+)' -- $version[1])
|
||||
if set -q fish_ver[2]; and test -n "$fish_ver[2]"
|
||||
set fish_major "$fish_ver[2]"
|
||||
end
|
||||
if set -q fish_ver[3]; and test -n "$fish_ver[3]"
|
||||
set fish_minor "$fish_ver[3]"
|
||||
end
|
||||
end
|
||||
|
||||
# Our OSC133A (prompt start) sequence. If we're using Fish >= 4.1
|
||||
# then it supports click_events so we enable that.
|
||||
set -g __ghostty_prompt_start_mark "\e]133;A\a"
|
||||
if test "$fish_major" -gt 4; or test "$fish_major" -eq 4 -a "$fish_minor" -ge 1
|
||||
set -g __ghostty_prompt_start_mark "\e]133;A;click_events=1\a"
|
||||
end
|
||||
|
||||
if string match -q 'cursor*' -- $features
|
||||
set -l cursor 5 # blinking bar
|
||||
contains cursor:steady $features && set cursor 6 # steady bar
|
||||
|
||||
# Change the cursor to a beam on prompt.
|
||||
function __ghostty_set_cursor_beam --on-event fish_prompt -V cursor -d "Set cursor shape"
|
||||
if not functions -q fish_vi_cursor_handle
|
||||
echo -en "\e[$cursor q"
|
||||
end
|
||||
end
|
||||
function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape"
|
||||
if not functions -q fish_vi_cursor_handle
|
||||
echo -en "\e[0 q"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add Ghostty binary to PATH if the path feature is enabled
|
||||
if contains path $features; and test -n "$GHOSTTY_BIN_DIR"
|
||||
fish_add_path --global --path --append "$GHOSTTY_BIN_DIR"
|
||||
end
|
||||
|
||||
# When using sudo shell integration feature, ensure $TERMINFO is set
|
||||
# and `sudo` is not already a function or alias
|
||||
if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x")
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
function sudo -d "Wrap sudo to preserve terminfo"
|
||||
set --function sudo_has_sudoedit_flags no
|
||||
for arg in $argv
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if string match -q -- -e "$arg"; or string match -q -- --edit "$arg"
|
||||
set --function sudo_has_sudoedit_flags yes
|
||||
break
|
||||
end
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
if not string match -r -q -- "^-" "$arg"; and not string match -r -q -- "=" "$arg"
|
||||
break
|
||||
end
|
||||
end
|
||||
if test "$sudo_has_sudoedit_flags" = yes
|
||||
command sudo $argv
|
||||
else
|
||||
command sudo --preserve-env=TERMINFO $argv
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# SSH Integration
|
||||
set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES")
|
||||
if contains ssh-env $features; or contains ssh-terminfo $features
|
||||
function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration"
|
||||
set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES")
|
||||
set -l ssh_term xterm-256color
|
||||
set -l ssh_opts
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if contains ssh-env $features
|
||||
set -a ssh_opts -o "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION"
|
||||
end
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if contains ssh-terminfo $features
|
||||
set -l ssh_user
|
||||
set -l ssh_hostname
|
||||
|
||||
for line in (command ssh -G $argv 2>/dev/null)
|
||||
set -l parts (string split ' ' -- $line)
|
||||
if test (count $parts) -ge 2
|
||||
switch $parts[1]
|
||||
case user
|
||||
set ssh_user $parts[2]
|
||||
case hostname
|
||||
set ssh_hostname $parts[2]
|
||||
end
|
||||
if test -n "$ssh_user"; and test -n "$ssh_hostname"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if test -n "$ssh_hostname"
|
||||
set -l ssh_target "$ssh_user@$ssh_hostname"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if test -x "$GHOSTTY_BIN_DIR/ghostty"; and "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1
|
||||
set ssh_term xterm-ghostty
|
||||
else if command -q infocmp
|
||||
set -l ssh_terminfo
|
||||
set -l ssh_cpath_dir
|
||||
set -l ssh_cpath
|
||||
|
||||
set ssh_terminfo "$(infocmp -0 -x xterm-ghostty 2>/dev/null)"
|
||||
|
||||
if test -n "$ssh_terminfo"
|
||||
echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
set ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random))
|
||||
set ssh_cpath "$ssh_cpath_dir/socket"
|
||||
|
||||
if echo "$ssh_terminfo" | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null
|
||||
set ssh_term xterm-ghostty
|
||||
set -a ssh_opts -o "ControlPath=$ssh_cpath"
|
||||
|
||||
# Cache successful installation
|
||||
if test -x "$GHOSTTY_BIN_DIR/ghostty"
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true
|
||||
end
|
||||
else
|
||||
echo "Warning: Failed to install terminfo." >&2
|
||||
end
|
||||
else
|
||||
echo "Warning: Could not generate terminfo data." >&2
|
||||
end
|
||||
else
|
||||
echo "Warning: ghostty command not available for cache management." >&2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" COLORTERM=truecolor command ssh $ssh_opts $argv
|
||||
end
|
||||
end
|
||||
|
||||
# Setup prompt marking
|
||||
function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_posterror
|
||||
# If we never got the output end event, then we need to send it now.
|
||||
if test "$__ghostty_prompt_state" != prompt-start
|
||||
echo -en "\e]133;D\a"
|
||||
end
|
||||
|
||||
set --global __ghostty_prompt_state prompt-start
|
||||
echo -en $__ghostty_prompt_start_mark
|
||||
end
|
||||
|
||||
function __ghostty_mark_output_start --on-event fish_preexec
|
||||
set --global __ghostty_prompt_state pre-exec
|
||||
echo -en "\e]133;C\a"
|
||||
end
|
||||
|
||||
function __ghostty_mark_output_end --on-event fish_postexec
|
||||
set --global __ghostty_prompt_state post-exec
|
||||
echo -en "\e]133;D;$status\a"
|
||||
end
|
||||
|
||||
# Report pwd. This is actually built-in to fish but only for terminals
|
||||
# that match an allowlist and that isn't us.
|
||||
function __update_cwd_osc --on-variable PWD -d 'Notify capable terminals when $PWD changes'
|
||||
if status --is-command-substitution || set -q INSIDE_EMACS
|
||||
return
|
||||
end
|
||||
printf \e\]7\;file://%s%s\a $hostname (string escape --style=url $PWD)
|
||||
end
|
||||
|
||||
# Enable fish to handle reflow because Ghostty clears the prompt on resize.
|
||||
set --global fish_handle_reflow 1
|
||||
|
||||
# Initial calls for first prompt
|
||||
if string match -q 'cursor*' -- $features
|
||||
__ghostty_set_cursor_beam
|
||||
end
|
||||
__ghostty_mark_prompt_start
|
||||
__update_cwd_osc
|
||||
end
|
||||
|
||||
ghostty_exit
|
||||
110
CommandNotch/CommandNotch/Resources/ghostty/shell-integration/nushell/vendor/autoload/ghostty.nu
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
# Ghostty shell integration
|
||||
export module ghostty {
|
||||
def has_feature [feature: string] {
|
||||
$feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',')
|
||||
}
|
||||
|
||||
# Wrap `ssh` with Ghostty TERMINFO support
|
||||
export def --wrapped ssh [...args] {
|
||||
mut ssh_env = {}
|
||||
mut ssh_opts = []
|
||||
|
||||
# `ssh-env`: use xterm-256color and propagate COLORTERM/TERM_PROGRAM vars
|
||||
if (has_feature "ssh-env") {
|
||||
$ssh_env.TERM = "xterm-256color"
|
||||
$ssh_env.COLORTERM = "truecolor"
|
||||
$ssh_opts = [
|
||||
"-o" "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION"
|
||||
]
|
||||
}
|
||||
|
||||
# `ssh-terminfo`: auto-install xterm-ghostty terminfo on remote hosts
|
||||
if (has_feature "ssh-terminfo") {
|
||||
let ghostty = ($env.GHOSTTY_BIN_DIR? | default "") | path join "ghostty"
|
||||
|
||||
let ssh_cfg = ^ssh -G ...$args
|
||||
| lines
|
||||
| parse "{key} {value}"
|
||||
| where key in ["user" "hostname"]
|
||||
| select key value
|
||||
| transpose -rd
|
||||
| default {user: $env.USER hostname: "localhost"}
|
||||
let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)"
|
||||
|
||||
if (^$ghostty "+ssh-cache" $"--host=($ssh_id)" | complete | $in.exit_code == 0) {
|
||||
$ssh_env.TERM = "xterm-ghostty"
|
||||
} else {
|
||||
$ssh_env.TERM = "xterm-256color"
|
||||
|
||||
let terminfo = try {
|
||||
^infocmp -0 -x xterm-ghostty
|
||||
} catch {
|
||||
print -e "infocmp failed, using xterm-256color"
|
||||
}
|
||||
|
||||
if ($terminfo | is-not-empty) {
|
||||
print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..."
|
||||
|
||||
let ctrl_path = (
|
||||
mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX"
|
||||
| path join "socket"
|
||||
)
|
||||
|
||||
let remote_args = $ssh_opts ++ [
|
||||
"-o" "ControlMaster=yes"
|
||||
"-o" $"ControlPath=($ctrl_path)"
|
||||
"-o" "ControlPersist=60s"
|
||||
] ++ $args
|
||||
|
||||
$terminfo | ^ssh ...$remote_args '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1'
|
||||
| complete
|
||||
| if $in.exit_code == 0 {
|
||||
^$ghostty "+ssh-cache" $"--add=($ssh_id)" e>| print -e
|
||||
$ssh_env.TERM = "xterm-ghostty"
|
||||
$ssh_opts = ($ssh_opts ++ ["-o" $"ControlPath=($ctrl_path)"])
|
||||
} else {
|
||||
print -e "terminfo install failed, using xterm-256color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssh_args = $ssh_opts ++ $args
|
||||
with-env $ssh_env {
|
||||
^ssh ...$ssh_args
|
||||
}
|
||||
}
|
||||
|
||||
# Wrap `sudo` to preserve Ghostty's TERMINFO environment variable
|
||||
export def --wrapped sudo [...args] {
|
||||
mut sudo_args = $args
|
||||
|
||||
if (has_feature "sudo") {
|
||||
# Extract just the sudo options (before the command)
|
||||
let sudo_options = (
|
||||
$args | take until {|arg|
|
||||
not (($arg | str starts-with "-") or ($arg | str contains "="))
|
||||
}
|
||||
)
|
||||
|
||||
# Prepend TERMINFO preservation flag if not using sudoedit
|
||||
if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) {
|
||||
$sudo_args = ($args | prepend "--preserve-env=TERMINFO")
|
||||
}
|
||||
}
|
||||
|
||||
^sudo ...$sudo_args
|
||||
}
|
||||
}
|
||||
|
||||
# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
if 'GHOSTTY_SHELL_INTEGRATION_XDG_DIR' in $env {
|
||||
if 'XDG_DATA_DIRS' in $env {
|
||||
$env.XDG_DATA_DIRS = ($env.XDG_DATA_DIRS | str replace $"($env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR):" "")
|
||||
}
|
||||
hide-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# Based on (started as) a copy of Kitty's zsh integration. Kitty is
|
||||
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||
# The license header is reproduced below:
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This script is sourced automatically by zsh when ZDOTDIR is set to this
|
||||
# directory. It therefore assumes it's running within our shell integration
|
||||
# environment and should not be sourced manually (unlike ghostty-integration).
|
||||
#
|
||||
# This file can get sourced with aliases enabled. To avoid alias expansion
|
||||
# we quote everything that can be quoted. Some aliases will still break us
|
||||
# though.
|
||||
|
||||
# Restore the original ZDOTDIR value if GHOSTTY_ZSH_ZDOTDIR is set.
|
||||
# Otherwise, unset the ZDOTDIR that was set during shell injection.
|
||||
if [[ -n "${GHOSTTY_ZSH_ZDOTDIR+X}" ]]; then
|
||||
'builtin' 'export' ZDOTDIR="$GHOSTTY_ZSH_ZDOTDIR"
|
||||
'builtin' 'unset' 'GHOSTTY_ZSH_ZDOTDIR'
|
||||
else
|
||||
'builtin' 'unset' 'ZDOTDIR'
|
||||
fi
|
||||
|
||||
# Use try-always to have the right error code.
|
||||
{
|
||||
# Zsh treats unset ZDOTDIR as if it was HOME. We do the same.
|
||||
#
|
||||
# Source the user's .zshenv before sourcing ghostty-integration because the
|
||||
# former might set fpath and other things without which ghostty-integration
|
||||
# won't work.
|
||||
#
|
||||
# Use typeset in case we are in a function with warn_create_global in
|
||||
# effect. Unlikely but better safe than sorry.
|
||||
'builtin' 'typeset' _ghostty_file=${ZDOTDIR-$HOME}"/.zshenv"
|
||||
# Zsh ignores unreadable rc files. We do the same.
|
||||
# Zsh ignores rc files that are directories, and so does source.
|
||||
[[ ! -r "$_ghostty_file" ]] || 'builtin' 'source' '--' "$_ghostty_file"
|
||||
} always {
|
||||
if [[ -o 'interactive' ]]; then
|
||||
# ${(%):-%x} is the path to the current file.
|
||||
# On top of it we add :A:h to get the directory.
|
||||
'builtin' 'typeset' _ghostty_file="${${(%):-%x}:A:h}"/ghostty-integration
|
||||
if [[ -r "$_ghostty_file" ]]; then
|
||||
'builtin' 'autoload' '-Uz' '--' "$_ghostty_file"
|
||||
"${_ghostty_file:t}"
|
||||
'builtin' 'unfunction' '--' "${_ghostty_file:t}"
|
||||
fi
|
||||
fi
|
||||
'builtin' 'unset' '_ghostty_file'
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
# vim:ft=zsh
|
||||
#
|
||||
# Based on (started as) a copy of Kitty's zsh integration. Kitty is
|
||||
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||
# The license header is reproduced below:
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#
|
||||
# Enables integration between zsh and ghostty.
|
||||
#
|
||||
# This is an autoloadable function. It's invoked automatically in shells
|
||||
# directly spawned by Ghostty but not in any other shells. For example, running
|
||||
# `exec zsh`, `sudo -E zsh`, `tmux`, or plain `zsh` will create a shell where
|
||||
# ghostty-integration won't automatically run. Zsh users who want integration with
|
||||
# Ghostty in all shells should add the following lines to their .zshrc:
|
||||
#
|
||||
# if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
|
||||
# source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
|
||||
# fi
|
||||
#
|
||||
# Implementation note: We can assume that alias expansion is disabled in this
|
||||
# file, so no need to quote defensively. We still have to defensively prefix all
|
||||
# builtins with `builtin` to avoid accidentally invoking user-defined functions.
|
||||
# We avoid `function` reserved word as an additional defensive measure.
|
||||
|
||||
# Note that updating options with `builtin emulate -L zsh` affects the global options
|
||||
# if it's called outside of a function. So nearly all code has to be in functions.
|
||||
_entrypoint() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
[[ -o interactive ]] || builtin return 0 # non-interactive shell
|
||||
(( ! $+_ghostty_state )) || builtin return 0 # already initialized
|
||||
|
||||
# We require zsh 5.1+ (released Sept 2015) for features like functions_source,
|
||||
# introspection arrays, and array pattern substitution.
|
||||
if ! { builtin autoload -- is-at-least 2>/dev/null && is-at-least 5.1; }; then
|
||||
builtin echo "Zsh ${ZSH_VERSION} is too old for ghostty shell integration (5.1+ required)" >&2
|
||||
builtin return 1
|
||||
fi
|
||||
|
||||
# 0: no OSC 133 [AC] marks have been written yet.
|
||||
# 1: the last written OSC 133 C has not been closed with D yet.
|
||||
# 2: none of the above.
|
||||
builtin typeset -gi _ghostty_state
|
||||
|
||||
# Attempt to create a writable file descriptor to the TTY so that we can print
|
||||
# to the TTY later even when STDOUT is redirected. This code is fairly subtle.
|
||||
#
|
||||
# - It's tempting to do `[[ -t 1 ]] && exec {_ghostty_state}>&1` but we cannot do this
|
||||
# because it'll create a file descriptor >= 10 without O_CLOEXEC. This file
|
||||
# descriptor will leak to child processes.
|
||||
# - If we do `exec {3}>&1`, the file descriptor won't leak to the child processes
|
||||
# but it'll still leak if the current process is replaced with another. In
|
||||
# addition, it'll break user code that relies on fd 3 being available.
|
||||
# - Zsh doesn't expose dup3, which would have allowed us to copy STDOUT with
|
||||
# O_CLOEXEC. The only way to create a file descriptor with O_CLOEXEC is via
|
||||
# sysopen.
|
||||
# - `zmodload zsh/system` and `sysopen -o cloexec -wu _ghostty_fd -- /dev/tty` can
|
||||
# fail with an error message to STDERR (the latter can happen even if /dev/tty
|
||||
# is writable), hence the redirection of STDERR. We do it for the whole block
|
||||
# for performance reasons (redirections are slow).
|
||||
# - We must open the file descriptor right here rather than in _ghostty_deferred_init
|
||||
# because there are broken zsh plugins out there that run `exec {fd}< <(cmd)`
|
||||
# and then close the file descriptor more than once while suppressing errors.
|
||||
# This could end up closing our file descriptor if we opened it in
|
||||
# _ghostty_deferred_init.
|
||||
typeset -gi _ghostty_fd
|
||||
{
|
||||
builtin zmodload zsh/system && (( $+builtins[sysopen] )) && {
|
||||
{ [[ -w $TTY ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- $TTY } ||
|
||||
{ [[ -w /dev/tty ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- /dev/tty }
|
||||
}
|
||||
} 2>/dev/null || (( _ghostty_fd = 1 ))
|
||||
|
||||
# Defer initialization so that other zsh init files can be configure
|
||||
# the integration.
|
||||
builtin typeset -ag precmd_functions
|
||||
precmd_functions+=(_ghostty_deferred_init)
|
||||
}
|
||||
|
||||
_ghostty_deferred_init() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
# Enable semantic markup with OSC 133.
|
||||
_ghostty_precmd() {
|
||||
builtin local -i cmd_status=$?
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
# Don't write OSC 133 D when our precmd handler is invoked from zle.
|
||||
# Some plugins do that to update prompt on cd.
|
||||
if ! builtin zle; then
|
||||
# This code works incorrectly in the presence of a precmd or chpwd
|
||||
# hook that prints. For example, sindresorhus/pure prints an empty
|
||||
# line on precmd and marlonrichert/zsh-snap prints $PWD on chpwd.
|
||||
# We'll end up writing our OSC 133 D mark too late.
|
||||
#
|
||||
# Another failure mode is when the output of a command doesn't end
|
||||
# with LF and prompst_sp is set (it is by default). In this case
|
||||
# we'll incorrectly state that '%' from prompt_sp is a part of the
|
||||
# command's output.
|
||||
if (( _ghostty_state == 1 )); then
|
||||
# The last written OSC 133 C has not been closed with D yet.
|
||||
# Close it and supply status.
|
||||
builtin print -nu $_ghostty_fd '\e]133;D;'$cmd_status'\a'
|
||||
(( _ghostty_state = 2 ))
|
||||
elif (( _ghostty_state == 2 )); then
|
||||
# There might be an unclosed OSC 133 C. Close that.
|
||||
builtin print -nu $_ghostty_fd '\e]133;D\a'
|
||||
fi
|
||||
fi
|
||||
|
||||
builtin local mark1=$'%{\e]133;A;cl=line\a%}'
|
||||
if [[ -o prompt_percent ]]; then
|
||||
builtin typeset -g precmd_functions
|
||||
if [[ ${precmd_functions[-1]} == _ghostty_precmd ]]; then
|
||||
# This is the best case for us: we can add our marks to PS1 and
|
||||
# PS2. This way our marks will be printed whenever zsh
|
||||
# redisplays prompt: on reset-prompt, on SIGWINCH, and on
|
||||
# SIGCHLD if notify is set. Themes that update prompt
|
||||
# asynchronously from a `zle -F` handler might still remove our
|
||||
# marks. Oh well.
|
||||
|
||||
# Restore PS1/PS2 to their pre-mark state if nothing else has
|
||||
# modified them since we last added marks. This avoids exposing
|
||||
# PS1 with our marks to other hooks (which can break themes like
|
||||
# Pure that use pattern matching to strip/rebuild the prompt).
|
||||
# If PS1 was modified (by a theme, async update, etc.), we
|
||||
# keep the modified version, prioritizing the theme's changes.
|
||||
builtin local ps1_changed=0
|
||||
if [[ -n ${_ghostty_saved_ps1+x} ]]; then
|
||||
if [[ $PS1 == $_ghostty_marked_ps1 ]]; then
|
||||
PS1=$_ghostty_saved_ps1
|
||||
PS2=$_ghostty_saved_ps2
|
||||
elif [[ $PS1 != $_ghostty_saved_ps1 ]]; then
|
||||
ps1_changed=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save the clean PS1/PS2 before we add marks.
|
||||
_ghostty_saved_ps1=$PS1
|
||||
_ghostty_saved_ps2=$PS2
|
||||
|
||||
# Add our marks. Since we always start from a clean PS1
|
||||
# (either restored above or freshly set by a theme), we can
|
||||
# unconditionally add mark1 and markB.
|
||||
builtin local mark2=$'%{\e]133;P;k=s\a%}'
|
||||
builtin local markB=$'%{\e]133;B\a%}'
|
||||
# If PS1 ends with a bare '%', it combines with the '{'
|
||||
# in markB to form a '%{' prompt escape, swallowing the
|
||||
# marker and producing a visible '{'. Fix by doubling the
|
||||
# trailing '%' so it becomes a literal '%%'.
|
||||
[[ $PS1 == *[^%]% || $PS1 == % ]] && PS1=$PS1%
|
||||
PS1=${mark1}${PS1}${markB}
|
||||
|
||||
# Handle multiline prompts by marking newline-separated
|
||||
# continuation lines with k=s (mark2).
|
||||
#
|
||||
# We skip this when PS1 changed because injecting marks into
|
||||
# newlines can break pattern matching in themes that
|
||||
# strip/rebuild the prompt dynamically (e.g., Pure).
|
||||
if (( ! ps1_changed )) && [[ $PS1 == *$'\n'* ]]; then
|
||||
PS1=${PS1//$'\n'/$'\n'${mark2}}
|
||||
fi
|
||||
|
||||
# PS2 mark is needed when clearing the prompt on resize
|
||||
[[ $PS2 == *[^%]% || $PS2 == % ]] && PS2=$PS2%
|
||||
PS2=${mark2}${PS2}${markB}
|
||||
|
||||
# Save the marked PS1 so we can detect modifications
|
||||
# by other hooks in the next cycle.
|
||||
_ghostty_marked_ps1=$PS1
|
||||
(( _ghostty_state = 2 ))
|
||||
else
|
||||
# If our precmd hook is not the last, we cannot rely on prompt
|
||||
# changes to stick, so we don't even try. At least we can move
|
||||
# our hook to the end to have better luck next time. If there is
|
||||
# another piece of code that wants to take this privileged
|
||||
# position, this won't work well. We'll break them as much as
|
||||
# they are breaking us.
|
||||
precmd_functions=(${precmd_functions:#_ghostty_precmd} _ghostty_precmd)
|
||||
# Plugins that invoke precmd hooks from zle do that before zle
|
||||
# is trashed. This means that the cursor is in the middle of
|
||||
# BUFFER and we cannot print our mark there. Prompt might
|
||||
# already have a mark, so the following reset-prompt will write
|
||||
# it. If it doesn't, there is nothing we can do.
|
||||
if ! builtin zle; then
|
||||
builtin print -rnu $_ghostty_fd -- $mark1[3,-3]
|
||||
(( _ghostty_state = 2 ))
|
||||
fi
|
||||
fi
|
||||
elif ! builtin zle; then
|
||||
# Without prompt_percent we cannot patch prompt. Just print the
|
||||
# mark, except when we are invoked from zle. In the latter case we
|
||||
# cannot do anything.
|
||||
builtin print -rnu $_ghostty_fd -- $mark1[3,-3]
|
||||
(( _ghostty_state = 2 ))
|
||||
fi
|
||||
}
|
||||
|
||||
_ghostty_preexec() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
# Restore the original PS1/PS2 if nothing else has modified them
|
||||
# since our precmd added marks. This ensures other preexec hooks
|
||||
# see a clean PS1 without our marks. If PS1 was modified (e.g.,
|
||||
# by an async theme update), we leave it alone.
|
||||
if [[ -n ${_ghostty_saved_ps1+x} && $PS1 == $_ghostty_marked_ps1 ]]; then
|
||||
PS1=$_ghostty_saved_ps1
|
||||
PS2=$_ghostty_saved_ps2
|
||||
fi
|
||||
|
||||
# This will work incorrectly in the presence of a preexec hook that
|
||||
# prints. For example, if MichaelAquilina/zsh-you-should-use installs
|
||||
# its preexec hook before us, we'll incorrectly mark its output as
|
||||
# belonging to the command (as if the user typed it into zle) rather
|
||||
# than command output.
|
||||
builtin print -nu $_ghostty_fd '\e]133;C\a'
|
||||
(( _ghostty_state = 1 ))
|
||||
}
|
||||
|
||||
# Enable reporting current working dir to terminal. Ghostty supports
|
||||
# the kitty-shell-cwd format.
|
||||
_ghostty_report_pwd() { builtin print -nu $_ghostty_fd '\e]7;kitty-shell-cwd://'"$HOST""$PWD"'\a'; }
|
||||
chpwd_functions=(${chpwd_functions[@]} "_ghostty_report_pwd")
|
||||
# An executed program could change cwd and report the changed cwd, so also report cwd at each new prompt
|
||||
# as in this case chpwd_functions is insufficient. chpwd_functions is still needed for things like: cd x && something
|
||||
functions[_ghostty_precmd]+="
|
||||
_ghostty_report_pwd"
|
||||
_ghostty_report_pwd
|
||||
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
# Enable terminal title changes, formatted for user-friendly display.
|
||||
functions[_ghostty_precmd]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'"
|
||||
functions[_ghostty_preexec]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${1//[[:cntrl:]]}\"\$'\\a'"
|
||||
fi
|
||||
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
# Enable cursor shape changes depending on the current keymap.
|
||||
# This implementation leaks blinking block cursor into external commands
|
||||
# executed from zle. For example, users of fzf-based widgets may find
|
||||
# themselves with a blinking block cursor within fzf.
|
||||
_ghostty_zle_line_init _ghostty_zle_line_finish _ghostty_zle_keymap_select() {
|
||||
builtin local steady=0
|
||||
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && steady=1
|
||||
case ${KEYMAP-} in
|
||||
vicmd|visual) builtin print -nu "$_ghostty_fd" "\e[$(( 1 + steady )) q" ;; # block
|
||||
*) builtin print -nu "$_ghostty_fd" "\e[$(( 5 + steady )) q" ;; # bar
|
||||
esac
|
||||
}
|
||||
# Restore the default shape before executing an external command
|
||||
functions[_ghostty_preexec]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e[0 q'"
|
||||
fi
|
||||
|
||||
# Emit semantic prompt markers at line-init if PS1 doesn't contain our
|
||||
# marks. This ensures the terminal sees prompt markers even if another
|
||||
# plugin (like zinit or oh-my-posh) regenerated PS1 after our precmd ran.
|
||||
# We use 133;P instead of 133;A to avoid fresh-line behavior which would
|
||||
# disrupt the display since the prompt has already been drawn. We also
|
||||
# emit 133;B to mark the input area, which is needed for click-to-move.
|
||||
(( $+functions[_ghostty_zle_line_init] )) || _ghostty_zle_line_init() { builtin true; }
|
||||
functions[_ghostty_zle_line_init]="
|
||||
if [[ \$PS1 != *$'%{\\e]133;A'* ]]; then
|
||||
builtin print -nu \$_ghostty_fd '\\e]133;P;k=i\\a\\e]133;B\\a'
|
||||
fi
|
||||
"${functions[_ghostty_zle_line_init]}
|
||||
|
||||
# Add Ghostty binary to PATH if the path feature is enabled
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* ]] && [[ -n "$GHOSTTY_BIN_DIR" ]]; then
|
||||
if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then
|
||||
builtin export PATH="$PATH:$GHOSTTY_BIN_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sudo
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* ]] && [[ -n "$TERMINFO" ]]; then
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
function sudo() {
|
||||
builtin local sudo_has_sudoedit_flags="no"
|
||||
for arg in "$@"; do
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
|
||||
sudo_has_sudoedit_flags="yes"
|
||||
builtin break
|
||||
fi
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
if [[ "$arg" != -* && "$arg" != *=* ]]; then
|
||||
builtin break
|
||||
fi
|
||||
done
|
||||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@";
|
||||
else
|
||||
builtin command sudo --preserve-env=TERMINFO "$@";
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# SSH Integration
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
function ssh() {
|
||||
emulate -L zsh
|
||||
setopt local_options no_glob_subst
|
||||
|
||||
local ssh_term ssh_opts
|
||||
ssh_term="xterm-256color"
|
||||
ssh_opts=()
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then
|
||||
ssh_opts+=(-o "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
fi
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then
|
||||
local ssh_user ssh_hostname
|
||||
|
||||
while IFS=' ' read -r ssh_key ssh_value; do
|
||||
case "$ssh_key" in
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
esac
|
||||
[[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break
|
||||
done < <(command ssh -G "$@" 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_hostname" ]]; then
|
||||
local ssh_target="${ssh_user}@${ssh_hostname}"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then
|
||||
ssh_term="xterm-ghostty"
|
||||
elif (( $+commands[infocmp] )); then
|
||||
local ssh_terminfo ssh_cpath_dir ssh_cpath
|
||||
|
||||
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_terminfo" ]]; then
|
||||
print "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$"
|
||||
ssh_cpath="$ssh_cpath_dir/socket"
|
||||
|
||||
if builtin print -r "$ssh_terminfo" | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null; then
|
||||
ssh_term="xterm-ghostty"
|
||||
ssh_opts+=(-o "ControlPath=$ssh_cpath")
|
||||
|
||||
# Cache successful installation
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true
|
||||
else
|
||||
print "Warning: Failed to install terminfo." >&2
|
||||
fi
|
||||
else
|
||||
print "Warning: Could not generate terminfo data." >&2
|
||||
fi
|
||||
else
|
||||
print "Warning: ghostty command not available for cache management." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" COLORTERM=truecolor command ssh "${ssh_opts[@]}" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
# Some zsh users manually run `source ~/.zshrc` in order to apply rc file
|
||||
# changes to the current shell. This is a terrible practice that breaks many
|
||||
# things, including our shell integration. For example, Oh My Zsh and Prezto
|
||||
# (both very popular among zsh users) will remove zle-line-init and
|
||||
# zle-line-finish hooks if .zshrc is manually sourced. Prezto will also remove
|
||||
# zle-keymap-select.
|
||||
#
|
||||
# Another common (and much more robust) way to apply rc file changes to the
|
||||
# current shell is `exec zsh`. This will remove our integration from the shell
|
||||
# unless it's explicitly invoked from .zshrc. This is not an issue with
|
||||
# `exec zsh` but rather with our implementation of automatic shell integration.
|
||||
|
||||
# In the ideal world we would use add-zle-hook-widget to hook zle-line-init
|
||||
# and similar widget. This breaks user configs though, so we have do this
|
||||
# horrible thing instead.
|
||||
builtin local hook func widget orig_widget flag
|
||||
for hook in line-init line-finish keymap-select; do
|
||||
func=_ghostty_zle_${hook/-/_}
|
||||
(( $+functions[$func] )) || builtin continue
|
||||
widget=zle-$hook
|
||||
if [[ $widgets[$widget] == user:azhw:* &&
|
||||
$+functions[add-zle-hook-widget] -eq 1 ]]; then
|
||||
# If the widget is already hooked by add-zle-hook-widget at the top
|
||||
# level, add our hook at the end. We MUST do it this way. We cannot
|
||||
# just wrap the widget ourselves in this case because it would
|
||||
# trigger bugs in add-zle-hook-widget.
|
||||
add-zle-hook-widget $hook $func
|
||||
else
|
||||
if (( $+widgets[$widget] )); then
|
||||
# There is a widget but it's not from add-zle-hook-widget. We
|
||||
# can rename the original widget, install our own and invoke
|
||||
# the original when we are called.
|
||||
#
|
||||
# Note: The leading dot is to work around bugs in
|
||||
# zsh-syntax-highlighting.
|
||||
orig_widget=._ghostty_orig_$widget
|
||||
builtin zle -A $widget $orig_widget
|
||||
if [[ $widgets[$widget] == user:* ]]; then
|
||||
# No -w here to preserve $WIDGET within the original widget.
|
||||
flag=
|
||||
else
|
||||
flag=w
|
||||
fi
|
||||
functions[$func]+="
|
||||
builtin zle $orig_widget -N$flag -- \"\$@\""
|
||||
fi
|
||||
builtin zle -N $widget $func
|
||||
fi
|
||||
done
|
||||
|
||||
if (( $+functions[_ghostty_preexec] )); then
|
||||
builtin typeset -ag preexec_functions
|
||||
preexec_functions+=(_ghostty_preexec)
|
||||
fi
|
||||
|
||||
builtin typeset -ag precmd_functions
|
||||
if (( $+functions[_ghostty_precmd] )); then
|
||||
precmd_functions=(${precmd_functions:#_ghostty_deferred_init} _ghostty_precmd)
|
||||
_ghostty_precmd
|
||||
else
|
||||
precmd_functions=(${precmd_functions:#_ghostty_deferred_init})
|
||||
fi
|
||||
|
||||
# Unfunction _ghostty_deferred_init to save memory. Don't unfunction
|
||||
# ghostty-integration though because decent public functions aren't supposed to
|
||||
# to unfunction themselves when invoked. Unfunctioning is done by calling code.
|
||||
builtin unfunction _ghostty_deferred_init
|
||||
}
|
||||
|
||||
_entrypoint
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/0x96f
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#262427
|
||||
palette = 1=#ff666d
|
||||
palette = 2=#b3e03a
|
||||
palette = 3=#ffc739
|
||||
palette = 4=#00cde8
|
||||
palette = 5=#a392e8
|
||||
palette = 6=#9deaf6
|
||||
palette = 7=#fcfcfa
|
||||
palette = 8=#545452
|
||||
palette = 9=#ff7e83
|
||||
palette = 10=#bee55e
|
||||
palette = 11=#ffd05e
|
||||
palette = 12=#1bd5eb
|
||||
palette = 13=#b0a3eb
|
||||
palette = 14=#acedf8
|
||||
palette = 15=#fcfcfa
|
||||
background = #262427
|
||||
foreground = #fcfcfa
|
||||
cursor-color = #fcfcfa
|
||||
cursor-text = #000000
|
||||
selection-background = #fcfcfa
|
||||
selection-foreground = #262427
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#000000
|
||||
palette = 1=#a03050
|
||||
palette = 2=#40d080
|
||||
palette = 3=#e09040
|
||||
palette = 4=#3060b0
|
||||
palette = 5=#603090
|
||||
palette = 6=#0090c0
|
||||
palette = 7=#dbded8
|
||||
palette = 8=#685656
|
||||
palette = 9=#c06060
|
||||
palette = 10=#90d050
|
||||
palette = 11=#e0d000
|
||||
palette = 12=#00b0c0
|
||||
palette = 13=#801070
|
||||
palette = 14=#20b0c0
|
||||
palette = 15=#ffffff
|
||||
background = #040404
|
||||
foreground = #feffff
|
||||
cursor-color = #e0d000
|
||||
cursor-text = #000000
|
||||
selection-background = #606060
|
||||
selection-foreground = #ffffff
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/3024 Day
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#090300
|
||||
palette = 1=#db2d20
|
||||
palette = 2=#01a252
|
||||
palette = 3=#caba00
|
||||
palette = 4=#01a0e4
|
||||
palette = 5=#a16a94
|
||||
palette = 6=#8fbece
|
||||
palette = 7=#a5a2a2
|
||||
palette = 8=#5c5855
|
||||
palette = 9=#dbaec3
|
||||
palette = 10=#3a3432
|
||||
palette = 11=#4a4543
|
||||
palette = 12=#807d7c
|
||||
palette = 13=#bcbbba
|
||||
palette = 14=#cdab53
|
||||
palette = 15=#f7f7f7
|
||||
background = #f7f7f7
|
||||
foreground = #4a4543
|
||||
cursor-color = #4a4543
|
||||
cursor-text = #f7f7f7
|
||||
selection-background = #a5a2a2
|
||||
selection-foreground = #4a4543
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#090300
|
||||
palette = 1=#db2d20
|
||||
palette = 2=#01a252
|
||||
palette = 3=#fded02
|
||||
palette = 4=#01a0e4
|
||||
palette = 5=#a16a94
|
||||
palette = 6=#b5e4f4
|
||||
palette = 7=#a5a2a2
|
||||
palette = 8=#5c5855
|
||||
palette = 9=#e8bbd0
|
||||
palette = 10=#47413f
|
||||
palette = 11=#4a4543
|
||||
palette = 12=#807d7c
|
||||
palette = 13=#d6d5d4
|
||||
palette = 14=#cdab53
|
||||
palette = 15=#f7f7f7
|
||||
background = #090300
|
||||
foreground = #a5a2a2
|
||||
cursor-color = #a5a2a2
|
||||
cursor-text = #090300
|
||||
selection-background = #4a4543
|
||||
selection-foreground = #a5a2a2
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#191919
|
||||
palette = 1=#aa342e
|
||||
palette = 2=#4b8c0f
|
||||
palette = 3=#dbba00
|
||||
palette = 4=#1370d3
|
||||
palette = 5=#c43ac3
|
||||
palette = 6=#008eb0
|
||||
palette = 7=#bebebe
|
||||
palette = 8=#525252
|
||||
palette = 9=#f05b50
|
||||
palette = 10=#95dc55
|
||||
palette = 11=#ffe763
|
||||
palette = 12=#60a4ec
|
||||
palette = 13=#e26be2
|
||||
palette = 14=#60b6cb
|
||||
palette = 15=#f7f7f7
|
||||
background = #102040
|
||||
foreground = #dddddd
|
||||
cursor-color = #007acc
|
||||
cursor-text = #bfdbfe
|
||||
selection-background = #bfdbfe
|
||||
selection-foreground = #000000
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Abernathy
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#000000
|
||||
palette = 1=#cd0000
|
||||
palette = 2=#00cd00
|
||||
palette = 3=#cdcd00
|
||||
palette = 4=#1093f5
|
||||
palette = 5=#cd00cd
|
||||
palette = 6=#00cdcd
|
||||
palette = 7=#faebd7
|
||||
palette = 8=#404040
|
||||
palette = 9=#ff0000
|
||||
palette = 10=#00ff00
|
||||
palette = 11=#ffff00
|
||||
palette = 12=#11b5f6
|
||||
palette = 13=#ff00ff
|
||||
palette = 14=#00ffff
|
||||
palette = 15=#ffffff
|
||||
background = #111416
|
||||
foreground = #eeeeec
|
||||
cursor-color = #bbbbbb
|
||||
cursor-text = #ffffff
|
||||
selection-background = #eeeeec
|
||||
selection-foreground = #333333
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Adventure
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#040404
|
||||
palette = 1=#d84a33
|
||||
palette = 2=#5da602
|
||||
palette = 3=#eebb6e
|
||||
palette = 4=#417ab3
|
||||
palette = 5=#e5c499
|
||||
palette = 6=#bdcfe5
|
||||
palette = 7=#dbded8
|
||||
palette = 8=#685656
|
||||
palette = 9=#d76b42
|
||||
palette = 10=#99b52c
|
||||
palette = 11=#ffb670
|
||||
palette = 12=#97d7ef
|
||||
palette = 13=#aa7900
|
||||
palette = 14=#bdcfe5
|
||||
palette = 15=#e4d5c7
|
||||
background = #040404
|
||||
foreground = #feffff
|
||||
cursor-color = #feffff
|
||||
cursor-text = #000000
|
||||
selection-background = #606060
|
||||
selection-foreground = #ffffff
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#050404
|
||||
palette = 1=#bd0013
|
||||
palette = 2=#4ab118
|
||||
palette = 3=#e7741e
|
||||
palette = 4=#0f4ac6
|
||||
palette = 5=#665993
|
||||
palette = 6=#70a598
|
||||
palette = 7=#f8dcc0
|
||||
palette = 8=#4e7cbf
|
||||
palette = 9=#fc5f5a
|
||||
palette = 10=#9eff6e
|
||||
palette = 11=#efc11a
|
||||
palette = 12=#1997c6
|
||||
palette = 13=#9b5953
|
||||
palette = 14=#c8faf4
|
||||
palette = 15=#f6f5fb
|
||||
background = #1f1d45
|
||||
foreground = #f8dcc0
|
||||
cursor-color = #efbf38
|
||||
cursor-text = #08080a
|
||||
selection-background = #706b4e
|
||||
selection-foreground = #f3d9c4
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Adwaita
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#241f31
|
||||
palette = 1=#c01c28
|
||||
palette = 2=#2ec27e
|
||||
palette = 3=#e8b504
|
||||
palette = 4=#1e78e4
|
||||
palette = 5=#9841bb
|
||||
palette = 6=#0ab9dc
|
||||
palette = 7=#c0bfbc
|
||||
palette = 8=#5e5c64
|
||||
palette = 9=#ed333b
|
||||
palette = 10=#4ad67c
|
||||
palette = 11=#d2be36
|
||||
palette = 12=#51a1ff
|
||||
palette = 13=#c061cb
|
||||
palette = 14=#4fd2fd
|
||||
palette = 15=#f6f5f4
|
||||
background = #ffffff
|
||||
foreground = #000000
|
||||
cursor-color = #000000
|
||||
cursor-text = #ffffff
|
||||
selection-background = #c0bfbc
|
||||
selection-foreground = #000000
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#241f31
|
||||
palette = 1=#c01c28
|
||||
palette = 2=#2ec27e
|
||||
palette = 3=#f5c211
|
||||
palette = 4=#1e78e4
|
||||
palette = 5=#9841bb
|
||||
palette = 6=#0ab9dc
|
||||
palette = 7=#c0bfbc
|
||||
palette = 8=#5e5c64
|
||||
palette = 9=#ed333b
|
||||
palette = 10=#57e389
|
||||
palette = 11=#f8e45c
|
||||
palette = 12=#51a1ff
|
||||
palette = 13=#c061cb
|
||||
palette = 14=#4fd2fd
|
||||
palette = 15=#f6f5f4
|
||||
background = #1d1d20
|
||||
foreground = #ffffff
|
||||
cursor-color = #ffffff
|
||||
cursor-text = #1d1d20
|
||||
selection-background = #ffffff
|
||||
selection-foreground = #5e5c64
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Afterglow
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#151515
|
||||
palette = 1=#ac4142
|
||||
palette = 2=#7e8e50
|
||||
palette = 3=#e5b567
|
||||
palette = 4=#6c99bb
|
||||
palette = 5=#9f4e85
|
||||
palette = 6=#7dd6cf
|
||||
palette = 7=#d0d0d0
|
||||
palette = 8=#505050
|
||||
palette = 9=#ac4142
|
||||
palette = 10=#7e8e50
|
||||
palette = 11=#e5b567
|
||||
palette = 12=#6c99bb
|
||||
palette = 13=#9f4e85
|
||||
palette = 14=#7dd6cf
|
||||
palette = 15=#f5f5f5
|
||||
background = #212121
|
||||
foreground = #d0d0d0
|
||||
cursor-color = #d0d0d0
|
||||
cursor-text = #151515
|
||||
selection-background = #303030
|
||||
selection-foreground = #d0d0d0
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#1a1a1a
|
||||
palette = 1=#f08898
|
||||
palette = 2=#a4e09c
|
||||
palette = 3=#f5dea4
|
||||
palette = 4=#84b4f8
|
||||
palette = 5=#c8a2f4
|
||||
palette = 6=#90dcd0
|
||||
palette = 7=#d0d6f0
|
||||
palette = 8=#444444
|
||||
palette = 9=#f08898
|
||||
palette = 10=#a4e09c
|
||||
palette = 11=#f5dea4
|
||||
palette = 12=#84b4f8
|
||||
palette = 13=#c8a2f4
|
||||
palette = 14=#90dcd0
|
||||
palette = 15=#ffffff
|
||||
background = #1a1a1a
|
||||
foreground = #d0d6f0
|
||||
cursor-color = #f8b080
|
||||
cursor-text = #1a1a1a
|
||||
selection-background = #333333
|
||||
selection-foreground = #d0d6f0
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#f0f2f6
|
||||
palette = 1=#d00c36
|
||||
palette = 2=#3e9e28
|
||||
palette = 3=#dd8c1a
|
||||
palette = 4=#1c64f2
|
||||
palette = 5=#8636ec
|
||||
palette = 6=#159096
|
||||
palette = 7=#4a4d66
|
||||
palette = 8=#adb2bc
|
||||
palette = 9=#d00c36
|
||||
palette = 10=#3e9e28
|
||||
palette = 11=#dd8c1a
|
||||
palette = 12=#1c64f2
|
||||
palette = 13=#8636ec
|
||||
palette = 14=#159096
|
||||
palette = 15=#4a4d66
|
||||
background = #f0f2f6
|
||||
foreground = #4a4d66
|
||||
cursor-color = #fc6008
|
||||
cursor-text = #f0f2f6
|
||||
selection-background = #bdc2cc
|
||||
selection-foreground = #4a4d66
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Alabaster
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#000000
|
||||
palette = 1=#aa3731
|
||||
palette = 2=#448c27
|
||||
palette = 3=#cb9000
|
||||
palette = 4=#325cc0
|
||||
palette = 5=#7a3e9d
|
||||
palette = 6=#0083b2
|
||||
palette = 7=#b7b7b7
|
||||
palette = 8=#777777
|
||||
palette = 9=#f05050
|
||||
palette = 10=#60cb00
|
||||
palette = 11=#f2af50
|
||||
palette = 12=#007acc
|
||||
palette = 13=#e64ce6
|
||||
palette = 14=#00aacb
|
||||
palette = 15=#f7f7f7
|
||||
background = #f7f7f7
|
||||
foreground = #000000
|
||||
cursor-color = #007acc
|
||||
cursor-text = #bfdbfe
|
||||
selection-background = #bfdbfe
|
||||
selection-foreground = #000000
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#112616
|
||||
palette = 1=#7f2b27
|
||||
palette = 2=#2f7e25
|
||||
palette = 3=#717f24
|
||||
palette = 4=#2f6a7f
|
||||
palette = 5=#47587f
|
||||
palette = 6=#327f77
|
||||
palette = 7=#647d75
|
||||
palette = 8=#3c4812
|
||||
palette = 9=#e08009
|
||||
palette = 10=#18e000
|
||||
palette = 11=#bde000
|
||||
palette = 12=#00aae0
|
||||
palette = 13=#0058e0
|
||||
palette = 14=#00e0c4
|
||||
palette = 15=#73fa91
|
||||
background = #0f1610
|
||||
foreground = #637d75
|
||||
cursor-color = #73fa91
|
||||
cursor-text = #0f1610
|
||||
selection-background = #1d4125
|
||||
selection-foreground = #73fa91
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Andromeda
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#000000
|
||||
palette = 1=#cd3131
|
||||
palette = 2=#05bc79
|
||||
palette = 3=#e5e512
|
||||
palette = 4=#2472c8
|
||||
palette = 5=#bc3fbc
|
||||
palette = 6=#0fa8cd
|
||||
palette = 7=#e5e5e5
|
||||
palette = 8=#666666
|
||||
palette = 9=#cd3131
|
||||
palette = 10=#05bc79
|
||||
palette = 11=#e5e512
|
||||
palette = 12=#2472c8
|
||||
palette = 13=#bc3fbc
|
||||
palette = 14=#0fa8cd
|
||||
palette = 15=#e5e5e5
|
||||
background = #262a33
|
||||
foreground = #e5e5e5
|
||||
cursor-color = #f8f8f0
|
||||
cursor-text = #b5b5a8
|
||||
selection-background = #5a5c62
|
||||
selection-foreground = #ece7e7
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#000000
|
||||
palette = 1=#c91b00
|
||||
palette = 2=#00c200
|
||||
palette = 3=#c7c400
|
||||
palette = 4=#1c3fe1
|
||||
palette = 5=#ca30c7
|
||||
palette = 6=#00c5c7
|
||||
palette = 7=#c7c7c7
|
||||
palette = 8=#686868
|
||||
palette = 9=#ff6e67
|
||||
palette = 10=#5ffa68
|
||||
palette = 11=#fffc67
|
||||
palette = 12=#6871ff
|
||||
palette = 13=#ff77ff
|
||||
palette = 14=#60fdff
|
||||
palette = 15=#ffffff
|
||||
background = #2c2b2b
|
||||
foreground = #d5a200
|
||||
cursor-color = #c7c7c7
|
||||
cursor-text = #8c8c8c
|
||||
selection-background = #6b5b02
|
||||
selection-foreground = #67e000
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#1a1a1a
|
||||
palette = 1=#cc372e
|
||||
palette = 2=#26a439
|
||||
palette = 3=#cdac08
|
||||
palette = 4=#0869cb
|
||||
palette = 5=#9647bf
|
||||
palette = 6=#479ec2
|
||||
palette = 7=#98989d
|
||||
palette = 8=#464646
|
||||
palette = 9=#ff453a
|
||||
palette = 10=#32d74b
|
||||
palette = 11=#ffd60a
|
||||
palette = 12=#0a84ff
|
||||
palette = 13=#bf5af2
|
||||
palette = 14=#76d6ff
|
||||
palette = 15=#ffffff
|
||||
background = #1e1e1e
|
||||
foreground = #ffffff
|
||||
cursor-color = #98989d
|
||||
cursor-text = #ffffff
|
||||
selection-background = #3f638b
|
||||
selection-foreground = #ffffff
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#1a1a1a
|
||||
palette = 1=#cc372e
|
||||
palette = 2=#26a439
|
||||
palette = 3=#cdac08
|
||||
palette = 4=#0869cb
|
||||
palette = 5=#9647bf
|
||||
palette = 6=#479ec2
|
||||
palette = 7=#98989d
|
||||
palette = 8=#464646
|
||||
palette = 9=#ff453a
|
||||
palette = 10=#32d74b
|
||||
palette = 11=#e5bc00
|
||||
palette = 12=#0a84ff
|
||||
palette = 13=#bf5af2
|
||||
palette = 14=#69c9f2
|
||||
palette = 15=#ffffff
|
||||
background = #feffff
|
||||
foreground = #000000
|
||||
cursor-color = #98989d
|
||||
cursor-text = #ffffff
|
||||
selection-background = #abd8ff
|
||||
selection-foreground = #000000
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Arcoiris
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#333333
|
||||
palette = 1=#da2700
|
||||
palette = 2=#12c258
|
||||
palette = 3=#ffc656
|
||||
palette = 4=#518bfc
|
||||
palette = 5=#e37bd9
|
||||
palette = 6=#63fad5
|
||||
palette = 7=#bab2b2
|
||||
palette = 8=#777777
|
||||
palette = 9=#ffb9b9
|
||||
palette = 10=#e3f6aa
|
||||
palette = 11=#ffddaa
|
||||
palette = 12=#b3e8f3
|
||||
palette = 13=#cbbaf9
|
||||
palette = 14=#bcffc7
|
||||
palette = 15=#efefef
|
||||
background = #201f1e
|
||||
foreground = #eee4d9
|
||||
cursor-color = #872929
|
||||
cursor-text = #fffbf2
|
||||
selection-background = #25524a
|
||||
selection-foreground = #f3fffd
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Ardoise
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#2c2c2c
|
||||
palette = 1=#d3322d
|
||||
palette = 2=#588b35
|
||||
palette = 3=#fca93a
|
||||
palette = 4=#2465c2
|
||||
palette = 5=#7332b4
|
||||
palette = 6=#64e1b8
|
||||
palette = 7=#f7f7f7
|
||||
palette = 8=#535353
|
||||
palette = 9=#fa5852
|
||||
palette = 10=#8dc252
|
||||
palette = 11=#ffea51
|
||||
palette = 12=#6ab5f8
|
||||
palette = 13=#be68ca
|
||||
palette = 14=#89ffdb
|
||||
palette = 15=#fefefe
|
||||
background = #1e1e1e
|
||||
foreground = #eaeaea
|
||||
cursor-color = #f7f7f7
|
||||
cursor-text = #000000
|
||||
selection-background = #46515e
|
||||
selection-foreground = #f1f3f5
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Argonaut
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#232323
|
||||
palette = 1=#ff000f
|
||||
palette = 2=#8ce10b
|
||||
palette = 3=#ffb900
|
||||
palette = 4=#008df8
|
||||
palette = 5=#6d43a6
|
||||
palette = 6=#00d8eb
|
||||
palette = 7=#ffffff
|
||||
palette = 8=#444444
|
||||
palette = 9=#ff2740
|
||||
palette = 10=#abe15b
|
||||
palette = 11=#ffd242
|
||||
palette = 12=#0092ff
|
||||
palette = 13=#9a5feb
|
||||
palette = 14=#67fff0
|
||||
palette = 15=#ffffff
|
||||
background = #0e1019
|
||||
foreground = #fffaf4
|
||||
cursor-color = #ff0018
|
||||
cursor-text = #ff8ca4
|
||||
selection-background = #002a3b
|
||||
selection-foreground = #ffffff
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Arthur
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#3d352a
|
||||
palette = 1=#cd5c5c
|
||||
palette = 2=#86af80
|
||||
palette = 3=#e8ae5b
|
||||
palette = 4=#6495ed
|
||||
palette = 5=#deb887
|
||||
palette = 6=#b0c4de
|
||||
palette = 7=#bbaa99
|
||||
palette = 8=#554444
|
||||
palette = 9=#cc5533
|
||||
palette = 10=#88aa22
|
||||
palette = 11=#ffa75d
|
||||
palette = 12=#87ceeb
|
||||
palette = 13=#996600
|
||||
palette = 14=#b0c4de
|
||||
palette = 15=#ddccbb
|
||||
background = #1c1c1c
|
||||
foreground = #ddeedd
|
||||
cursor-color = #e2bbef
|
||||
cursor-text = #000000
|
||||
selection-background = #4d4d4d
|
||||
selection-foreground = #ffffff
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#202746
|
||||
palette = 1=#c94922
|
||||
palette = 2=#ac9739
|
||||
palette = 3=#c08b30
|
||||
palette = 4=#3d8fd1
|
||||
palette = 5=#6679cc
|
||||
palette = 6=#22a2c9
|
||||
palette = 7=#979db4
|
||||
palette = 8=#6b7394
|
||||
palette = 9=#c76b29
|
||||
palette = 10=#4f587c
|
||||
palette = 11=#5e6687
|
||||
palette = 12=#898ea4
|
||||
palette = 13=#dfe2f1
|
||||
palette = 14=#9c637a
|
||||
palette = 15=#f5f7ff
|
||||
background = #202746
|
||||
foreground = #979db4
|
||||
cursor-color = #979db4
|
||||
cursor-text = #202746
|
||||
selection-background = #5e6687
|
||||
selection-foreground = #979db4
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Atom
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#000000
|
||||
palette = 1=#fd5ff1
|
||||
palette = 2=#87c38a
|
||||
palette = 3=#ffd7b1
|
||||
palette = 4=#85befd
|
||||
palette = 5=#b9b6fc
|
||||
palette = 6=#85befd
|
||||
palette = 7=#e0e0e0
|
||||
palette = 8=#4c4c4c
|
||||
palette = 9=#fd5ff1
|
||||
palette = 10=#94fa36
|
||||
palette = 11=#f5ffa8
|
||||
palette = 12=#96cbfe
|
||||
palette = 13=#b9b6fc
|
||||
palette = 14=#85befd
|
||||
palette = 15=#e0e0e0
|
||||
background = #161719
|
||||
foreground = #c5c8c6
|
||||
cursor-color = #d0d0d0
|
||||
cursor-text = #151515
|
||||
selection-background = #444444
|
||||
selection-foreground = #c5c8c6
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#21252b
|
||||
palette = 1=#e06c75
|
||||
palette = 2=#98c379
|
||||
palette = 3=#e5c07b
|
||||
palette = 4=#61afef
|
||||
palette = 5=#c678dd
|
||||
palette = 6=#56b6c2
|
||||
palette = 7=#abb2bf
|
||||
palette = 8=#767676
|
||||
palette = 9=#e06c75
|
||||
palette = 10=#98c379
|
||||
palette = 11=#e5c07b
|
||||
palette = 12=#61afef
|
||||
palette = 13=#c678dd
|
||||
palette = 14=#56b6c2
|
||||
palette = 15=#abb2bf
|
||||
background = #21252b
|
||||
foreground = #abb2bf
|
||||
cursor-color = #abb2bf
|
||||
cursor-text = #21252b
|
||||
selection-background = #323844
|
||||
selection-foreground = #abb2bf
|
||||
@@ -0,0 +1,22 @@
|
||||
palette = 0=#000000
|
||||
palette = 1=#de3e35
|
||||
palette = 2=#3f953a
|
||||
palette = 3=#d2b67c
|
||||
palette = 4=#2f5af3
|
||||
palette = 5=#950095
|
||||
palette = 6=#3f953a
|
||||
palette = 7=#bbbbbb
|
||||
palette = 8=#000000
|
||||
palette = 9=#de3e35
|
||||
palette = 10=#3f953a
|
||||
palette = 11=#d2b67c
|
||||
palette = 12=#2f5af3
|
||||
palette = 13=#a00095
|
||||
palette = 14=#3f953a
|
||||
palette = 15=#ffffff
|
||||
background = #f9f9f9
|
||||
foreground = #2a2c33
|
||||
cursor-color = #bbbbbb
|
||||
cursor-text = #ffffff
|
||||
selection-background = #ededed
|
||||
selection-foreground = #2a2c33
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Aura
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#110f18
|
||||
palette = 1=#ff6767
|
||||
palette = 2=#61ffca
|
||||
palette = 3=#ffca85
|
||||
palette = 4=#a277ff
|
||||
palette = 5=#a277ff
|
||||
palette = 6=#61ffca
|
||||
palette = 7=#edecee
|
||||
palette = 8=#4d4d4d
|
||||
palette = 9=#ffca85
|
||||
palette = 10=#a277ff
|
||||
palette = 11=#ffca85
|
||||
palette = 12=#a277ff
|
||||
palette = 13=#a277ff
|
||||
palette = 14=#61ffca
|
||||
palette = 15=#edecee
|
||||
background = #15141b
|
||||
foreground = #edecee
|
||||
cursor-color = #a277ff
|
||||
cursor-text = #edecee
|
||||
selection-background = #a277ff
|
||||
selection-foreground = #edecee
|
||||
22
CommandNotch/CommandNotch/Resources/ghostty/themes/Aurora
Normal file
@@ -0,0 +1,22 @@
|
||||
palette = 0=#23262e
|
||||
palette = 1=#f0266f
|
||||
palette = 2=#8fd46d
|
||||
palette = 3=#ffe66d
|
||||
palette = 4=#102ee4
|
||||
palette = 5=#ee5d43
|
||||
palette = 6=#03d6b8
|
||||
palette = 7=#c74ded
|
||||
palette = 8=#4f545e
|
||||
palette = 9=#f92672
|
||||
palette = 10=#8fd46d
|
||||
palette = 11=#ffe66d
|
||||
palette = 12=#03d6b8
|
||||
palette = 13=#ee5d43
|
||||
palette = 14=#03d6b8
|
||||
palette = 15=#c74ded
|
||||
background = #23262e
|
||||
foreground = #ffca28
|
||||
cursor-color = #ee5d43
|
||||
cursor-text = #ffd29c
|
||||
selection-background = #292e38
|
||||
selection-foreground = #00e8c6
|
||||