From 1441f83388091111bb55d146445146054a129752 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 4 Mar 2026 14:06:51 +0530 Subject: [PATCH] Update stuff --- DynamicIsland/Localizable.xcstrings | 123 +++++++++- .../Live activities/InlineHUD.swift | 2 +- .../components/Notch/NotchTerminalView.swift | 5 +- .../components/Settings/SettingsView.swift | 134 ++++++++++- DynamicIsland/managers/TerminalManager.swift | 214 ++++++++++++++++-- DynamicIsland/models/Constants.swift | 8 + 6 files changed, 456 insertions(+), 30 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 459d121..412ac56 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -1595,6 +1595,10 @@ } } }, + "±10 Seconds" : { + "comment" : "Display name for the \"±10 Seconds\" music skip behavior option.", + "isCommentAutoGenerated" : true + }, "0" : { "comment" : "A label displayed in a circular gauge to indicate the minimum battery level.", "isCommentAutoGenerated" : true, @@ -1911,6 +1915,10 @@ } } }, + "Accent" : { + "comment" : "Display name for the \"Accent\" caps lock indicator tint mode.", + "isCommentAutoGenerated" : true + }, "Accent color" : { "localizations" : { "ar" : { @@ -2256,6 +2264,10 @@ } } }, + "Adaptive gradient" : { + "comment" : "Name of the timer icon color mode option that uses an adaptive gradient.", + "isCommentAutoGenerated" : true + }, "Add" : { "localizations" : { "ar" : { @@ -2748,9 +2760,8 @@ } } }, - "Adds a Guake-style dropdown terminal tab. The terminal runs a shell session that persists while the notch is open." : { - "comment" : "A description of the terminal settings feature.", - "isCommentAutoGenerated" : true + "Adds a Guake-style dropdown terminal tab. The terminal session persists across notch open/close cycles." : { + }, "Adds a notch HUD when Caps Lock is enabled, with optional label and tint controls." : { "localizations" : { @@ -3352,6 +3363,9 @@ "Allow minimalistic overrides" : { "comment" : "Label for a toggle that allows minimalistic overrides in notch experiences.", "isCommentAutoGenerated" : true + }, + "Allow mouse reporting" : { + }, "Allowed Features" : { "comment" : "A label describing the allowed features for an app extension.", @@ -3849,6 +3863,10 @@ } } }, + "Apple Music" : { + "comment" : "Localized name for the \"Now Playing\" media controller type.", + "isCommentAutoGenerated" : true + }, "Applies the notch-style parallax effect to the lock screen media widget album art." : { "localizations" : { "pt-BR" : { @@ -4247,6 +4265,9 @@ } } } + }, + "Background" : { + }, "Backlight" : { "comment" : "A control for adjusting the keyboard backlight intensity.", @@ -4260,6 +4281,10 @@ } } }, + "Bar" : { + "comment" : "Localized name for the 'Bar' timer progress style.", + "isCommentAutoGenerated" : true + }, "Battery" : { "localizations" : { "ar" : { @@ -4825,6 +4850,9 @@ } } } + }, + "Bold text as bright colors" : { + }, "Boost your productivity with Clipboard Manager" : { "localizations" : { @@ -6877,6 +6905,9 @@ } } } + }, + "Colors" : { + }, "Coming soon" : { "localizations" : { @@ -7477,6 +7508,12 @@ } } } + }, + "Cursor" : { + + }, + "Cursor style" : { + }, "Custom" : { "extractionState" : "stale", @@ -8518,6 +8555,10 @@ } } }, + "Digital" : { + "comment" : "The display name for the \"Digital\" reminder presentation style.", + "isCommentAutoGenerated" : true + }, "Dimensions" : { "localizations" : { "pt-BR" : { @@ -11054,6 +11095,9 @@ "Font size" : { "comment" : "A label displayed above a slider that controls the font size of the terminal", "isCommentAutoGenerated" : true + }, + "Foreground" : { + }, "Free · %@" : { "comment" : "A label displaying the amount of free storage on a disk device. The argument is the number of free bytes on the disk.", @@ -11910,6 +11954,10 @@ } } }, + "Green" : { + "comment" : "Name of the \"Green\" Caps Lock Indicator Tint Mode.", + "isCommentAutoGenerated" : true + }, "Height:" : { "extractionState" : "stale", "localizations" : { @@ -12998,6 +13046,9 @@ } } } + }, + "Input" : { + }, "Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD." : { "comment" : "A description of the benefits of installing BetterDisplay.", @@ -15584,7 +15635,6 @@ } }, "Minutes" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -17800,6 +17850,13 @@ } } } + }, + "Now Playing" : { + "comment" : "Localized name for each media controller type.", + "isCommentAutoGenerated" : true + }, + "Number of lines kept in the scrollback buffer. Higher values use more memory." : { + }, "Off" : { "comment" : "Text displayed in a status label at the top of the screen indicating whether low power mode is on or off.", @@ -18130,6 +18187,12 @@ } } } + }, + "Option as Meta key" : { + + }, + "Option as Meta sends Esc+key instead of macOS special characters. Mouse reporting forwards mouse events to terminal applications like vim or tmux." : { + }, "Output devices" : { "comment" : "Section title for the list of available audio output devices in the media output selector.", @@ -18167,6 +18230,10 @@ } } }, + "Panel" : { + "comment" : "Display name for the \"Panel\" clipboard display mode.", + "isCommentAutoGenerated" : true + }, "Panel mode shows clipboard in a floating window near the notch." : { "comment" : "A description of the \"Panel\" clipboard display mode.", "isCommentAutoGenerated" : true, @@ -19781,6 +19848,10 @@ } } }, + "Ring" : { + "comment" : "Localized name for the \"Ring\" timer progress style.", + "isCommentAutoGenerated" : true + }, "Rotation:" : { "extractionState" : "stale", "localizations" : { @@ -20093,6 +20164,12 @@ } } } + }, + "Scrollback" : { + + }, + "Scrollback lines" : { + }, "Search clipboard..." : { "localizations" : { @@ -20601,6 +20678,10 @@ } } }, + "Separate Tab" : { + "comment" : "Name of the clipboard display mode option that opens the clipboard in a separate tab.", + "isCommentAutoGenerated" : true + }, "Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : { "comment" : "A description of the \"Separate Tab\" clipboard display mode.", "isCommentAutoGenerated" : true, @@ -20864,6 +20945,9 @@ } } } + }, + "Shell" : { + }, "Shell path" : { "comment" : "A setting option for the path to the shell to use in the terminal", @@ -21659,6 +21743,14 @@ } } }, + "Shows screen assistant as a dropdown attached to the AI button" : { + "comment" : "Description of the \"Popover\" display mode for the screen assistant.", + "isCommentAutoGenerated" : true + }, + "Shows screen assistant in a floating panel near the notch" : { + "comment" : "Description of the \"panel\" display mode for the screen assistant.", + "isCommentAutoGenerated" : true + }, "Shows the model's reasoning process before the final answer" : { "localizations" : { "fr" : { @@ -21721,6 +21813,10 @@ } } }, + "Skip forward or backward by ten seconds" : { + "comment" : "Description of the \"Skip forward or backward by ten seconds\" music skip behavior.", + "isCommentAutoGenerated" : true + }, "Slider color" : { "localizations" : { "ar" : { @@ -22289,6 +22385,10 @@ } } }, + "Spotify" : { + "comment" : "Add more media controller types here.", + "isCommentAutoGenerated" : true + }, "Square" : { "localizations" : { "ar" : { @@ -22401,6 +22501,10 @@ }, "Standard notch media controls are hidden. Re-enable the toggle above to restore them." : { + }, + "Standard previous/next track controls" : { + "comment" : "Description of a music skip behavior option that uses standard previous/next track controls.", + "isCommentAutoGenerated" : true }, "Start" : { "localizations" : { @@ -23762,6 +23866,10 @@ }, "shouldTranslate" : false }, + "Track Skip" : { + "comment" : "Name of the music skip behavior option that skips to the next track.", + "isCommentAutoGenerated" : true + }, "Transform" : { "extractionState" : "stale", "localizations" : { @@ -25315,6 +25423,9 @@ } } } + }, + "When bold-as-bright is off, bold text uses a heavier font weight instead of bright ANSI colors." : { + }, "When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { "localizations" : { @@ -25680,6 +25791,10 @@ "comment" : "A footer note explaining the app's funding.", "isCommentAutoGenerated" : true }, + "Youtube Music" : { + "comment" : "Add more media player types here.", + "isCommentAutoGenerated" : true + }, "YouTube Music requires this third-party app to be installed: " : { "localizations" : { "ar" : { diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index 975e88d..a9391d1 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -237,7 +237,7 @@ struct InlineHUD: View { LoopingVideoIcon(url: url, size: CGSize(width: 20, height: 20)) .frame(width: 20, height: 20, alignment: .center) } else { - Image(systemName: icon.isEmpty ? "bluetooth" : icon) + Image(systemName: icon.isEmpty ? "dot.radiowaves.left.and.right" : icon) .symbolRenderingMode(.hierarchical) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) diff --git a/DynamicIsland/components/Notch/NotchTerminalView.swift b/DynamicIsland/components/Notch/NotchTerminalView.swift index bc9068d..a3f4336 100644 --- a/DynamicIsland/components/Notch/NotchTerminalView.swift +++ b/DynamicIsland/components/Notch/NotchTerminalView.swift @@ -123,10 +123,9 @@ struct NotchTerminalView: View { Divider() .padding(.horizontal, 8) - // Terminal content — .id(sessionGeneration) forces - // representable recreation only after an explicit restart. + // Terminal content — the containerView is stable across + // notch close/open cycles; updateNSView handles restart. TerminalNSViewRepresentable(terminalManager: terminalManager) - .id(terminalManager.sessionGeneration) .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) .padding(.horizontal, 8) .padding(.bottom, 8) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index f7c0efa..551d297 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -794,6 +794,14 @@ struct SettingsView: View { SettingsSearchEntry(tab: .terminal, title: "Font size", keywords: ["terminal", "font", "text size"], highlightID: SettingsTab.terminal.highlightID(for: "Font size")), SettingsSearchEntry(tab: .terminal, title: "Terminal opacity", keywords: ["terminal", "opacity", "transparency"], highlightID: SettingsTab.terminal.highlightID(for: "Terminal opacity")), SettingsSearchEntry(tab: .terminal, title: "Maximum height", keywords: ["terminal", "height", "size"], highlightID: SettingsTab.terminal.highlightID(for: "Maximum height")), + SettingsSearchEntry(tab: .terminal, title: "Background color", keywords: ["terminal", "background", "color", "theme"], highlightID: SettingsTab.terminal.highlightID(for: "Background color")), + SettingsSearchEntry(tab: .terminal, title: "Foreground color", keywords: ["terminal", "foreground", "text color", "theme"], highlightID: SettingsTab.terminal.highlightID(for: "Foreground color")), + SettingsSearchEntry(tab: .terminal, title: "Cursor color", keywords: ["terminal", "cursor", "caret", "color"], highlightID: SettingsTab.terminal.highlightID(for: "Cursor color")), + SettingsSearchEntry(tab: .terminal, title: "Bold as bright", keywords: ["terminal", "bold", "bright", "colors"], highlightID: SettingsTab.terminal.highlightID(for: "Bold as bright")), + SettingsSearchEntry(tab: .terminal, title: "Cursor style", keywords: ["terminal", "cursor", "block", "underline", "bar", "blink"], highlightID: SettingsTab.terminal.highlightID(for: "Cursor style")), + SettingsSearchEntry(tab: .terminal, title: "Scrollback lines", keywords: ["terminal", "scrollback", "buffer", "history"], highlightID: SettingsTab.terminal.highlightID(for: "Scrollback lines")), + SettingsSearchEntry(tab: .terminal, title: "Option as Meta", keywords: ["terminal", "option", "meta", "alt", "key"], highlightID: SettingsTab.terminal.highlightID(for: "Option as Meta")), + SettingsSearchEntry(tab: .terminal, title: "Mouse reporting", keywords: ["terminal", "mouse", "reporting", "vim", "tmux"], highlightID: SettingsTab.terminal.highlightID(for: "Mouse reporting")), ] } @@ -7016,6 +7024,14 @@ struct TerminalSettings: View { @Default(.terminalFontSize) var terminalFontSize @Default(.terminalOpacity) var terminalOpacity @Default(.terminalMaxHeightFraction) var terminalMaxHeightFraction + @Default(.terminalCursorStyle) var terminalCursorStyle + @Default(.terminalScrollbackLines) var terminalScrollbackLines + @Default(.terminalOptionAsMeta) var terminalOptionAsMeta + @Default(.terminalMouseReporting) var terminalMouseReporting + @Default(.terminalBoldAsBright) var terminalBoldAsBright + @Default(.terminalBackgroundColor) var terminalBackgroundColor + @Default(.terminalForegroundColor) var terminalForegroundColor + @Default(.terminalCursorColor) var terminalCursorColor private func highlightID(_ title: String) -> String { SettingsTab.terminal.highlightID(for: title) @@ -7025,6 +7041,13 @@ struct TerminalSettings: View { "\(Int(terminalMaxHeightFraction * 100))% of screen" } + private var cursorStyleBinding: Binding { + Binding( + get: { TerminalCursorStyleOption(rawValue: terminalCursorStyle) ?? .blinkBlock }, + set: { terminalCursorStyle = $0.rawValue } + ) + } + var body: some View { Form { Section { @@ -7033,12 +7056,13 @@ struct TerminalSettings: View { } header: { Text("General") } footer: { - Text("Adds a Guake-style dropdown terminal tab. The terminal runs a shell session that persists while the notch is open.") + Text("Adds a Guake-style dropdown terminal tab. The terminal session persists across notch open/close cycles.") .font(.caption) .foregroundStyle(.secondary) } if enableTerminalFeature { + // MARK: Shell Section { HStack { Text("Shell path") @@ -7049,7 +7073,12 @@ struct TerminalSettings: View { .multilineTextAlignment(.trailing) } .settingsHighlight(id: highlightID("Shell path")) + } header: { + Text("Shell") + } + // MARK: Appearance + Section { VStack(alignment: .leading, spacing: 8) { HStack { Text("Font size") @@ -7095,6 +7124,109 @@ struct TerminalSettings: View { Text("Appearance") } + // MARK: Colors + Section { + ColorPicker("Background", selection: $terminalBackgroundColor, supportsOpacity: false) + .onChange(of: terminalBackgroundColor) { _, newValue in + terminalManager.applyBackgroundColor(newValue) + } + .settingsHighlight(id: highlightID("Background color")) + + ColorPicker("Foreground", selection: $terminalForegroundColor, supportsOpacity: false) + .onChange(of: terminalForegroundColor) { _, newValue in + terminalManager.applyForegroundColor(newValue) + } + .settingsHighlight(id: highlightID("Foreground color")) + + ColorPicker("Cursor", selection: $terminalCursorColor, supportsOpacity: false) + .onChange(of: terminalCursorColor) { _, newValue in + terminalManager.applyCursorColor(newValue) + } + .settingsHighlight(id: highlightID("Cursor color")) + + Toggle("Bold text as bright colors", isOn: $terminalBoldAsBright) + .onChange(of: terminalBoldAsBright) { _, newValue in + terminalManager.applyBoldAsBright(newValue) + } + .settingsHighlight(id: highlightID("Bold as bright")) + } header: { + Text("Colors") + } footer: { + Text("When bold-as-bright is off, bold text uses a heavier font weight instead of bright ANSI colors.") + .font(.caption) + .foregroundStyle(.secondary) + } + + // MARK: Cursor + Section { + Picker("Cursor style", selection: cursorStyleBinding) { + ForEach(TerminalCursorStyleOption.allCases, id: \.self) { style in + Text(style.displayName).tag(style) + } + } + .onChange(of: terminalCursorStyle) { _, newValue in + if let style = TerminalCursorStyleOption(rawValue: newValue) { + terminalManager.applyCursorStyle(style) + } + } + .settingsHighlight(id: highlightID("Cursor style")) + } header: { + Text("Cursor") + } + + // MARK: Scrollback + Section { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Scrollback lines") + Spacer() + Text("\(terminalScrollbackLines)") + .foregroundStyle(.secondary) + .monospacedDigit() + } + Slider( + value: Binding( + get: { Double(terminalScrollbackLines) }, + set: { terminalScrollbackLines = Int($0) } + ), + in: 100...10000, + step: 100 + ) + .onChange(of: terminalScrollbackLines) { _, newValue in + terminalManager.applyScrollback(newValue) + } + } + .settingsHighlight(id: highlightID("Scrollback lines")) + } header: { + Text("Scrollback") + } footer: { + Text("Number of lines kept in the scrollback buffer. Higher values use more memory.") + .font(.caption) + .foregroundStyle(.secondary) + } + + // MARK: Input + Section { + Toggle("Option as Meta key", isOn: $terminalOptionAsMeta) + .onChange(of: terminalOptionAsMeta) { _, newValue in + terminalManager.applyOptionAsMeta(newValue) + } + .settingsHighlight(id: highlightID("Option as Meta")) + + Toggle("Allow mouse reporting", isOn: $terminalMouseReporting) + .onChange(of: terminalMouseReporting) { _, newValue in + terminalManager.applyMouseReporting(newValue) + } + .settingsHighlight(id: highlightID("Mouse reporting")) + } header: { + Text("Input") + } footer: { + Text("Option as Meta sends Esc+key instead of macOS special characters. Mouse reporting forwards mouse events to terminal applications like vim or tmux.") + .font(.caption) + .foregroundStyle(.secondary) + } + + // MARK: Actions Section { Button("Restart Shell") { terminalManager.restartShell() diff --git a/DynamicIsland/managers/TerminalManager.swift b/DynamicIsland/managers/TerminalManager.swift index b723fd9..212c81b 100644 --- a/DynamicIsland/managers/TerminalManager.swift +++ b/DynamicIsland/managers/TerminalManager.swift @@ -21,11 +21,53 @@ import SwiftUI import SwiftTerm import Defaults +// MARK: - Stable Container + +/// Container NSView that shields its terminal child from zero-sized frames. +/// +/// When the notch closes, SwiftUI removes the `NSViewRepresentable` from the +/// hierarchy — but later re-adds the same `containerView` on reopen. During +/// that insertion, SwiftUI momentarily sets the frame to `.zero`. Without +/// protection, AppKit's autoresizing mask propagates the zero frame to the +/// terminal child, causing SwiftTerm's `processSizeChange` to resize the +/// emulator to 2 cols × 1 row (the enforced minimum), which destroys the +/// scrollback buffer. +/// +/// We bypass `super.resizeSubviews(withOldSize:)` entirely and manually set +/// children to fill the container's bounds. This avoids the autoresizing +/// calculation which produces corrupt geometry when `oldSize` was `.zero` +/// (recorded during the transient removal) but the child kept its previous +/// large frame. +final class StableTerminalContainerView: NSView { + override func resizeSubviews(withOldSize oldSize: NSSize) { + let size = bounds.size + // Block degenerate sizes — the terminal would collapse to 2×1 + guard size.width >= 10, size.height >= 10 else { return } + // Manually fill children to bounds — bypasses autoresizing math + // that breaks when oldSize was zero but child kept its old frame. + for child in subviews { + child.frame = bounds + } + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + guard window != nil else { return } + // When re-inserted into a window (notch reopened), force a full + // redraw so SwiftTerm re-renders the terminal buffer contents. + for child in subviews { + child.needsDisplay = true + } + } +} + +// MARK: - Terminal Manager + /// Manages the Guake-style dropdown terminal session lifecycle. /// The terminal is lazily created when the user first switches to the terminal tab, /// and the process is kept alive across notch open/close cycles. /// -/// Uses a stable `containerView` (NSView) as the host so that SwiftUI's +/// Uses a stable `StableTerminalContainerView` as the host so that SwiftUI's /// `NSViewRepresentable` lifecycle (make/update/dismantle) never tears down /// the actual terminal. The `LocalProcessTerminalView` is added as a subview /// of the container and survives notch close/open cycles. @@ -39,12 +81,11 @@ class TerminalManager: ObservableObject { /// The current terminal title reported by the shell. @Published var terminalTitle: String = "Terminal" - /// Bumped on restart so the `NSViewRepresentable` can recreate via `.id()`. - @Published var sessionGeneration: Int = 0 - /// Stable container returned to SwiftUI — never deallocated. - let containerView: NSView = { - let v = NSView(frame: .zero) + /// Uses `StableTerminalContainerView` to prevent zero-frame transients + /// from destroying the scrollback buffer. + let containerView: StableTerminalContainerView = { + let v = StableTerminalContainerView(frame: .zero) v.autoresizingMask = [.width, .height] v.wantsLayer = true return v @@ -63,24 +104,25 @@ class TerminalManager: ObservableObject { if let existing = terminalView, existing.superview === containerView { // Already mounted — just re-wire the delegate in case the coordinator changed. existing.processDelegate = delegate + // Force a redraw — the view may have been off-screen (notch closed) + // and needs to re-render the terminal buffer. + existing.needsDisplay = true return } - let view = LocalProcessTerminalView(frame: containerView.bounds) + // Use the container's current bounds if valid, otherwise a reasonable + // default. SwiftTerm's init calls setupOptions() which reads the + // frame — a zero frame creates a 2×1 terminal that is corrected + // once the container gets its proper layout size. + let initialFrame = containerView.bounds.size.width >= 10 + ? containerView.bounds + : CGRect(x: 0, y: 0, width: 400, height: 300) + + let view = LocalProcessTerminalView(frame: initialFrame) view.autoresizingMask = [.width, .height] - // Apply font size from settings - let fontSize = CGFloat(Defaults[.terminalFontSize]) - if let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) as NSFont? { - view.font = font - } - - // Apply opacity - view.layer?.opacity = Float(Defaults[.terminalOpacity]) - - // Configure the dark terminal appearance - view.nativeBackgroundColor = .black - view.nativeForegroundColor = .white + // Apply all settings from Defaults + applyAllSettings(to: view) view.processDelegate = delegate @@ -88,6 +130,12 @@ class TerminalManager: ObservableObject { containerView.subviews.forEach { $0.removeFromSuperview() } containerView.addSubview(view) terminalView = view + + // If the container already has a valid size, snap the child to it. + let containerSize = containerView.bounds.size + if containerSize.width >= 10, containerSize.height >= 10 { + view.frame = containerView.bounds + } } /// Starts the shell process if not already running. @@ -112,6 +160,12 @@ class TerminalManager: ObservableObject { } /// Restarts the shell by tearing down the old terminal and creating a fresh one. + /// + /// Instead of bumping a generation counter (which forced SwiftUI to destroy + /// and recreate the `NSViewRepresentable` — recycling the same `containerView` + /// across identities which broke layout), we simply nil out `terminalView` + /// and change `@Published` state. SwiftUI's `updateNSView` will fire, + /// see the nil terminal, and call `ensureTerminalView` to mount a new one. func restartShell() { // Terminate the running process gracefully terminalView?.terminate() @@ -120,8 +174,6 @@ class TerminalManager: ObservableObject { terminalView = nil isProcessRunning = false terminalTitle = "Terminal" - // Bump generation so the SwiftUI representable re-mounts - sessionGeneration += 1 } /// Updates the terminal title from the shell escape sequence. @@ -129,6 +181,42 @@ class TerminalManager: ObservableObject { terminalTitle = title } + // MARK: - Settings Application + + /// Applies all persisted settings to a terminal view at creation time. + private func applyAllSettings(to view: LocalProcessTerminalView) { + // Font + let fontSize = CGFloat(Defaults[.terminalFontSize]) + if let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) as NSFont? { + view.font = font + } + + // Opacity + view.layer?.opacity = Float(Defaults[.terminalOpacity]) + + // Colors + view.nativeBackgroundColor = NSColor(Defaults[.terminalBackgroundColor]) + view.nativeForegroundColor = NSColor(Defaults[.terminalForegroundColor]) + view.caretColor = NSColor(Defaults[.terminalCursorColor]) + + // Cursor style + let cursorStyle = TerminalCursorStyleOption(rawValue: Defaults[.terminalCursorStyle]) + ?? .blinkBlock + view.getTerminal().options.cursorStyle = cursorStyle.swiftTermStyle + + // Scrollback + let scrollback = Defaults[.terminalScrollbackLines] + view.getTerminal().buffer.changeHistorySize(scrollback) + view.getTerminal().options.scrollback = scrollback + + // Input behavior + view.optionAsMetaKey = Defaults[.terminalOptionAsMeta] + view.allowMouseReporting = Defaults[.terminalMouseReporting] + + // Rendering + view.useBrightColors = Defaults[.terminalBoldAsBright] + } + /// Updates font size on the live terminal view. func applyFontSize(_ size: Double) { guard let view = terminalView else { return } @@ -143,6 +231,56 @@ class TerminalManager: ObservableObject { view.layer?.opacity = Float(opacity) } + /// Updates cursor style on the live terminal view. + func applyCursorStyle(_ style: TerminalCursorStyleOption) { + guard let view = terminalView else { return } + view.getTerminal().options.cursorStyle = style.swiftTermStyle + view.setNeedsDisplay(view.bounds) + } + + /// Updates scrollback buffer size on the live terminal view. + func applyScrollback(_ lines: Int) { + guard let view = terminalView else { return } + view.getTerminal().buffer.changeHistorySize(lines) + view.getTerminal().options.scrollback = lines + } + + /// Updates option-as-meta on the live terminal view. + func applyOptionAsMeta(_ enabled: Bool) { + guard let view = terminalView else { return } + view.optionAsMetaKey = enabled + } + + /// Updates mouse reporting on the live terminal view. + func applyMouseReporting(_ enabled: Bool) { + guard let view = terminalView else { return } + view.allowMouseReporting = enabled + } + + /// Updates bold-as-bright on the live terminal view. + func applyBoldAsBright(_ enabled: Bool) { + guard let view = terminalView else { return } + view.useBrightColors = enabled + } + + /// Updates background color on the live terminal view. + func applyBackgroundColor(_ color: SwiftUI.Color) { + guard let view = terminalView else { return } + view.nativeBackgroundColor = NSColor(color) + } + + /// Updates foreground color on the live terminal view. + func applyForegroundColor(_ color: SwiftUI.Color) { + guard let view = terminalView else { return } + view.nativeForegroundColor = NSColor(color) + } + + /// Updates cursor color on the live terminal view. + func applyCursorColor(_ color: SwiftUI.Color) { + guard let view = terminalView else { return } + view.caretColor = NSColor(color) + } + // MARK: - Environment /// Builds the environment for the child shell process. @@ -155,3 +293,37 @@ class TerminalManager: ObservableObject { return env.map { "\($0.key)=\($0.value)" } } } + +// MARK: - Cursor Style Bridge + +/// Codable-friendly cursor style enum that bridges to SwiftTerm's `CursorStyle`. +enum TerminalCursorStyleOption: String, CaseIterable, Defaults.Serializable { + case blinkBlock = "blinkBlock" + case steadyBlock = "steadyBlock" + case blinkUnderline = "blinkUnderline" + case steadyUnderline = "steadyUnderline" + case blinkBar = "blinkBar" + case steadyBar = "steadyBar" + + var swiftTermStyle: CursorStyle { + switch self { + case .blinkBlock: return .blinkBlock + case .steadyBlock: return .steadyBlock + case .blinkUnderline: return .blinkUnderline + case .steadyUnderline: return .steadyUnderline + case .blinkBar: return .blinkBar + case .steadyBar: return .steadyBar + } + } + + var displayName: String { + switch self { + case .blinkBlock: return "Block (blinking)" + case .steadyBlock: return "Block (steady)" + case .blinkUnderline: return "Underline (blinking)" + case .steadyUnderline: return "Underline (steady)" + case .blinkBar: return "Bar (blinking)" + case .steadyBar: return "Bar (steady)" + } + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 86d60b1..c718ea5 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -917,6 +917,14 @@ extension Defaults.Keys { static let terminalFontSize = Key("terminalFontSize", default: 12.0) static let terminalOpacity = Key("terminalOpacity", default: 1.0) static let terminalMaxHeightFraction = Key("terminalMaxHeightFraction", default: 0.4) + static let terminalCursorStyle = Key("terminalCursorStyle", default: "blinkBlock") + static let terminalScrollbackLines = Key("terminalScrollbackLines", default: 1000) + static let terminalOptionAsMeta = Key("terminalOptionAsMeta", default: true) + static let terminalMouseReporting = Key("terminalMouseReporting", default: true) + static let terminalBoldAsBright = Key("terminalBoldAsBright", default: true) + static let terminalBackgroundColor = Key("terminalBackgroundColor", default: .black) + static let terminalForegroundColor = Key("terminalForegroundColor", default: .white) + static let terminalCursorColor = Key("terminalCursorColor", default: Color(.selectedControlColor)) // MARK: Timer Feature static let enableTimerFeature = Key("enableTimerFeature", default: true)