Compare commits

5 Commits

89 changed files with 2411 additions and 890 deletions

BIN
.github/assets/CommandNotch-Demo.mp4 vendored Normal file

Binary file not shown.

BIN
.github/assets/CommandNotch-Open.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

BIN
.github/assets/CommandNotch-Settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

BIN
.github/assets/bch-receiving-address.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

422
.github/demo/aurora.py vendored Executable file
View 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:]))

4
.gitignore vendored
View File

@@ -79,7 +79,5 @@ jspm_packages/
dist/ dist/
build/ build/
**/Release* # Mac... files
CommandNotch 20*
**/.DS_Store **/.DS_Store

89
CONTRIBUTING.md Normal file
View 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.

View File

@@ -0,0 +1,787 @@
// !$*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 */; };
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 */; };
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 */; };
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 */; };
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 */; };
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 */; };
DAD3AB4A0DAADA32C02D959E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3125FD3DC55420122CF85D80 /* SettingsView.swift */; };
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22AA47452CF798A977A6F47 /* TerminalCommandArrowBehavior.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 */; };
/* 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>"; };
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; };
39CA6F2C2CA32D0AA58F6C43 /* AppSettingsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsStoreTests.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchState.swift; 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>"; };
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>"; };
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>"; };
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 */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
00BC30DD7FA5C3C26404733B /* Models */ = {
isa = PBXGroup;
children = (
74463E4EAB78F56345360CD5 /* AppSettings.swift */,
EB28950392C0198E69F3564B /* AppSettingsController.swift */,
9E6C98C406899F4B242075AF /* AppSettingsStore.swift */,
8BB1C403BC2157756F572ACF /* HotkeyBinding.swift */,
DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */,
297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */,
4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */,
7181BB1F3926B457445105E5 /* ScreenContext.swift */,
AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */,
567E85A2ED628460CEC760DB /* TerminalManager.swift */,
49E1791BB45E1505500ACC67 /* TerminalSession.swift */,
CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */,
3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */,
0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */,
F3B957BD1F6120D2592613ED /* WorkspaceStore.swift */,
7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */,
);
path = Models;
sourceTree = "<group>";
};
618799FE544A4373B457DCDA /* Extensions */ = {
isa = PBXGroup;
children = (
5DDC7451F0E7ACFE0BEC5473 /* NSScreen+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
7043235A31A4023478DA1302 = {
isa = PBXGroup;
children = (
84740FA9CF6A18B35EC82623 /* CommandNotch */,
A2F9603AB9C86C4EA62FFA59 /* CommandNotchTests */,
E6C841E864A0CC68B9B05BAC /* CommandNotchUITests */,
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 */,
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 */,
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" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = 7043235A31A4023478DA1302;
minimizedProjectReferenceProxies = 1;
packageReferences = (
28377BE3F9997892D4929B6E /* XCRemoteSwiftPackageReference "SwiftTerm" */,
);
preferredProjectObjectVersion = 77;
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 */,
);
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 */,
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 */,
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 */,
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 */,
5F3534F66A5DBE4E081AFFA6 /* SwiftTermView.swift in Sources */,
88113BA9B217DA579C36BEBE /* TabBar.swift in Sources */,
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */,
6F249EDFA2D654457DF385F1 /* TerminalManager.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;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
PRODUCT_NAME = CommandNotch;
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;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
PRODUCT_NAME = CommandNotch;
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 */;
}

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "2620" LastUpgradeVersion = "1600"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES"
buildArchitectures = "Automatic"> runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -15,7 +15,7 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B" BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
BuildableName = "CommandNotch.app" BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch" BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj"> ReferencedContainer = "container:CommandNotch.xcodeproj">
@@ -28,7 +28,42 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES"> onlyGenerateCoverageForSpecifiedTargets = "NO">
<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"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1C8D00CBB29219BD347E9CC4"
BuildableName = "CommandNotchUITests.xctest"
BlueprintName = "CommandNotchUITests"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -44,12 +79,14 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B" BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
BuildableName = "CommandNotch.app" BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch" BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj"> ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -61,12 +98,14 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B" BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
BuildableName = "CommandNotch.app" BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch" BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj"> ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "2620" LastUpgradeVersion = "1600"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES"
buildArchitectures = "Automatic"> runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -15,7 +15,7 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B" BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
BuildableName = "CommandNotch.app" BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch" BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj"> ReferencedContainer = "container:CommandNotch.xcodeproj">
@@ -28,13 +28,48 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES"> onlyGenerateCoverageForSpecifiedTargets = "NO">
<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"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1C8D00CBB29219BD347E9CC4"
BuildableName = "CommandNotchUITests.xctest"
BlueprintName = "CommandNotchUITests"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Release"
selectedDebuggerIdentifier = "" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "1" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
@@ -44,12 +79,14 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B" BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
BuildableName = "CommandNotch.app" BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch" BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj"> ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -61,12 +98,14 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B" BlueprintIdentifier = "D5585E5732CD067DF6EF0C69"
BuildableName = "CommandNotch.app" BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch" BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj"> ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">

View File

@@ -76,9 +76,7 @@ struct HotkeyRecorderField: NSViewRepresentable {
} }
func updateNSView(_ nsView: HotkeyNSView, context: Context) { func updateNSView(_ nsView: HotkeyNSView, context: Context) {
nsView.currentLabel = binding.displayString nsView.update(currentLabel: binding.displayString, isRecording: isRecording)
nsView.showRecording = isRecording
nsView.needsDisplay = true
} }
} }
@@ -99,9 +97,7 @@ struct OptionalHotkeyRecorderField: NSViewRepresentable {
} }
func updateNSView(_ nsView: HotkeyNSView, context: Context) { func updateNSView(_ nsView: HotkeyNSView, context: Context) {
nsView.currentLabel = binding?.displayString ?? "Not set" nsView.update(currentLabel: binding?.displayString ?? "Not set", isRecording: isRecording)
nsView.showRecording = isRecording
nsView.needsDisplay = true
} }
} }
@@ -183,6 +179,12 @@ class HotkeyNSView: NSView {
updateLabelAppearance() updateLabelAppearance()
} }
func update(currentLabel: String, isRecording: Bool) {
self.currentLabel = currentLabel
showRecording = isRecording
updateLabelAppearance()
}
private func updateLabelAppearance() { private func updateLabelAppearance() {
label.stringValue = showRecording ? "Press keys..." : currentLabel label.stringValue = showRecording ? "Press keys..." : currentLabel
label.textColor = showRecording ? .controlAccentColor : .labelColor label.textColor = showRecording ? .controlAccentColor : .labelColor

View File

@@ -9,17 +9,6 @@ struct TabBar: View {
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
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)
}
Spacer()
Button { Button {
workspace.newTab() workspace.newTab()
} label: { } label: {
@@ -31,6 +20,15 @@ struct TabBar: View {
.accessibilityIdentifier("notch.new-tab") .accessibilityIdentifier("notch.new-tab")
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(.horizontal, 8) .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) .frame(height: 28)
.background(.black) .background(.black)

View File

@@ -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
}
}
}

View File

