Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions Sources/Brygga/Views/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Brygga contributors

import AppKit
import BryggaCore
import SwiftUI

Expand Down Expand Up @@ -774,6 +775,7 @@ struct ServerMessageList: View {
}
}
.padding(12)
.textSelection(.enabled)
}
.onChange(of: server.messages.count) {
if let last = server.messages.last {
Expand Down Expand Up @@ -893,6 +895,7 @@ struct MessageList: View {
}
}
.padding(12)
.textSelection(.enabled)
}
.onChange(of: channel.messages.count) {
if let last = channel.messages.last {
Expand Down Expand Up @@ -1032,7 +1035,6 @@ struct MessageRow: View {
.foregroundStyle(senderColor(message.sender))
Text(AttributedString.fromIRC(message.content))
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
case .notice:
Text("-\(message.sender)-")
Expand All @@ -1041,7 +1043,6 @@ struct MessageRow: View {
Text(AttributedString.fromIRC(message.content))
.font(.system(.body, design: .monospaced))
.foregroundStyle(.orange)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
case .action:
Text("*")
Expand All @@ -1059,6 +1060,34 @@ struct MessageRow: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.contextMenu {
Button("Copy Message") { copy(plainLogLine) }
Button("Copy Text") { copy(IRCFormatting.stripControlCodes(message.content)) }
Button("Copy Nickname") { copy(message.sender) }
}
}

/// Single-line plain-text rendering of the message suitable for the
/// pasteboard. Matches the on-screen layout (timestamp, sender decoration,
/// content) with all mIRC control codes stripped.
private var plainLogLine: String {
let body = IRCFormatting.stripControlCodes(message.content)
switch message.kind {
case .privmsg:
return "[\(timestampText)] <\(message.sender)> \(body)"
case .notice:
return "[\(timestampText)] -\(message.sender)- \(body)"
case .action:
return "[\(timestampText)] * \(message.sender) \(body)"
default:
return "[\(timestampText)] * \(message.sender) \(body)"
}
}

private func copy(_ text: String) {
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(text, forType: .string)
}
}

Expand Down
8 changes: 8 additions & 0 deletions Sources/BryggaCore/IRC/IRCFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ public enum IRCFormatting {
return runs
}

/// Returns `text` with all mIRC/IRCv3 formatting control bytes removed —
/// bold, italic, underline, strikethrough, reverse, reset, monospace, and
/// any `^K<fg>[,<bg>]` color sequences. Suitable for placing on the system
/// pasteboard when the user copies a message.
public static func stripControlCodes(_ text: String) -> String {
parse(text).map(\.text).joined()
}

// MARK: - mIRC 16-color palette (RGB in 0…1)

public struct RGB: Sendable, Equatable {
Expand Down
34 changes: 34 additions & 0 deletions Tests/IRCFormattingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Brygga contributors

@testable import BryggaCore
import XCTest

final class IRCFormattingTests: XCTestCase {
func testStripControlCodesRemovesBoldItalicUnderline() {
let input = "\u{02}bold\u{02} and \u{1D}italic\u{1D} and \u{1F}under\u{1F}"
XCTAssertEqual(IRCFormatting.stripControlCodes(input), "bold and italic and under")
}

func testStripControlCodesRemovesColorSequences() {
let input = "\u{03}04red\u{03} back \u{03}04,02fg+bg\u{03} end"
XCTAssertEqual(IRCFormatting.stripControlCodes(input), "red back fg+bg end")
}

func testStripControlCodesKeepsCommaWhenNotPartOfColor() {
// A bare ^K followed by non-digits resets colors; the literal comma
// survives because it's not a bg separator.
let input = "\u{03}3,meh"
XCTAssertEqual(IRCFormatting.stripControlCodes(input), ",meh")
}

func testStripControlCodesRemovesResetAndReverse() {
let input = "a\u{0F}b\u{16}c\u{1E}d"
XCTAssertEqual(IRCFormatting.stripControlCodes(input), "abcd")
}

func testStripControlCodesIsIdentityForPlainText() {
let input = "hello, world — no control codes here"
XCTAssertEqual(IRCFormatting.stripControlCodes(input), input)
}
}
Loading