Skip to content
16 changes: 16 additions & 0 deletions RemuxApp/Sources/App/RemuxAppChrome.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ enum RemuxAppPalette {

typealias LibraryHomePalette = RemuxAppPalette

/// The app's accent, the same in every terminal theme: a theme sets only the terminal's colours and whether
/// the chrome around it is light or dark.
enum RemuxAccent {
static let fill = Color.accentColor
/// Text and glyphs drawn on `fill`.
static let onFill = Color.white

static let toolbarButtonActiveFill = fill.opacity(0.16)
static let selectedFill = fill.opacity(0.18)
static let selectedStroke = fill.opacity(0.76)

/// `fill` for UIKit: the app's named accent colour, or the system blue `Color.accentColor` is without one.
static let uiFill = UIColor(named: "AccentColor") ?? .systemBlue
static let uiOnFill = UIColor.white
}

extension View {
func remuxAppListRowSurface() -> some View {
listRowBackground(RemuxAppPalette.rowSurface)
Expand Down
8 changes: 1 addition & 7 deletions RemuxApp/Sources/App/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,7 @@ private struct RemuxWorkspaceShell: View {
}
)
.remuxSheetPresentationBackground()
.ghosttyTerminalChromePresentation(
model.terminalSettings.theme.terminalChromeColorScheme,
chromeStyle: model.terminalSettings.theme.terminalChromeStyle
)
.ghosttyTerminalChromePresentation(model.terminalSettings.theme.terminalChromeColorScheme)
.tailscaleSSHCheckAlert(
request: $tailscaleSSHCheckRequest,
isActive: true
Expand Down Expand Up @@ -382,9 +379,6 @@ private struct RemuxWorkspaceShell: View {
}
}
.remuxSheetPresentationBackground()
.ghosttyTerminalChromePresentation(
chromeStyle: model.terminalSettings.theme.terminalChromeStyle
)
.alert(
setupSubmissionIssueTitle(setup.submissionIssue),
isPresented: setupSubmissionIssueIsPresented(setup.submissionIssue),
Expand Down
6 changes: 2 additions & 4 deletions RemuxApp/Sources/App/SessionAgentsList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,6 @@ struct SessionAgentsProjection: Equatable {
/// an agent that appears, or comes back to the filter, is added after the rest and keeps its place from then
/// on. Herdr's client sorts again at once.
struct SessionAgentsList: View {
@Environment(\.ghosttyTerminalChromeStyle) private var chromeStyle

let projection: SessionAgentsProjection
@Binding var filter: SessionAgentsFilter
let onOpen: (SessionAgentsProjection.Session, HerdrAgentPresentation.Agent) -> Void
Expand Down Expand Up @@ -208,11 +206,11 @@ struct SessionAgentsList: View {
Text("\(option.title) \(count)")
.font(.system(size: 13, weight: .semibold))
.monospacedDigit()
.foregroundStyle(isSelected ? chromeStyle.accentForeground : TerminalSelectionSheetPalette.primary)
.foregroundStyle(isSelected ? RemuxAccent.onFill : TerminalSelectionSheetPalette.primary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
isSelected ? chromeStyle.accent : TerminalSelectionSheetPalette.controlFill,
isSelected ? RemuxAccent.fill : TerminalSelectionSheetPalette.controlFill,
in: Capsule()
)
.contentShape(Capsule())
Expand Down
15 changes: 5 additions & 10 deletions RemuxApp/Sources/App/SessionSwitcherView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ struct SessionSwitcherView<NewSessionContent: View>: View {
}

@Environment(\.dismiss) private var dismiss
@Environment(\.ghosttyTerminalChromeStyle) private var chromeStyle

let projection: SessionSwitcherProjection
let servers: [SavedServer]
Expand Down Expand Up @@ -471,8 +470,7 @@ struct SessionSwitcherView<NewSessionContent: View>: View {
ActiveSessionSwitcherRow(
session: session,
disclosure: disclosure,
agentStatus: herdrAgents(session.id)?.status,
chromeStyle: chromeStyle
agentStatus: herdrAgents(session.id)?.status
)
}
.buttonStyle(.plain)
Expand Down Expand Up @@ -509,8 +507,7 @@ struct SessionSwitcherView<NewSessionContent: View>: View {
workspace: workspace,
isCurrent: isCurrent,
agentStatus: agentStatus,
isStale: isStale,
chromeStyle: chromeStyle
isStale: isStale
)
}
.buttonStyle(.plain)
Expand Down Expand Up @@ -573,7 +570,7 @@ struct SessionSwitcherView<NewSessionContent: View>: View {
} label: {
Label("New Workspace", systemImage: "plus")
.font(.body)
.foregroundStyle(chromeStyle.accent)
.foregroundStyle(RemuxAccent.fill)
.padding(.leading, HerdrWorkspaceSwitcherRow.leadingInset)
.frame(maxWidth: .infinity, minHeight: 44, alignment: .leading)
.contentShape(Rectangle())
Expand Down Expand Up @@ -771,7 +768,6 @@ private struct ActiveSessionSwitcherRow: View {
let disclosure: Bool?
/// The session's most urgent agent, for a session that reports agents.
let agentStatus: TerminalAgentStatus?
let chromeStyle: GhosttyTerminalChromeStyle

var body: some View {
HStack(spacing: 12) {
Expand Down Expand Up @@ -813,7 +809,7 @@ private struct ActiveSessionSwitcherRow: View {
if session.showsCheckmark(disclosure: disclosure) {
Image(systemName: "checkmark")
.font(.body.weight(.semibold))
.foregroundStyle(chromeStyle.accent)
.foregroundStyle(RemuxAccent.fill)
.accessibilityHidden(true)
}

Expand Down Expand Up @@ -855,7 +851,6 @@ private struct HerdrWorkspaceSwitcherRow: View {
let agentStatus: TerminalAgentStatus?
/// The session isn't connected, so the agents' state may be out of date.
let isStale: Bool
let chromeStyle: GhosttyTerminalChromeStyle

private var tabCount: String {
workspace.tabs.count == 1 ? "1 tab" : "\(workspace.tabs.count) tabs"
Expand Down Expand Up @@ -891,7 +886,7 @@ private struct HerdrWorkspaceSwitcherRow: View {
if isCurrent {
Image(systemName: "checkmark")
.font(.body.weight(.semibold))
.foregroundStyle(chromeStyle.accent)
.foregroundStyle(RemuxAccent.fill)
.accessibilityHidden(true)
}
}
Expand Down
14 changes: 6 additions & 8 deletions RemuxApp/Sources/Ghostty/GhosttyAttachmentPreviewSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import UIKit

struct GhosttyAttachmentPreviewSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.ghosttyTerminalChromeStyle) private var chromeStyle
@State private var selectedAttachmentID: UUID?
@State private var markupRequest: GhosttyAttachmentMarkupRequest?
@State private var preparingMarkupAttachmentID: UUID?
Expand Down Expand Up @@ -120,8 +119,7 @@ struct GhosttyAttachmentPreviewSheet: View {
}
.buttonStyle(
GhosttyAttachmentPreviewChipButtonStyle(
isSelected: isSelected(attachment),
chromeStyle: chromeStyle
isSelected: isSelected(attachment)
)
)
.id(attachment.id)
Expand Down Expand Up @@ -233,7 +231,7 @@ struct GhosttyAttachmentPreviewSheet: View {
HStack(spacing: 8) {
Image(systemName: "globe")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(chromeStyle.accent)
.foregroundStyle(RemuxAccent.fill)
.accessibilityHidden(true)

Text(linkTitle(for: url))
Expand All @@ -259,11 +257,11 @@ struct GhosttyAttachmentPreviewSheet: View {
.padding(.horizontal, 18)
.frame(height: 40)
.frame(maxWidth: .infinity)
.foregroundStyle(chromeStyle.accent)
.background(chromeStyle.selectedFill, in: Capsule())
.foregroundStyle(RemuxAccent.fill)
.background(RemuxAccent.selectedFill, in: Capsule())
.overlay {
Capsule()
.strokeBorder(chromeStyle.accent.opacity(0.18), lineWidth: 0.75)
.strokeBorder(RemuxAccent.fill.opacity(0.18), lineWidth: 0.75)
}
}
}
Expand All @@ -289,7 +287,7 @@ struct GhosttyAttachmentPreviewSheet: View {
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.scrollContentBackground(.hidden)
.tint(chromeStyle.accent)
.tint(RemuxAccent.fill)
.padding(.horizontal, 4)
.padding(.vertical, 2)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand Down
19 changes: 7 additions & 12 deletions RemuxApp/Sources/Ghostty/GhosttyAttachmentPreviewStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ struct GhosttyAttachmentPreviewCloseButtonStyle: ButtonStyle {

struct GhosttyAttachmentPreviewChipButtonStyle: ButtonStyle {
let isSelected: Bool
let chromeStyle: GhosttyTerminalChromeStyle

func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(isSelected ? chromeStyle.accent : GhosttySheetPalette.primary)
.foregroundStyle(isSelected ? RemuxAccent.fill : GhosttySheetPalette.primary)
.ghosttyAttachmentPreviewChipSurface(
isSelected: isSelected,
isPressed: configuration.isPressed,
chromeStyle: chromeStyle
isPressed: configuration.isPressed
)
.scaleEffect(configuration.isPressed ? 0.98 : 1)
.animation(.easeOut(duration: 0.12), value: configuration.isPressed)
Expand Down Expand Up @@ -80,16 +78,14 @@ extension View {

func ghosttyAttachmentPreviewChipSurface(
isSelected: Bool,
isPressed: Bool,
chromeStyle: GhosttyTerminalChromeStyle
isPressed: Bool
) -> some View {
let shape = Capsule()
let fill = attachmentPreviewChipFill(
isSelected: isSelected,
isPressed: isPressed,
chromeStyle: chromeStyle
isPressed: isPressed
)
let stroke = isSelected ? GhosttySheetPalette.selectedStroke(chromeStyle) : GhosttyAttachmentPreviewStyle.controlStroke
let stroke = isSelected ? GhosttySheetPalette.selectedStroke : GhosttyAttachmentPreviewStyle.controlStroke

return self
.background(fill, in: shape)
Expand All @@ -101,11 +97,10 @@ extension View {

private func attachmentPreviewChipFill(
isSelected: Bool,
isPressed: Bool,
chromeStyle: GhosttyTerminalChromeStyle
isPressed: Bool
) -> Color {
if isSelected {
return chromeStyle.selectedFill
return RemuxAccent.selectedFill
}

return isPressed
Expand Down
3 changes: 1 addition & 2 deletions RemuxApp/Sources/Ghostty/GhosttyComposeBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ private struct GhosttyComposerInputLayout: Layout {
}

struct GhosttyComposeBar: View {
@Environment(\.ghosttyTerminalChromeStyle) private var chromeStyle
@AppStorage("terminal.composer.keyboardDismissHintShown")
private var hasShownKeyboardDismissHint = false
@State private var isShowingKeyboardDismissHint = false
Expand Down Expand Up @@ -442,7 +441,7 @@ struct GhosttyComposeBar: View {
ZStack(alignment: .topLeading) {
GhosttyComposerTextView(
text: $composer.draft,
caretColor: UIColor(chromeStyle.accent),
caretColor: RemuxAccent.uiFill,
allowsUserEdits: submissionState.allowsComposerInput
&& !dictationPhase.isActive,
wantsFirstResponder: wantsKeyboardFocus,
Expand Down
73 changes: 4 additions & 69 deletions RemuxApp/Sources/Ghostty/GhosttyIOSurfaceFrame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,49 +121,9 @@ struct GhosttyIOSurfaceFrame: Sendable {
)
}

static func sourceRect(
width: Int,
height: Int,
centeredOn anchor: CGRect,
maxWidth: UInt32,
maxHeight: UInt32
) -> CGRect? {
guard width > 0, height > 0,
maxWidth > 0, maxHeight > 0,
anchor.origin.x.isFinite, anchor.origin.y.isFinite,
anchor.width.isFinite, anchor.height.isFinite,
anchor.width > 0, anchor.height > 0
else { return nil }

let bounds = CGRect(x: 0, y: 0, width: width, height: height)
guard anchor.intersects(bounds) else { return nil }

let aspectRatio = Double(maxWidth) / Double(maxHeight)
var cropWidth = min(Double(width), Double(maxWidth))
var cropHeight = min(Double(height), Double(maxHeight))
if cropWidth / cropHeight > aspectRatio {
cropWidth = cropHeight * aspectRatio
} else {
cropHeight = cropWidth / aspectRatio
}

let integralWidth = max(1, min(width, Int(cropWidth.rounded(.down))))
let integralHeight = max(1, min(height, Int(cropHeight.rounded(.down))))
let x = min(
max(0, Int((anchor.midX - CGFloat(integralWidth) / 2).rounded(.down))),
width - integralWidth
)
let y = min(
max(0, Int((anchor.midY - CGFloat(integralHeight) / 2).rounded(.down))),
height - integralHeight
)
return CGRect(x: x, y: y, width: integralWidth, height: integralHeight)
}

func image(maxWidth: UInt32, maxHeight: UInt32) throws -> CGImage {
guard maxWidth > 0, maxHeight > 0,
let provider = CGDataProvider(data: bytes as CFData),
let source = CGImage(
func image() throws -> CGImage {
guard let provider = CGDataProvider(data: bytes as CFData),
let image = CGImage(
width: width,
height: height,
bitsPerComponent: 8,
Expand All @@ -173,35 +133,10 @@ struct GhosttyIOSurfaceFrame: Sendable {
bitmapInfo: Self.bitmapInfo,
provider: provider,
decode: nil,
shouldInterpolate: false,
shouldInterpolate: true,
intent: .defaultIntent
)
else { throw ReadError.imageCreationFailed }

let scale = min(
1,
min(
Double(maxWidth) / Double(source.width),
Double(maxHeight) / Double(source.height)
)
)
guard scale < 1 else { return source }

let targetWidth = max(1, Int((Double(source.width) * scale).rounded(.down)))
let targetHeight = max(1, Int((Double(source.height) * scale).rounded(.down)))
let targetBytesPerRow = targetWidth * 4
guard let context = CGContext(
data: nil,
width: targetWidth,
height: targetHeight,
bitsPerComponent: 8,
bytesPerRow: targetBytesPerRow,
space: Self.colorSpace,
bitmapInfo: Self.bitmapInfo.rawValue
) else { throw ReadError.imageCreationFailed }
context.interpolationQuality = .medium
context.draw(source, in: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight))
guard let image = context.makeImage() else { throw ReadError.imageCreationFailed }
return image
}

Expand Down
Loading
Loading