@@ -5,7 +5,7 @@ import Combine
/// Manages global and local hotkeys. /// Manages global and local hotkeys.
/// ///
/// The toggle hotkey uses Carbon's `RegisterEventHotKey` which works /// 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). /// use a local `NSEvent` monitor (only fires when our app is active).
@MainActor @MainActor
class HotkeyManager { class HotkeyManager {
@@ -19,21 +19,29 @@ class HotkeyManager {
var onCloseTab: (() -> Void)? var onCloseTab: (() -> Void)?
var onNextTab: (() -> Void)? var onNextTab: (() -> Void)?
var onPreviousTab: (() -> Void)? var onPreviousTab: (() -> Void)?
var onNextWorkspace: (() -> Void)?
var onPreviousWorkspace: (() -> Void)?
var onDetachTab: (() -> Void)? var onDetachTab: (() -> Void)?
var onApplySizePreset: ((TerminalSizePreset) -> Void)? var onApplySizePreset: ((TerminalSizePreset) -> Void)?
var onSwitchToTab: ((Int) -> 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 var isNotchOpen: Bool = false
private var hotKeyRef: EventHotKeyRef? private var hotKeyRef: EventHotKeyRef?
private var eventHandlerRef: EventHandlerRef? private var eventHandlerRef: EventHandlerRef?
private var localMonitor: Any? private var localMonitor: Any?
private let settingsProvider: TerminalSessionConfigurationProviding private let settingsProvider: TerminalSessionConfigurationProviding
private let workspaceRegistry: WorkspaceRegistry
private var settingsCancellable: AnyCancellable? private var settingsCancellable: AnyCancellable?
init(settingsProvider: TerminalSessionConfigurationProviding? = nil) { init(
settingsProvider: TerminalSessionConfigurationProviding? = nil,
workspaceRegistry: WorkspaceRegistry? = nil
) {
self.settingsProvider = settingsProvider ?? AppSettingsController.shared self.settingsProvider = settingsProvider ?? AppSettingsController.shared
self.workspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
} }
// MARK: - Resolved bindings from typed runtime settings // MARK: - Resolved bindings from typed runtime settings
@@ -53,6 +61,12 @@ class HotkeyManager {
private var prevTabBinding: HotkeyBinding { private var prevTabBinding: HotkeyBinding {
settingsProvider.hotkeySettings.previousTab settingsProvider.hotkeySettings.previousTab
} }
private var nextWorkspaceBinding: HotkeyBinding {
settingsProvider.hotkeySettings.nextWorkspace
}
private var previousWorkspaceBinding: HotkeyBinding {
settingsProvider.hotkeySettings.previousWorkspace
}
private var detachBinding: HotkeyBinding { private var detachBinding: HotkeyBinding {
settingsProvider.hotkeySettings.detachTab settingsProvider.hotkeySettings.detachTab
} }
@@ -173,7 +187,7 @@ class HotkeyManager {
} }
} }
// 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() { private func installLocalMonitor() {
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
@@ -189,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 { 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 } guard isNotchOpen else { return false }
if newTabBinding.matches(event) { if newTabBinding.matches(event) {
@@ -210,10 +224,25 @@ class HotkeyManager {
onPreviousTab?() onPreviousTab?()
return true return true
} }
if nextWorkspaceBinding.matches(event) {
onNextWorkspace?()
return true
}
if previousWorkspaceBinding.matches(event) {
onPreviousWorkspace?()
return true
}
if detachBinding.matches(event) { if detachBinding.matches(event) {
onDetachTab?() onDetachTab?()
return true 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 { for preset in sizePresets {
guard let binding = preset.hotkey else { continue } guard let binding = preset.hotkey else { continue }
if binding.matches(event) { if binding.matches(event) {

View File

@@ -9,6 +9,7 @@ final class ScreenManager: ObservableObject {
static let shared = ScreenManager() static let shared = ScreenManager()
private let screenRegistry = ScreenRegistry.shared private let screenRegistry = ScreenRegistry.shared
private let workspaceRegistry = WorkspaceRegistry.shared
private let windowCoordinator = WindowCoordinator() private let windowCoordinator = WindowCoordinator()
private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self) private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self)
@@ -55,6 +56,12 @@ final class ScreenManager: ObservableObject {
hotkeyManager.onPreviousTab = { [weak self] in hotkeyManager.onPreviousTab = { [weak self] in
MainActor.assumeIsolated { self?.activeWorkspace().previousTab() } 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 hotkeyManager.onDetachTab = { [weak self] in
MainActor.assumeIsolated { self?.detachActiveTab() } MainActor.assumeIsolated { self?.detachActiveTab() }
} }
@@ -64,6 +71,9 @@ final class ScreenManager: ObservableObject {
hotkeyManager.onSwitchToTab = { [weak self] index in hotkeyManager.onSwitchToTab = { [weak self] index in
MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) } MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) }
} }
hotkeyManager.onSwitchToWorkspace = { [weak self] workspaceID in
MainActor.assumeIsolated { self?.switchActiveScreen(to: workspaceID) }
}
hotkeyManager.start() hotkeyManager.start()
} }
@@ -92,6 +102,33 @@ final class ScreenManager: ObservableObject {
} }
} }
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) { func applySizePreset(_ preset: TerminalSizePreset) {
guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else { guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else {
AppSettingsController.shared.update { AppSettingsController.shared.update {

View File

@@ -56,6 +56,8 @@ struct AppSettings: Equatable, Codable {
closeTab: .cmdW, closeTab: .cmdW,
nextTab: .cmdShiftRB, nextTab: .cmdShiftRB,
previousTab: .cmdShiftLB, previousTab: .cmdShiftLB,
nextWorkspace: .cmdShiftDown,
previousWorkspace: .cmdShiftUp,
detachTab: .cmdD detachTab: .cmdD
) )
) )
@@ -121,6 +123,8 @@ extension AppSettings {
var closeTab: HotkeyBinding var closeTab: HotkeyBinding
var nextTab: HotkeyBinding var nextTab: HotkeyBinding
var previousTab: HotkeyBinding var previousTab: HotkeyBinding
var nextWorkspace: HotkeyBinding
var previousWorkspace: HotkeyBinding
var detachTab: HotkeyBinding var detachTab: HotkeyBinding
} }
} }

View File

@@ -60,6 +60,8 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
closeTab: hotkey(NotchSettings.Keys.hotkeyCloseTab, default: .cmdW), closeTab: hotkey(NotchSettings.Keys.hotkeyCloseTab, default: .cmdW),
nextTab: hotkey(NotchSettings.Keys.hotkeyNextTab, default: .cmdShiftRB), nextTab: hotkey(NotchSettings.Keys.hotkeyNextTab, default: .cmdShiftRB),
previousTab: hotkey(NotchSettings.Keys.hotkeyPreviousTab, default: .cmdShiftLB), 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) detachTab: hotkey(NotchSettings.Keys.hotkeyDetachTab, default: .cmdD)
) )
) )
@@ -106,6 +108,8 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
defaults.set(settings.hotkeys.closeTab.toJSON(), forKey: NotchSettings.Keys.hotkeyCloseTab) defaults.set(settings.hotkeys.closeTab.toJSON(), forKey: NotchSettings.Keys.hotkeyCloseTab)
defaults.set(settings.hotkeys.nextTab.toJSON(), forKey: NotchSettings.Keys.hotkeyNextTab) defaults.set(settings.hotkeys.nextTab.toJSON(), forKey: NotchSettings.Keys.hotkeyNextTab)
defaults.set(settings.hotkeys.previousTab.toJSON(), forKey: NotchSettings.Keys.hotkeyPreviousTab) 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) defaults.set(settings.hotkeys.detachTab.toJSON(), forKey: NotchSettings.Keys.hotkeyDetachTab)
} }

View File

@@ -88,6 +88,8 @@ struct HotkeyBinding: Codable, Equatable, Hashable {
static let cmdW = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 13) 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 cmdShiftRB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30) // ]
static let cmdShiftLB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 33) // [ 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 let cmdD = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)
static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? { static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? {

View File

@@ -57,6 +57,8 @@ enum NotchSettings {
static let hotkeyCloseTab = "hotkey_closeTab" static let hotkeyCloseTab = "hotkey_closeTab"
static let hotkeyNextTab = "hotkey_nextTab" static let hotkeyNextTab = "hotkey_nextTab"
static let hotkeyPreviousTab = "hotkey_previousTab" static let hotkeyPreviousTab = "hotkey_previousTab"
static let hotkeyNextWorkspace = "hotkey_nextWorkspace"
static let hotkeyPreviousWorkspace = "hotkey_previousWorkspace"
static let hotkeyDetachTab = "hotkey_detachTab" static let hotkeyDetachTab = "hotkey_detachTab"
} }
@@ -104,6 +106,8 @@ enum NotchSettings {
static let hotkeyCloseTab: String = HotkeyBinding.cmdW.toJSON() static let hotkeyCloseTab: String = HotkeyBinding.cmdW.toJSON()
static let hotkeyNextTab: String = HotkeyBinding.cmdShiftRB.toJSON() static let hotkeyNextTab: String = HotkeyBinding.cmdShiftRB.toJSON()
static let hotkeyPreviousTab: String = HotkeyBinding.cmdShiftLB.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() static let hotkeyDetachTab: String = HotkeyBinding.cmdD.toJSON()
} }
@@ -151,6 +155,8 @@ enum NotchSettings {
Keys.hotkeyCloseTab: Defaults.hotkeyCloseTab, Keys.hotkeyCloseTab: Defaults.hotkeyCloseTab,
Keys.hotkeyNextTab: Defaults.hotkeyNextTab, Keys.hotkeyNextTab: Defaults.hotkeyNextTab,
Keys.hotkeyPreviousTab: Defaults.hotkeyPreviousTab, Keys.hotkeyPreviousTab: Defaults.hotkeyPreviousTab,
Keys.hotkeyNextWorkspace: Defaults.hotkeyNextWorkspace,
Keys.hotkeyPreviousWorkspace: Defaults.hotkeyPreviousWorkspace,
Keys.hotkeyDetachTab: Defaults.hotkeyDetachTab, Keys.hotkeyDetachTab: Defaults.hotkeyDetachTab,
]) ])
} }

View File

@@ -9,16 +9,26 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
let id = UUID() let id = UUID()
let terminalView: TerminalView let terminalView: TerminalView
private var process: LocalProcess? private var process: LocalProcess?
private var keyEventMonitor: Any?
private let backgroundColor = NSColor.black private let backgroundColor = NSColor.black
private let configuredShellPath: String private let configuredShellPath: String
private let launchDirectory: String
@Published var title: String = "shell" @Published var title: String = "shell"
@Published var isRunning: Bool = true @Published var isRunning: Bool = true
@Published var currentDirectory: String? @Published var currentDirectory: String?
init(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) { init(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String? = nil,
startImmediately: Bool = true
) {
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300)) terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
configuredShellPath = shellPath configuredShellPath = shellPath
launchDirectory = Self.resolveInitialDirectory(initialDirectory)
currentDirectory = launchDirectory
super.init() super.init()
terminalView.terminalDelegate = self terminalView.terminalDelegate = self
@@ -26,8 +36,19 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
terminalView.font = font terminalView.font = font
applyTheme(theme) applyTheme(theme)
installCommandArrowMonitor()
startShell() if startImmediately {
startShell()
} else {
isRunning = false
}
}
deinit {
if let keyEventMonitor {
NSEvent.removeMonitor(keyEventMonitor)
}
} }
// MARK: - Shell management // MARK: - Shell management
@@ -44,12 +65,29 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
args: ["-l"], args: ["-l"],
environment: nil, environment: nil,
execName: loginExecName, execName: loginExecName,
currentDirectory: NSHomeDirectory() currentDirectory: launchDirectory
) )
process = proc process = proc
title = shellName title = shellName
} }
private static func resolveInitialDirectory(_ directory: String?) -> String {
normalizedDirectory(directory) ?? NSHomeDirectory()
}
private 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 func resolveShell() -> String { private func resolveShell() -> String {
let custom = configuredShellPath.trimmingCharacters(in: .whitespacesAndNewlines) let custom = configuredShellPath.trimmingCharacters(in: .whitespacesAndNewlines)
if !custom.isEmpty && FileManager.default.isExecutableFile(atPath: custom) { if !custom.isEmpty && FileManager.default.isExecutableFile(atPath: custom) {
@@ -58,6 +96,26 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
return ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh" return ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
} }
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
}
}
func updateFontSize(_ size: CGFloat) { func updateFontSize(_ size: CGFloat) {
terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular) terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
} }
@@ -117,7 +175,8 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
} }
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) { func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
currentDirectory = directory guard let normalizedDirectory = Self.normalizedDirectory(directory) else { return }
currentDirectory = normalizedDirectory
} }
func scrolled(source: TerminalView, position: Double) {} func scrolled(source: TerminalView, position: Double) {}

View File

@@ -3,12 +3,27 @@ import Combine
@MainActor @MainActor
protocol TerminalSessionFactoryType { protocol TerminalSessionFactoryType {
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession
} }
struct LiveTerminalSessionFactory: TerminalSessionFactoryType { struct LiveTerminalSessionFactory: TerminalSessionFactoryType {
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession { func makeSession(
TerminalSession(fontSize: fontSize, theme: theme, shellPath: shellPath) fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
TerminalSession(
fontSize: fontSize,
theme: theme,
shellPath: shellPath,
initialDirectory: initialDirectory
)
} }
} }
@@ -18,6 +33,7 @@ final class WorkspaceController: ObservableObject {
let createdAt: Date let createdAt: Date
@Published private(set) var name: String @Published private(set) var name: String
@Published private(set) var hotkey: HotkeyBinding?
@Published private(set) var tabs: [TerminalSession] = [] @Published private(set) var tabs: [TerminalSession] = []
@Published private(set) var activeTabIndex: Int = 0 @Published private(set) var activeTabIndex: Int = 0
@@ -34,6 +50,7 @@ final class WorkspaceController: ObservableObject {
self.id = summary.id self.id = summary.id
self.name = summary.name self.name = summary.name
self.createdAt = summary.createdAt self.createdAt = summary.createdAt
self.hotkey = summary.hotkey
self.sessionFactory = sessionFactory self.sessionFactory = sessionFactory
self.settingsProvider = settingsProvider self.settingsProvider = settingsProvider
@@ -51,7 +68,7 @@ final class WorkspaceController: ObservableObject {
} }
var summary: WorkspaceSummary { var summary: WorkspaceSummary {
WorkspaceSummary(id: id, name: name, createdAt: createdAt) WorkspaceSummary(id: id, name: name, createdAt: createdAt, hotkey: hotkey)
} }
var state: WorkspaceState { var state: WorkspaceState {
@@ -78,12 +95,18 @@ final class WorkspaceController: ObservableObject {
name = trimmed name = trimmed
} }
func updateHotkey(_ updatedHotkey: HotkeyBinding?) {
guard hotkey != updatedHotkey else { return }
hotkey = updatedHotkey
}
func newTab() { func newTab() {
let config = settingsProvider.terminalSessionConfiguration let config = settingsProvider.terminalSessionConfiguration
let session = sessionFactory.makeSession( let session = sessionFactory.makeSession(
fontSize: config.fontSize, fontSize: config.fontSize,
theme: config.theme, theme: config.theme,
shellPath: config.shellPath shellPath: config.shellPath,
initialDirectory: activeTab?.currentDirectory
) )
titleObservers[session.id] = session.$title titleObservers[session.id] = session.$title

View File

@@ -104,6 +104,37 @@ final class WorkspaceRegistry: ObservableObject {
persistWorkspaceSummaries() 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 @discardableResult
func deleteWorkspace(id: WorkspaceID) -> Bool { func deleteWorkspace(id: WorkspaceID) -> Bool {
guard canDeleteWorkspace(id: id) else { return false } guard canDeleteWorkspace(id: id) else { return false }

View File

@@ -6,11 +6,13 @@ struct WorkspaceSummary: Identifiable, Equatable, Codable {
var id: WorkspaceID var id: WorkspaceID
var name: String var name: String
var createdAt: Date var createdAt: Date
var hotkey: HotkeyBinding?
init(id: WorkspaceID = UUID(), name: String, createdAt: Date = Date()) { init(id: WorkspaceID = UUID(), name: String, createdAt: Date = Date(), hotkey: HotkeyBinding? = nil) {
self.id = id self.id = id
self.name = name self.name = name
self.createdAt = createdAt self.createdAt = createdAt
self.hotkey = hotkey
} }
} }

View File

@@ -17,8 +17,13 @@ struct HotkeySettingsView: View {
HotkeyRecorderView(label: "Detach tab", binding: settingsController.binding(\.hotkeys.detachTab)) HotkeyRecorderView(label: "Detach tab", binding: settingsController.binding(\.hotkeys.detachTab))
} }
Section("Workspaces (active when notch is open)") {
HotkeyRecorderView(label: "Next workspace", binding: settingsController.binding(\.hotkeys.nextWorkspace))
HotkeyRecorderView(label: "Previous workspace", binding: settingsController.binding(\.hotkeys.previousWorkspace))
}
Section { Section {
Text("⌘19 always switch to tab by number. Size preset hotkeys are configured in Terminal > Size Presets.") Text("⌘19 always switch to tab by number. Size preset hotkeys are configured in Terminal > Size Presets. Per-workspace jump hotkeys are configured in Workspaces.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }

View File

@@ -2,21 +2,7 @@ import SwiftUI
struct TerminalSettingsView: View { struct TerminalSettingsView: View {
@ObservedObject private var settingsController = AppSettingsController.shared @ObservedObject private var settingsController = AppSettingsController.shared
@State private var sizePresets: [TerminalSizePreset] = []
private var sizePresetsBinding: Binding<[TerminalSizePreset]> {
Binding(
get: {
TerminalSizePresetStore.decodePresets(
from: settingsController.settings.terminal.sizePresetsJSON
) ?? TerminalSizePresetStore.loadDefaults()
},
set: { newValue in
settingsController.update {
$0.terminal.sizePresetsJSON = TerminalSizePresetStore.encodePresets(newValue)
}
}
)
}
var body: some View { var body: some View {
Form { Form {
@@ -55,7 +41,7 @@ struct TerminalSettingsView: View {
} }
Section("Size Presets") { Section("Size Presets") {
ForEach(sizePresetsBinding) { $preset in ForEach($sizePresets) { $preset in
TerminalSizePresetEditor( TerminalSizePresetEditor(
preset: $preset, preset: $preset,
currentOpenWidth: settingsController.settings.display.openWidth, currentOpenWidth: settingsController.settings.display.openWidth,
@@ -67,20 +53,18 @@ struct TerminalSettingsView: View {
HStack { HStack {
Button("Add Preset") { Button("Add Preset") {
var presets = sizePresetsBinding.wrappedValue sizePresets.append(
presets.append(
TerminalSizePreset( TerminalSizePreset(
name: "Preset \(presets.count + 1)", name: "Preset \(sizePresets.count + 1)",
width: settingsController.settings.display.openWidth, width: settingsController.settings.display.openWidth,
height: settingsController.settings.display.openHeight, height: settingsController.settings.display.openHeight,
hotkey: TerminalSizePresetStore.suggestedHotkey(for: presets) hotkey: TerminalSizePresetStore.suggestedHotkey(for: sizePresets)
) )
) )
sizePresetsBinding.wrappedValue = presets
} }
Button("Reset Presets") { Button("Reset Presets") {
sizePresetsBinding.wrappedValue = TerminalSizePresetStore.loadDefaults() sizePresets = TerminalSizePresetStore.loadDefaults()
} }
} }
@@ -90,10 +74,24 @@ struct TerminalSettingsView: View {
} }
} }
.formStyle(.grouped) .formStyle(.grouped)
.onAppear {
synchronizePresetsFromSettings()
}
.onChange(of: settingsController.settings.terminal.sizePresetsJSON) { _, _ in
synchronizePresetsFromSettings()
}
.onChange(of: sizePresets) { _, newValue in
let encoded = TerminalSizePresetStore.encodePresets(newValue)
guard encoded != settingsController.settings.terminal.sizePresetsJSON else { return }
settingsController.update {
$0.terminal.sizePresetsJSON = encoded
}
}
} }
private func deletePreset(id: UUID) { private func deletePreset(id: UUID) {
sizePresetsBinding.wrappedValue.removeAll { $0.id == id } sizePresets.removeAll { $0.id == id }
} }
private func applyPreset(_ preset: TerminalSizePreset) { private func applyPreset(_ preset: TerminalSizePreset) {
@@ -103,6 +101,15 @@ struct TerminalSettingsView: View {
} }
ScreenManager.shared.applySizePreset(preset) ScreenManager.shared.applySizePreset(preset)
} }
private func synchronizePresetsFromSettings() {
let decoded = TerminalSizePresetStore.decodePresets(
from: settingsController.settings.terminal.sizePresetsJSON
) ?? TerminalSizePresetStore.loadDefaults()
guard decoded != sizePresets else { return }
sizePresets = decoded
}
} }
private struct TerminalSizePresetEditor: View { private struct TerminalSizePresetEditor: View {

View File

@@ -75,6 +75,11 @@ struct WorkspacesSettingsView: View {
renameSelectedWorkspace() renameSelectedWorkspace()
} }
OptionalHotkeyRecorderView(
label: "Jump Hotkey",
binding: workspaceHotkeyBinding(for: summary.id)
)
HStack { HStack {
Button("Save Name") { Button("Save Name") {
renameSelectedWorkspace() renameSelectedWorkspace()
@@ -86,6 +91,10 @@ struct WorkspacesSettingsView: View {
} }
.accessibilityIdentifier("settings.workspaces.new") .accessibilityIdentifier("settings.workspaces.new")
} }
Text("Workspace jump hotkeys are active when the notch is open and switch the current screen to this workspace.")
.font(.caption)
.foregroundStyle(.secondary)
} }
Section("Usage") { Section("Usage") {
@@ -256,6 +265,17 @@ struct WorkspacesSettingsView: View {
renameDraft = workspaceRegistry.summary(for: workspaceID)?.name ?? "" renameDraft = workspaceRegistry.summary(for: workspaceID)?.name ?? ""
} }
private func workspaceHotkeyBinding(for workspaceID: WorkspaceID) -> Binding<HotkeyBinding?> {
Binding(
get: {
workspaceRegistry.summary(for: workspaceID)?.hotkey
},
set: { newValue in
workspaceRegistry.updateWorkspaceHotkey(id: workspaceID, to: newValue)
}
)
}
private func deleteSelectedWorkspace() { private func deleteSelectedWorkspace() {
guard let effectiveSelectedWorkspaceID, guard let effectiveSelectedWorkspaceID,
let fallbackWorkspaceID = screenRegistry.deleteWorkspace( let fallbackWorkspaceID = screenRegistry.deleteWorkspace(

View File

@@ -313,7 +313,12 @@ private final class ScreenRegistryTestSettingsProvider: TerminalSessionConfigura
private struct ScreenRegistryUnusedTerminalSessionFactory: TerminalSessionFactoryType { private struct ScreenRegistryUnusedTerminalSessionFactory: TerminalSessionFactoryType {
@MainActor @MainActor
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession { func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
fatalError("ScreenRegistryTests should not create live terminal sessions.") fatalError("ScreenRegistryTests should not create live terminal sessions.")
} }
} }

View File

@@ -0,0 +1,57 @@
import AppKit
import Carbon.HIToolbox
import XCTest
import SwiftTerm
@testable import CommandNotch
final class TerminalCommandArrowBehaviorTests: XCTestCase {
func testCommandLeftUsesHomeSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_LeftArrow),
applicationCursor: false
)
XCTAssertEqual(sequence, EscapeSequences.moveHomeNormal)
}
func testCommandRightUsesApplicationEndSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_RightArrow),
applicationCursor: true
)
XCTAssertEqual(sequence, EscapeSequences.moveEndApp)
}
func testOptionLeftKeepsSwiftTermWordNavigationPath() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.option],
keyCode: UInt16(kVK_LeftArrow),
applicationCursor: false
)
XCTAssertNil(sequence)
}
func testCommandDeleteUsesLineKillSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_Delete),
applicationCursor: false
)
XCTAssertEqual(sequence, [0x15])
}
func testCommandLUsesClearScreenSequence() {
let sequence = TerminalCommandArrowBehavior.sequence(
for: [.command],
keyCode: UInt16(kVK_ANSI_L),
applicationCursor: false
)
XCTAssertEqual(sequence, [0x0c])
}
}

View File

@@ -74,6 +74,30 @@ final class WorkspaceRegistryTests: XCTestCase {
XCTAssertEqual(store.savedSummaries.map(\.name), ["Main"]) XCTAssertEqual(store.savedSummaries.map(\.name), ["Main"])
} }
func testUpdateWorkspaceHotkeyPersistsAndUpdatesSummary() {
let store = InMemoryWorkspaceStore()
let registry = makeRegistry(store: store)
let docsID = registry.createWorkspace(named: "Docs")
let hotkey = HotkeyBinding.cmdShiftDigit(4)
registry.updateWorkspaceHotkey(id: docsID, to: hotkey)
XCTAssertEqual(registry.summary(for: docsID)?.hotkey, hotkey)
XCTAssertEqual(store.savedSummaries.last?.hotkey, hotkey)
}
func testNextAndPreviousWorkspaceWrapAroundRegistryOrder() {
let registry = makeRegistry()
let mainID = registry.defaultWorkspaceID
let docsID = registry.createWorkspace(named: "Docs")
let reviewID = registry.createWorkspace(named: "Review")
XCTAssertEqual(registry.nextWorkspaceID(after: mainID), docsID)
XCTAssertEqual(registry.nextWorkspaceID(after: reviewID), mainID)
XCTAssertEqual(registry.previousWorkspaceID(before: mainID), reviewID)
XCTAssertEqual(registry.previousWorkspaceID(before: docsID), mainID)
}
private func makeRegistry( private func makeRegistry(
initialWorkspaces: [WorkspaceSummary]? = [], initialWorkspaces: [WorkspaceSummary]? = [],
store: (any WorkspaceStoreType)? = nil store: (any WorkspaceStoreType)? = nil
@@ -96,6 +120,64 @@ final class WorkspaceRegistryTests: XCTestCase {
} }
} }
@MainActor
final class WorkspaceControllerTests: XCTestCase {
func testNewTabUsesActiveTabCurrentDirectory() {
let factory = RecordingTerminalSessionFactory()
let controller = WorkspaceController(
summary: WorkspaceSummary(name: "Main"),
sessionFactory: factory,
settingsProvider: TestSettingsProvider(),
bootstrapDefaultTab: false
)
controller.newTab()
controller.activeTab?.currentDirectory = "/tmp/Raycast"
controller.newTab()
XCTAssertEqual(factory.requestedDirectories, [nil, "/tmp/Raycast"])
XCTAssertEqual(controller.activeTab?.currentDirectory, "/tmp/Raycast")
XCTAssertEqual(controller.tabs.count, 2)
XCTAssertEqual(controller.activeTabIndex, 1)
}
func testNewTabNormalizesCurrentDirectoryFileURL() {
let factory = RecordingTerminalSessionFactory()
let controller = WorkspaceController(
summary: WorkspaceSummary(name: "Main"),
sessionFactory: factory,
settingsProvider: TestSettingsProvider(),
bootstrapDefaultTab: false
)
let expectedPath = "/tmp/Raycast Folder"
controller.newTab()
controller.activeTab?.currentDirectory = URL(fileURLWithPath: expectedPath).absoluteString
controller.newTab()
XCTAssertEqual(controller.activeTab?.currentDirectory, expectedPath)
}
func testNewTabFallsBackToHomeDirectoryWhenWorkspaceHasNoTabs() {
let factory = RecordingTerminalSessionFactory()
let controller = WorkspaceController(
summary: WorkspaceSummary(name: "Main"),
sessionFactory: factory,
settingsProvider: TestSettingsProvider(),
bootstrapDefaultTab: false
)
controller.newTab()
XCTAssertEqual(factory.requestedDirectories, [nil])
XCTAssertEqual(controller.activeTab?.currentDirectory, NSHomeDirectory())
XCTAssertEqual(controller.tabs.count, 1)
XCTAssertEqual(controller.activeTabIndex, 0)
}
}
private final class InMemoryWorkspaceStore: WorkspaceStoreType { private final class InMemoryWorkspaceStore: WorkspaceStoreType {
var savedSummaries: [WorkspaceSummary] = [] var savedSummaries: [WorkspaceSummary] = []
@@ -114,9 +196,35 @@ private final class TestSettingsProvider: TerminalSessionConfigurationProviding
let terminalSizePresets = TerminalSizePresetStore.loadDefaults() let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
} }
private final class RecordingTerminalSessionFactory: TerminalSessionFactoryType {
private(set) var requestedDirectories: [String?] = []
@MainActor
func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
requestedDirectories.append(initialDirectory)
return TerminalSession(
fontSize: fontSize,
theme: theme,
shellPath: shellPath,
initialDirectory: initialDirectory,
startImmediately: false
)
}
}
private struct UnusedTerminalSessionFactory: TerminalSessionFactoryType { private struct UnusedTerminalSessionFactory: TerminalSessionFactoryType {
@MainActor @MainActor
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession { func makeSession(
fontSize: CGFloat,
theme: TerminalTheme,
shellPath: String,
initialDirectory: String?
) -> TerminalSession {
fatalError("WorkspaceRegistryTests should not create live terminal sessions.") fatalError("WorkspaceRegistryTests should not create live terminal sessions.")
} }
} }

View File

@@ -9,7 +9,11 @@ final class WorkspaceStoreTests: XCTestCase {
let store = UserDefaultsWorkspaceStore(defaults: defaults) let store = UserDefaultsWorkspaceStore(defaults: defaults)
let summaries = [ let summaries = [
WorkspaceSummary(id: UUID(uuidString: "11111111-1111-1111-1111-111111111111")!, name: "Main"), WorkspaceSummary(
id: UUID(uuidString: "11111111-1111-1111-1111-111111111111")!,
name: "Main",
hotkey: HotkeyBinding.cmdShiftDigit(4)
),
WorkspaceSummary(id: UUID(uuidString: "22222222-2222-2222-2222-222222222222")!, name: "Docs") WorkspaceSummary(id: UUID(uuidString: "22222222-2222-2222-2222-222222222222")!, name: "Docs")
] ]

View File

@@ -14,6 +14,45 @@ packages:
SwiftTerm: SwiftTerm:
url: https://github.com/migueldeicaza/SwiftTerm.git url: https://github.com/migueldeicaza/SwiftTerm.git
from: "1.2.0" from: "1.2.0"
schemes:
CommandNotch:
build:
targets:
CommandNotch: all
test:
config: Debug
targets:
- CommandNotchTests
- CommandNotchUITests
run:
config: Debug
profile:
config: Release
analyze:
config: Debug
archive:
config: Release
management:
shared: true
Release-CommandNotch:
build:
targets:
CommandNotch: all
test:
config: Debug
targets:
- CommandNotchTests
- CommandNotchUITests
run:
config: Release
profile:
config: Release
analyze:
config: Debug
archive:
config: Release
management:
shared: true
targets: targets:
CommandNotch: CommandNotch:
type: application type: application
@@ -29,9 +68,9 @@ targets:
properties: properties:
CFBundleName: CommandNotch CFBundleName: CommandNotch
CFBundleDisplayName: CommandNotch CFBundleDisplayName: CommandNotch
CFBundleIdentifier: com.commandnotch.app CFBundleIdentifier: "$(PRODUCT_BUNDLE_IDENTIFIER)"
CFBundleVersion: "1" CFBundleVersion: "1"
CFBundleShortVersionString: "0.2.0" CFBundleShortVersionString: "0.0.3"
CFBundlePackageType: APPL CFBundlePackageType: APPL
CFBundleExecutable: CommandNotch CFBundleExecutable: CommandNotch
LSMinimumSystemVersion: "14.0" LSMinimumSystemVersion: "14.0"

View File

@@ -1,809 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 63;
objects = {
/* Begin PBXBuildFile section */
0F4A88A33D93B6E100A1C001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F4A88A33D93B6E100A1C002 /* Assets.xcassets */; };
A10000000000000000000001 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* AppSettings.swift */; };
A10000000000000000000002 /* AppSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000002 /* AppSettingsController.swift */; };
A10000000000000000000003 /* AppSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* AppSettingsStore.swift */; };
A10000000000000000000004 /* WorkspaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000004 /* WorkspaceController.swift */; };
A10000000000000000000005 /* WorkspaceRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000005 /* WorkspaceRegistry.swift */; };
A10000000000000000000006 /* WorkspaceSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000006 /* WorkspaceSummary.swift */; };
A10000000000000000000007 /* AppSettingsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000008 /* AppSettingsStoreTests.swift */; };
A10000000000000000000008 /* WorkspaceRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* WorkspaceRegistryTests.swift */; };
A10000000000000000000009 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000A /* XCTest.framework */; };
A1000000000000000000000B /* ScreenContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000B /* ScreenContext.swift */; };
A1000000000000000000000C /* ScreenRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000C /* ScreenRegistryTests.swift */; };
A1000000000000000000000D /* ScreenRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000D /* ScreenRegistry.swift */; };
A1000000000000000000000E /* WorkspaceStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000E /* WorkspaceStore.swift */; };
A1000000000000000000000F /* WorkspaceStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000F /* WorkspaceStoreTests.swift */; };
A10000000000000000000010 /* NotchOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000010 /* NotchOrchestrator.swift */; };
A10000000000000000000011 /* NotchOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000011 /* NotchOrchestratorTests.swift */; };
A10000000000000000000012 /* ScreenContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000012 /* ScreenContextTests.swift */; };
A10000000000000000000013 /* WindowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000013 /* WindowCoordinator.swift */; };
A10000000000000000000014 /* WindowFrameCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000014 /* WindowFrameCalculatorTests.swift */; };
A10000000000000000000015 /* SettingsBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000015 /* SettingsBindings.swift */; };
A10000000000000000000016 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000016 /* GeneralSettingsView.swift */; };
A10000000000000000000017 /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000017 /* AppearanceSettingsView.swift */; };
A10000000000000000000018 /* AnimationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000018 /* AnimationSettingsView.swift */; };
A10000000000000000000019 /* TerminalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000019 /* TerminalSettingsView.swift */; };
A1000000000000000000001A /* HotkeySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000001A /* HotkeySettingsView.swift */; };
A1000000000000000000001B /* AboutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000001B /* AboutSettingsView.swift */; };
A1000000000000000000001C /* AppSettingsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000001C /* AppSettingsControllerTests.swift */; };
A1000000000000000000001D /* WorkspaceSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000001D /* WorkspaceSwitcherView.swift */; };
A1000000000000000000001E /* WorkspacesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000001E /* WorkspacesSettingsView.swift */; };
A1000000000000000000001F /* CommandNotchUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000001F /* CommandNotchUITests.swift */; };
A10000000000000000000020 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000A /* XCTest.framework */; };
2213F430F3D8A88033607CD2 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6359CF9DDF89413440300D /* NotchSettings.swift */; };
247C6F84E7ADE7AED43381E2 /* CommandNotchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B671125208055E5334CB85E /* CommandNotchApp.swift */; };
295653929D5B9C0E6C90D6D7 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 032AECA58EA4C274BE9F3320 /* SwiftTerm */; };
37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B598809B19C892470DE7268 /* TerminalSession.swift */; };
3A1F0C4BE9D84A5C8E2B7101 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */; };
4566E6B87CB62AF5C8D4B9D8 /* NotchShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC09C538CBE7C2D072008B2 /* NotchShape.swift */; };
4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B72743F178231E0B06DD3DE /* HotkeyManager.swift */; };
5B14FC23928E817FEB8D2A74 /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FEFF9074A85F02C43D9408 /* NotchWindow.swift */; };
7BD705CA6A34117929B362EC /* HotkeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490C53139360D970099D8F3D /* HotkeyRecorderView.swift */; };
7DE94234DC42EAB79896E176 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5CB3313B230019D0E988AFE /* SettingsView.swift */; };
7EA51C3720BED7E6189E057D /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F009B75D078A5070B5EA9738 /* TabBar.swift */; };
81A912E3E16165D999882078 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0779490DE9020FBBC464BE /* AppDelegate.swift */; };
888C45C650327089EBD39B2E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BA7F4716DA3909DA8BC381 /* ContentView.swift */; };
88EBFBB2292659EA7C42A8F9 /* HotkeyBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB81B6DA7126E1F5FBCC8B8 /* HotkeyBinding.swift */; };
8FC731CF99AB4C1C10C16FAB /* PopoutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1CD1DE020F0467AFB98DE3 /* PopoutWindowController.swift */; };
A70FDB7EEEB895D475ED96E8 /* NotchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5C99B7CD7F60E55844E40C /* NotchState.swift */; };
BE5E64222CF5689AC7088683 /* ScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A290D4D21D6C01A583A372 /* ScreenManager.swift */; };
C4C93F2911B41BC19A2AE934 /* SettingsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A973877BCE6084D0EBBBDBD /* SettingsWindowController.swift */; };
CC26C1677258E44F0D7B106A /* SwiftTermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47000112562615C7E59489 /* SwiftTermView.swift */; };
E9A064422790735E033E534F /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6843B571B41986DE386F5F /* TerminalManager.swift */; };
EA604F3F38D6638C7236CDC2 /* LaunchAtLoginHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B567F3B5D006D2B35630CFF /* LaunchAtLoginHelper.swift */; };
F0130A88D1453CD199FA65D7 /* NSScreen+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0CED6A0F25A6E57D8AA308A /* NSScreen+Extensions.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
A60000000000000000000001 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F72A983360EF3F99042A4895 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1485207FA11756EC2DF4F08B;
remoteInfo = CommandNotch;
};
A60000000000000000000002 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F72A983360EF3F99042A4895 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1485207FA11756EC2DF4F08B;
remoteInfo = CommandNotch;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
02FEFF9074A85F02C43D9408 /* NotchWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchWindow.swift; sourceTree = "<group>"; };
0A973877BCE6084D0EBBBDBD /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
0B567F3B5D006D2B35630CFF /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; };
0F4A88A33D93B6E100A1C002 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A20000000000000000000001 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
A20000000000000000000002 /* AppSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsController.swift; sourceTree = "<group>"; };
A20000000000000000000003 /* AppSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsStore.swift; sourceTree = "<group>"; };
A20000000000000000000004 /* WorkspaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceController.swift; sourceTree = "<group>"; };
A20000000000000000000005 /* WorkspaceRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRegistry.swift; sourceTree = "<group>"; };
A20000000000000000000006 /* WorkspaceSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSummary.swift; sourceTree = "<group>"; };
A20000000000000000000007 /* CommandNotchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommandNotchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A20000000000000000000008 /* AppSettingsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsStoreTests.swift; sourceTree = "<group>"; };
A20000000000000000000009 /* WorkspaceRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRegistryTests.swift; sourceTree = "<group>"; };
A2000000000000000000000A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = System/Library/Frameworks/XCTest.framework; sourceTree = SDKROOT; };
A2000000000000000000000B /* ScreenContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenContext.swift; sourceTree = "<group>"; };
A2000000000000000000000C /* ScreenRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenRegistryTests.swift; sourceTree = "<group>"; };
A2000000000000000000000D /* ScreenRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenRegistry.swift; sourceTree = "<group>"; };
A2000000000000000000000E /* WorkspaceStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStore.swift; sourceTree = "<group>"; };
A2000000000000000000000F /* WorkspaceStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStoreTests.swift; sourceTree = "<group>"; };
A20000000000000000000010 /* NotchOrchestrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOrchestrator.swift; sourceTree = "<group>"; };
A20000000000000000000011 /* NotchOrchestratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOrchestratorTests.swift; sourceTree = "<group>"; };
A20000000000000000000012 /* ScreenContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenContextTests.swift; sourceTree = "<group>"; };
A20000000000000000000013 /* WindowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCoordinator.swift; sourceTree = "<group>"; };
A20000000000000000000014 /* WindowFrameCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFrameCalculatorTests.swift; sourceTree = "<group>"; };
A20000000000000000000015 /* SettingsBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBindings.swift; sourceTree = "<group>"; };
A20000000000000000000016 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
A20000000000000000000017 /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
A20000000000000000000018 /* AnimationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationSettingsView.swift; sourceTree = "<group>"; };
A20000000000000000000019 /* TerminalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSettingsView.swift; sourceTree = "<group>"; };
A2000000000000000000001A /* HotkeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeySettingsView.swift; sourceTree = "<group>"; };
A2000000000000000000001B /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = "<group>"; };
A2000000000000000000001C /* AppSettingsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsControllerTests.swift; sourceTree = "<group>"; };
A2000000000000000000001D /* WorkspaceSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSwitcherView.swift; sourceTree = "<group>"; };
A2000000000000000000001E /* WorkspacesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacesSettingsView.swift; sourceTree = "<group>"; };
A2000000000000000000001F /* CommandNotchUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandNotchUITests.swift; sourceTree = "<group>"; };
A20000000000000000000020 /* CommandNotchUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommandNotchUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
15A290D4D21D6C01A583A372 /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; };
1E47000112562615C7E59489 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; };
1FC09C538CBE7C2D072008B2 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
20BA7F4716DA3909DA8BC381 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
2C5C99B7CD7F60E55844E40C /* NotchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchState.swift; sourceTree = "<group>"; };
3B72743F178231E0B06DD3DE /* HotkeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyManager.swift; sourceTree = "<group>"; };
490C53139360D970099D8F3D /* HotkeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderView.swift; sourceTree = "<group>"; };
4B671125208055E5334CB85E /* CommandNotchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandNotchApp.swift; sourceTree = "<group>"; };
4BB81B6DA7126E1F5FBCC8B8 /* HotkeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyBinding.swift; sourceTree = "<group>"; };
5C0779490DE9020FBBC464BE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
665CFC051CF185B71199608D /* CommandNotch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CommandNotch.app; sourceTree = BUILT_PRODUCTS_DIR; };
7B598809B19C892470DE7268 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = "<group>"; };
3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; };
9547A79F60E46F4521A70674 /* CommandNotch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CommandNotch.entitlements; sourceTree = "<group>"; };
AA6359CF9DDF89413440300D /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = "<group>"; };
BA6843B571B41986DE386F5F /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = "<group>"; };
C5CB3313B230019D0E988AFE /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
EA1CD1DE020F0467AFB98DE3 /* PopoutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoutWindowController.swift; sourceTree = "<group>"; };
F009B75D078A5070B5EA9738 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
F0CED6A0F25A6E57D8AA308A /* NSScreen+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+Extensions.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
6085DF2BDFFB2A99C4ABD514 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
295653929D5B9C0E6C90D6D7 /* SwiftTerm in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A40000000000000000000001 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A10000000000000000000009 /* XCTest.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A40000000000000000000003 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A10000000000000000000020 /* XCTest.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0EF94ED46B4860C241540F0A /* Resources */ = {
isa = PBXGroup;
children = (
0F4A88A33D93B6E100A1C002 /* Assets.xcassets */,
9547A79F60E46F4521A70674 /* CommandNotch.entitlements */,
);
path = Resources;
sourceTree = "<group>";
};
27C90448ECAC906F0DA429C0 /* Managers */ = {
isa = PBXGroup;
children = (
3B72743F178231E0B06DD3DE /* HotkeyManager.swift */,
0B567F3B5D006D2B35630CFF /* LaunchAtLoginHelper.swift */,
EA1CD1DE020F0467AFB98DE3 /* PopoutWindowController.swift */,
15A290D4D21D6C01A583A372 /* ScreenManager.swift */,
0A973877BCE6084D0EBBBDBD /* SettingsWindowController.swift */,
A20000000000000000000013 /* WindowCoordinator.swift */,
);
path = Managers;
sourceTree = "<group>";
};
792DD4F8C079680683D8FF7A /* Products */ = {
isa = PBXGroup;
children = (
665CFC051CF185B71199608D /* CommandNotch.app */,
A20000000000000000000007 /* CommandNotchTests.xctest */,
A20000000000000000000020 /* CommandNotchUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
A30000000000000000000001 /* CommandNotchTests */ = {
isa = PBXGroup;
children = (
A20000000000000000000008 /* AppSettingsStoreTests.swift */,
A2000000000000000000001C /* AppSettingsControllerTests.swift */,
A20000000000000000000012 /* ScreenContextTests.swift */,
A2000000000000000000000C /* ScreenRegistryTests.swift */,
A20000000000000000000011 /* NotchOrchestratorTests.swift */,
A20000000000000000000014 /* WindowFrameCalculatorTests.swift */,
A20000000000000000000009 /* WorkspaceRegistryTests.swift */,
A2000000000000000000000F /* WorkspaceStoreTests.swift */,
);
path = CommandNotchTests;
sourceTree = "<group>";
};
A30000000000000000000002 /* CommandNotchUITests */ = {
isa = PBXGroup;
children = (
A2000000000000000000001F /* CommandNotchUITests.swift */,
);
path = CommandNotchUITests;
sourceTree = "<group>";
};
869AD33E1CDEB9CBAD401BA6 /* Models */ = {
isa = PBXGroup;
children = (
A20000000000000000000001 /* AppSettings.swift */,
A20000000000000000000002 /* AppSettingsController.swift */,
A20000000000000000000003 /* AppSettingsStore.swift */,
4BB81B6DA7126E1F5FBCC8B8 /* HotkeyBinding.swift */,
AA6359CF9DDF89413440300D /* NotchSettings.swift */,
2C5C99B7CD7F60E55844E40C /* NotchState.swift */,
A20000000000000000000010 /* NotchOrchestrator.swift */,
A2000000000000000000000B /* ScreenContext.swift */,
A2000000000000000000000D /* ScreenRegistry.swift */,
BA6843B571B41986DE386F5F /* TerminalManager.swift */,
7B598809B19C892470DE7268 /* TerminalSession.swift */,
3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */,
A20000000000000000000004 /* WorkspaceController.swift */,
A20000000000000000000005 /* WorkspaceRegistry.swift */,
A20000000000000000000006 /* WorkspaceSummary.swift */,
A2000000000000000000000E /* WorkspaceStore.swift */,
);
path = Models;
sourceTree = "<group>";
};
8D95E0324E6AFC9E4DC0C087 /* Extensions */ = {
isa = PBXGroup;
children = (
F0CED6A0F25A6E57D8AA308A /* NSScreen+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
9E1CA4816F67033BBD52D8A3 /* CommandNotch */ = {
isa = PBXGroup;
children = (
5C0779490DE9020FBBC464BE /* AppDelegate.swift */,
20BA7F4716DA3909DA8BC381 /* ContentView.swift */,
4B671125208055E5334CB85E /* CommandNotchApp.swift */,
F32F526005A2589010E63C76 /* Components */,
8D95E0324E6AFC9E4DC0C087 /* Extensions */,
27C90448ECAC906F0DA429C0 /* Managers */,
869AD33E1CDEB9CBAD401BA6 /* Models */,
0EF94ED46B4860C241540F0A /* Resources */,
C2B8955F4D0A1DAA7E60326A /* Views */,
);
path = CommandNotch;
sourceTree = "<group>";
};
C2B8955F4D0A1DAA7E60326A /* Views */ = {
isa = PBXGroup;
children = (
A2000000000000000000001B /* AboutSettingsView.swift */,
A20000000000000000000017 /* AppearanceSettingsView.swift */,
A20000000000000000000018 /* AnimationSettingsView.swift */,
A20000000000000000000016 /* GeneralSettingsView.swift */,
A2000000000000000000001A /* HotkeySettingsView.swift */,
A20000000000000000000015 /* SettingsBindings.swift */,
C5CB3313B230019D0E988AFE /* SettingsView.swift */,
A20000000000000000000019 /* TerminalSettingsView.swift */,
A2000000000000000000001E /* WorkspacesSettingsView.swift */,
A2000000000000000000001D /* WorkspaceSwitcherView.swift */,
);
path = Views;
sourceTree = "<group>";
};
F32F526005A2589010E63C76 /* Components */ = {
isa = PBXGroup;
children = (
490C53139360D970099D8F3D /* HotkeyRecorderView.swift */,
1FC09C538CBE7C2D072008B2 /* NotchShape.swift */,
02FEFF9074A85F02C43D9408 /* NotchWindow.swift */,
1E47000112562615C7E59489 /* SwiftTermView.swift */,
F009B75D078A5070B5EA9738 /* TabBar.swift */,
);
path = Components;
sourceTree = "<group>";
};
FC6F23514BFE2235BD4154E8 = {
isa = PBXGroup;
children = (
9E1CA4816F67033BBD52D8A3 /* CommandNotch */,
A30000000000000000000001 /* CommandNotchTests */,
A30000000000000000000002 /* CommandNotchUITests */,
792DD4F8C079680683D8FF7A /* Products */,
);
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
1485207FA11756EC2DF4F08B /* CommandNotch */ = {
isa = PBXNativeTarget;
buildConfigurationList = 74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "CommandNotch" */;
buildPhases = (
F3C6D5CD1247D246A3F6F7AB /* Sources */,
6085DF2BDFFB2A99C4ABD514 /* Frameworks */,
0F4A88A33D93B6E100A1C003 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = CommandNotch;
packageProductDependencies = (
032AECA58EA4C274BE9F3320 /* SwiftTerm */,
);
productName = CommandNotch;
productReference = 665CFC051CF185B71199608D /* CommandNotch.app */;
productType = "com.apple.product-type.application";
};
A50000000000000000000001 /* CommandNotchTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = A90000000000000000000001 /* Build configuration list for PBXNativeTarget "CommandNotchTests" */;
buildPhases = (
A40000000000000000000002 /* Sources */,
A40000000000000000000001 /* Frameworks */,
);
buildRules = (
);
dependencies = (
A70000000000000000000001 /* PBXTargetDependency */,
);
name = CommandNotchTests;
packageProductDependencies = (
);
productName = CommandNotchTests;
productReference = A20000000000000000000007 /* CommandNotchTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
A50000000000000000000002 /* CommandNotchUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = A90000000000000000000002 /* Build configuration list for PBXNativeTarget "CommandNotchUITests" */;
buildPhases = (
A40000000000000000000004 /* Sources */,
A40000000000000000000003 /* Frameworks */,
);
buildRules = (
);
dependencies = (
A70000000000000000000002 /* PBXTargetDependency */,
);
name = CommandNotchUITests;
packageProductDependencies = (
);
productName = CommandNotchUITests;
productReference = A20000000000000000000020 /* CommandNotchUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
F72A983360EF3F99042A4895 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1600;
};
buildConfigurationList = D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "CommandNotch" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = FC6F23514BFE2235BD4154E8;
minimizedProjectReferenceProxies = 1;
packageReferences = (
80F03B77566BF59C9941EAD4 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
);
projectDirPath = "";
projectRoot = "";
targets = (
1485207FA11756EC2DF4F08B /* CommandNotch */,
A50000000000000000000001 /* CommandNotchTests */,
A50000000000000000000002 /* CommandNotchUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0F4A88A33D93B6E100A1C003 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0F4A88A33D93B6E100A1C001 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
A40000000000000000000002 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A10000000000000000000007 /* AppSettingsStoreTests.swift in Sources */,
A1000000000000000000001C /* AppSettingsControllerTests.swift in Sources */,
A10000000000000000000012 /* ScreenContextTests.swift in Sources */,
A1000000000000000000000C /* ScreenRegistryTests.swift in Sources */,
A10000000000000000000011 /* NotchOrchestratorTests.swift in Sources */,
A10000000000000000000014 /* WindowFrameCalculatorTests.swift in Sources */,
A10000000000000000000008 /* WorkspaceRegistryTests.swift in Sources */,
A1000000000000000000000F /* WorkspaceStoreTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A40000000000000000000004 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A1000000000000000000001F /* CommandNotchUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F3C6D5CD1247D246A3F6F7AB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A10000000000000000000001 /* AppSettings.swift in Sources */,
A10000000000000000000002 /* AppSettingsController.swift in Sources */,
A10000000000000000000003 /* AppSettingsStore.swift in Sources */,
81A912E3E16165D999882078 /* AppDelegate.swift in Sources */,
888C45C650327089EBD39B2E /* ContentView.swift in Sources */,
247C6F84E7ADE7AED43381E2 /* CommandNotchApp.swift in Sources */,
88EBFBB2292659EA7C42A8F9 /* HotkeyBinding.swift in Sources */,
4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */,
7BD705CA6A34117929B362EC /* HotkeyRecorderView.swift in Sources */,
EA604F3F38D6638C7236CDC2 /* LaunchAtLoginHelper.swift in Sources */,
F0130A88D1453CD199FA65D7 /* NSScreen+Extensions.swift in Sources */,
2213F430F3D8A88033607CD2 /* NotchSettings.swift in Sources */,
4566E6B87CB62AF5C8D4B9D8 /* NotchShape.swift in Sources */,
A70FDB7EEEB895D475ED96E8 /* NotchState.swift in Sources */,
A10000000000000000000010 /* NotchOrchestrator.swift in Sources */,
5B14FC23928E817FEB8D2A74 /* NotchWindow.swift in Sources */,
8FC731CF99AB4C1C10C16FAB /* PopoutWindowController.swift in Sources */,
A1000000000000000000000B /* ScreenContext.swift in Sources */,
A1000000000000000000000D /* ScreenRegistry.swift in Sources */,
BE5E64222CF5689AC7088683 /* ScreenManager.swift in Sources */,
7DE94234DC42EAB79896E176 /* SettingsView.swift in Sources */,
C4C93F2911B41BC19A2AE934 /* SettingsWindowController.swift in Sources */,
CC26C1677258E44F0D7B106A /* SwiftTermView.swift in Sources */,
7EA51C3720BED7E6189E057D /* TabBar.swift in Sources */,
E9A064422790735E033E534F /* TerminalManager.swift in Sources */,
37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */,
3A1F0C4BE9D84A5C8E2B7101 /* TerminalTheme.swift in Sources */,
A10000000000000000000013 /* WindowCoordinator.swift in Sources */,
A10000000000000000000004 /* WorkspaceController.swift in Sources */,
A10000000000000000000005 /* WorkspaceRegistry.swift in Sources */,
A10000000000000000000006 /* WorkspaceSummary.swift in Sources */,
A1000000000000000000000E /* WorkspaceStore.swift in Sources */,
A1000000000000000000001B /* AboutSettingsView.swift in Sources */,
A10000000000000000000017 /* AppearanceSettingsView.swift in Sources */,
A10000000000000000000018 /* AnimationSettingsView.swift in Sources */,
A10000000000000000000016 /* GeneralSettingsView.swift in Sources */,
A1000000000000000000001A /* HotkeySettingsView.swift in Sources */,
A10000000000000000000015 /* SettingsBindings.swift in Sources */,
A10000000000000000000019 /* TerminalSettingsView.swift in Sources */,
A1000000000000000000001E /* WorkspacesSettingsView.swift in Sources */,
A1000000000000000000001D /* WorkspaceSwitcherView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
A70000000000000000000001 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 1485207FA11756EC2DF4F08B /* CommandNotch */;
targetProxy = A60000000000000000000001 /* PBXContainerItemProxy */;
};
A70000000000000000000002 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 1485207FA11756EC2DF4F08B /* CommandNotch */;
targetProxy = A60000000000000000000002 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
0B8C784EF064E46C44076D6B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_USE_OPTIMIZATION_PROFILE = YES;
CODE_SIGN_ENTITLEMENTS = CommandNotch/Resources/CommandNotch.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
COMBINE_HIDPI_IMAGES = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = CommandNotch;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.3;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
PRODUCT_NAME = CommandNotch;
SDKROOT = macosx;
};
name = Release;
};
3595A9212275B9AEC4448C64 /* 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;
};
7020C02C1BDF63690CC9A3AC /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_USE_OPTIMIZATION_PROFILE = YES;
CODE_SIGN_ENTITLEMENTS = CommandNotch/Resources/CommandNotch.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
COMBINE_HIDPI_IMAGES = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = CommandNotch;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.3;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
PRODUCT_NAME = CommandNotch;
SDKROOT = macosx;
};
name = Debug;
};
A80000000000000000000001 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@loader_path/../Frameworks",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.10;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CommandNotch.app/Contents/MacOS/CommandNotch";
TEST_TARGET_NAME = CommandNotch;
};
name = Debug;
};
A80000000000000000000002 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@loader_path/../Frameworks",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_VERSION = 5.10;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CommandNotch.app/Contents/MacOS/CommandNotch";
TEST_TARGET_NAME = CommandNotch;
};
name = Release;
};
A80000000000000000000003 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.10;
TEST_TARGET_NAME = CommandNotch;
};
name = Debug;
};
A80000000000000000000004 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.CommandNotchUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_VERSION = 5.10;
TEST_TARGET_NAME = CommandNotch;
};
name = Release;
};
BC741C4C821EA399B645E547 /* 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;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "CommandNotch" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7020C02C1BDF63690CC9A3AC /* Debug */,
0B8C784EF064E46C44076D6B /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
A90000000000000000000001 /* Build configuration list for PBXNativeTarget "CommandNotchTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A80000000000000000000001 /* Debug */,
A80000000000000000000002 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
A90000000000000000000002 /* Build configuration list for PBXNativeTarget "CommandNotchUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A80000000000000000000003 /* Debug */,
A80000000000000000000004 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "CommandNotch" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3595A9212275B9AEC4448C64 /* Debug */,
BC741C4C821EA399B645E547 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
80F03B77566BF59C9941EAD4 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
032AECA58EA4C274BE9F3320 /* SwiftTerm */ = {
isa = XCSwiftPackageProductDependency;
package = 80F03B77566BF59C9941EAD4 /* XCRemoteSwiftPackageReference "SwiftTerm" */;
productName = SwiftTerm;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = F72A983360EF3F99042A4895 /* Project object */;
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Harvey Zuccon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

111
README.md Normal file
View File

@@ -0,0 +1,111 @@
# CommandNotch
<p align="center">
<img src="icons/Downterm-icon-256.png" width="128" alt="CommandNotch icon">
</p>
<p align="center">
A drop-down terminal for macOS that lives in the notch area.
</p>
<p align="center">
<img src="https://img.shields.io/badge/platform-macOS%2014%2B-black" alt="macOS 14+">
<img src="https://img.shields.io/badge/UI-SwiftUI%20%2B%20AppKit-black" alt="SwiftUI and AppKit">
<img src="https://img.shields.io/badge/license-MIT-black" alt="MIT License">
</p>
CommandNotch is a notch-native terminal overlay for macOS. It gives you a fast shell without switching spaces, keeping a full Terminal window open, or breaking your flow. Open it with a hotkey, drop into a shell, then get out of the way just as quickly.
The current Xcode target and bundle name are still `CommandNotch`, but the project is being presented publicly as **CommandNotch**.
## Why CommandNotch
- You want a terminal that is always one shortcut away.
- You like the idea of a terminal living in the menu bar / notch area instead of a full window.
- You work across multiple displays and want each screen to keep its own notch state.
- You want lightweight workspaces, detachable tabs, and shell access without giving up native macOS feel.
## Features
- Native macOS notch overlay with open and closed states.
- Fast shell sessions powered by SwiftTerm and a local login shell.
- Multiple tabs with hotkeys for new, close, next, previous, and direct tab switching.
- Workspace support, including shared workspaces across screens.
- Multi-display awareness with per-screen assignment and presentation state.
- Detachable tabs that can pop out into standalone terminal windows.
- Terminal themes: Classic, Xterm, Solarized Dark, Dracula, and Nord.
- Configurable terminal size presets with optional hotkeys.
- Hover-to-open behavior and animation tuning.
- Launch at login support.
- Global toggle hotkey and notch-scoped shortcut handling.
- Terminal-friendly macOS key behavior for `Command+Arrow`, `Option+Arrow`, `Command+Backspace`, and `Command+L`.
## Gallery
### Demo
[![CommandNotch demo video](.github/assets/CommandNotch-Open.png)](.github/assets/CommandNotch-Demo.mp4)
Click the preview above to watch the demo recording.
### Open Notch Terminal
![CommandNotch open notch terminal](.github/assets/CommandNotch-Open.png)
### Settings
![CommandNotch settings window](.github/assets/CommandNotch-Settings.png)
## Getting Started
### Requirements
- macOS 14 or later
- Xcode 16 or later
- Homebrew `xcodegen`
### Build
```bash
cd CommandNotch
xcodegen generate --spec project.yml
open CommandNotch.xcodeproj
```
Or from the command line:
```bash
cd CommandNotch
xcodegen generate --spec project.yml
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
xcodebuild build -project CommandNotch.xcodeproj -scheme CommandNotch -destination 'platform=macOS'
```
## Project Layout
```text
CommandNotch/
├── CommandNotch/ # XcodeGen spec, app target, tests
├── docs/ # architecture and planning notes
├── icons/ # app icons and branding assets
└── .github/assets/ # README screenshots and support assets
```
## Contributing
Contributions are welcome. If you want to fix bugs, improve the UX, tighten the architecture, or help polish the public release, start with [CONTRIBUTING.md](CONTRIBUTING.md).
## Help Fund Development
If CommandNotch saves you time, support helps justify more polish and faster iteration.
- Ko-fi: https://ko-fi.com/harvmaster
- BCH: `bitcoincash:zq5xlhahsk8svzk562m3kwrzgd9hrm80mcu8slnzv3`
<p align="center">
<img src=".github/assets/bch-receiving-address.png" width="220" alt="BCH receiving address QR code">
</p>
## License
CommandNotch is released under the [MIT License](LICENSE).

View File

@@ -618,7 +618,7 @@ Exit criteria:
## Proposed File Layout ## Proposed File Layout
```text ```text
Downterm/CommandNotch/ CommandNotch/CommandNotch/
App/ App/
CommandNotchApp.swift CommandNotchApp.swift
AppDelegate.swift AppDelegate.swift

View File

@@ -0,0 +1,351 @@
# Workspace Split Pane Plan
## Purpose
Capture the agreed design for workspace-owned split terminals so the feature can be implemented later without redoing scope and architecture decisions.
## Agreed Scope
- Split layouts belong to the workspace, not the screen.
- A workspace may only be actively presented on one screen at a time.
- Split layouts should be visible the same way when that workspace is opened on another monitor.
- Nested splits are supported.
- Any two tabs may be joined into a split.
- Closing a pane causes its sibling to expand.
- Split layout must survive relaunch.
## Non-Goals
- Simultaneous live rendering of the same workspace on multiple screens.
- Full multi-user or collaborative state sync.
- Arbitrary freeform pane layout. The model is tree-based binary splits.
- Solving all interaction polish in the first pass.
## Current Constraint
The current workspace model is flat:
- `WorkspaceController` owns `[TerminalSession]`
- one tab is active at a time
- `ContentView` renders exactly one active `SwiftTermView`
This means split panes are not an additive UI feature. They require a workspace model refactor.
## Recommended Model
Do not model splits as "adjacent tabs with special behavior" internally.
Instead:
1. A workspace owns ordered top-level tabs.
2. Each top-level tab owns a pane tree.
3. Pane tree leaves own `TerminalSession` instances.
4. Pane tree internal nodes represent binary splits.
5. The tab bar can visually present grouped/joined tabs based on the active tab's pane composition.
This keeps the domain model clean while still allowing the desired visual metaphor.
## Proposed Domain Types
```swift
typealias WorkspaceTabID = UUID
typealias PaneID = UUID
struct WorkspaceTab: Identifiable, Equatable, Codable {
var id: WorkspaceTabID
var rootPane: PaneNode
var selectedPaneID: PaneID?
var titleMode: WorkspaceTabTitleMode
}
enum PaneNode: Equatable, Codable, Identifiable {
case leaf(PaneLeaf)
case split(PaneSplit)
var id: PaneID { ... }
}
struct PaneLeaf: Equatable, Codable, Identifiable {
var id: PaneID
var sessionID: UUID
}
struct PaneSplit: Equatable, Codable, Identifiable {
var id: PaneID
var axis: SplitAxis
var ratio: Double
var first: PaneNode
var second: PaneNode
}
enum SplitAxis: String, Codable {
case horizontal
case vertical
}
```
Notes:
- `ratio` should be persisted so user-adjusted divider positions survive relaunch.
- `selectedPaneID` allows focus-based commands such as split active pane, close active pane, and move focus.
- The persisted model should store session identity separately from runtime AppKit/SwiftTerm objects.
## Runtime Ownership
Global:
- workspace summaries
- workspace ordering
- workspace-to-screen assignment
- hotkeys
Workspace:
- ordered top-level tabs
- active top-level tab
- per-tab pane tree
- active/selected pane
- terminal sessions for pane leaves
Screen:
- which workspace is assigned
- notch open/close state
- geometry and transitions
This remains aligned with the existing workspace architecture.
## Persistence Strategy
Split layout must survive relaunch, so workspace persistence needs to grow beyond summary metadata.
Recommended approach:
1. Keep `WorkspaceSummary` for list metadata only.
2. Introduce a persisted `WorkspaceDocument` or `WorkspaceSnapshot` per workspace.
3. Persist:
- top-level tabs
- pane tree
- active tab ID
- selected pane ID
- divider ratios
4. Do not attempt to persist shell process contents.
5. On relaunch, recreate sessions for pane leaves and restore layout structure only.
Important distinction:
- layout persistence is required
- terminal process continuity is not
## UI Model
### Main Content
The active top-level tab renders as a recursive split tree.
- leaf node -> one `SwiftTermView`
- split node -> `HSplitView`/`VSplitView` equivalent SwiftUI container with draggable divider
### Tab Bar
Top-level tabs remain the primary navigation unit.
Visual behavior:
- a normal unsplit tab looks like today
- a joined/split tab should look grouped
- grouped tabs should expose child pane titles visually inside the tab item
Recommended first-pass appearance:
- one outer tab pill per top-level tab
- inside that pill, show compact child title chips for each leaf pane
- highlight the selected pane's chip
This gives the "tabs are joined" feel without making pane leaves first-class top-level tabs in the data model.
## Command Model
New actions likely needed:
- split active pane horizontally
- split active pane vertically
- focus next pane
- focus previous pane
- close active pane
- resize focused split divider
- join tab A into tab B
- detach pane into new top-level tab
Semantics:
- joining any two tabs merges one tab's root pane into the other's tree
- the source top-level tab is removed
- the destination top-level tab remains
- sibling expansion on close is standard tree collapse
## Join Semantics
Joining any two tabs should work as:
1. Choose destination tab.
2. Choose source tab.
3. Replace destination root with a new split node:
- first child = old destination root
- second child = source root
4. Remove source tab from workspace order.
5. Select a predictable pane, preferably the source pane that was just added.
An explicit split axis should be required for the join action.
## Close Semantics
When closing a pane:
- if the pane has a sibling, the sibling expands into the parent's position
- if the pane was the only leaf in a top-level tab:
- close the whole top-level tab if more than one tab exists
- otherwise create a replacement shell pane, matching current single-tab safety behavior
## Focus Semantics
Pane focus must become explicit.
Recommended rules:
- mouse click focuses that pane
- splitting focuses the new pane
- joining focuses the moved-in pane
- closing a pane focuses the surviving sibling
- top-level tab switch restores the previously selected pane in that tab
## Migration Path
Implement in stages to reduce risk.
### Stage 1: Domain Refactor
- Replace flat workspace tabs with top-level tab objects.
- Introduce pane tree types.
- Keep only one leaf per tab initially so behavior is unchanged.
### Stage 2: Runtime Layout Rendering
- Render pane trees recursively in `ContentView`.
- Add active pane selection.
- Keep persistence off until runtime behavior is stable.
### Stage 3: Split Actions
- Split active pane horizontally/vertically.
- Close pane with sibling expansion.
- Basic focus movement.
### Stage 4: Joined Tab UI
- Update tab bar to show grouped child pane chips.
- Surface active pane clearly.
### Stage 5: Join / Unjoin Flows
- Join any two tabs with explicit axis choice.
- Support promoting a pane back to its own top-level tab if needed.
### Stage 6: Persistence
- Persist pane trees and top-level tab state.
- Recreate sessions on launch.
### Stage 7: Hotkeys and Polish
- Add pane-focused shortcuts.
- Add divider dragging polish.
- Improve visual grouped-tab affordances.
## Main Risks
### 1. Model complexity
The current flat `[TerminalSession]` model is simple. Tree-based layout introduces more state, more edge cases, and more focus semantics.
Mitigation:
- refactor the data model before touching complex UI
- keep top-level tabs and pane leaves distinct
### 2. SwiftTerm view ownership
`TerminalView` cannot be mounted in multiple places safely.
Mitigation:
- preserve current rule: only one active presenting screen per workspace
- keep one runtime `TerminalSession` per pane leaf
### 3. Persistence mismatch
Persisting layout is easy compared with persisting process state.
Mitigation:
- persist layout and selection only
- recreate shell sessions on launch
### 4. Joined-tab UX ambiguity
If grouped tabs are also used as pane chips, the interaction model can get confusing.
Mitigation:
- preserve top-level tabs as the real navigation unit
- use internal chips only as secondary indicators/actions
## Recommended First Implementation Boundary
The first deliverable should include:
- nested binary split trees
- active pane focus
- split and close pane actions
- sibling expansion
- grouped tab appearance
- join any two tabs
- persisted layout across relaunch
- same workspace layout when moved to another screen
The first deliverable should not include:
- simultaneous same-workspace rendering on multiple screens
- drag-and-drop tree editing
- restoring running shell process contents
## Files Likely Impacted
- `CommandNotch/CommandNotch/Models/WorkspaceController.swift`
- `CommandNotch/CommandNotch/Models/WorkspaceRegistry.swift`
- `CommandNotch/CommandNotch/Models/WorkspaceStore.swift`
- `CommandNotch/CommandNotch/ContentView.swift`
- `CommandNotch/CommandNotch/Components/TabBar.swift`
- `CommandNotch/CommandNotch/Models/TerminalSession.swift`
- workspace-related tests
Likely new files:
- pane tree domain types
- persisted workspace document types
- split-pane rendering view(s)
- pane-focused command helpers
## Decision Summary
The feature is feasible.
The correct architecture is:
- workspace-owned split tree
- top-level tabs remain the primary unit
- grouped tab visuals are a UI layer over the pane tree
- one presenting screen per workspace
- persisted layout, not persisted process contents
That is the implementation direction to use when this work is resumed.