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
2 changes: 2 additions & 0 deletions Hammerspoon 2.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HammerspoonOSAScriptHelper/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = HammerspoonOSAScriptHelper;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
Expand Down Expand Up @@ -861,6 +862,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HammerspoonOSAScriptHelper/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = HammerspoonOSAScriptHelper;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
Expand Down
57 changes: 44 additions & 13 deletions Hammerspoon 2/Windows/ConsoleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ struct ConsoleView: View {
@State var showSaveError: Bool = false

@Environment(\.dismissWindow) var dismissWindow
@Environment(\.locale) var locale
@Environment(\.timeZone) var timeZone
@Environment(\.calendar) var calendar

@AppStorage("minimumLogLevel") var minimumLogLevel: HammerspoonLogType = .Debug

Expand All @@ -40,11 +43,35 @@ struct ConsoleView: View {
Array(logs.entries(minimumLevel: minimumLogLevel, searchString: searchString).suffix(maxRenderedEntries))
}

/// Renders all displayed entries as a single AttributedString (rather than one `Text` per
/// entry) so SwiftUI's `.textSelection(.enabled)` treats the whole log as one text container —
/// a `LazyVStack` of separate `Text` views can't support selection spanning multiple rows.
///
/// Cached in state and rebuilt only when `displayedEntries`, `locale`, `timeZone`, or
/// `calendar` changes (see `.onChange` below), rather than computed inline in `body` —
/// `body` re-runs on every keystroke in the eval field, and rebuilding up to
/// `maxRenderedEntries` lines of AttributedString each time would be wasteful.
@State var displayedLogText = AttributedString()

private func rebuildDisplayedLogText() {
var result = AttributedString()
let entries = displayedEntries
for (index, entry) in entries.enumerated() {
var line = AttributedString(formatEntry(entry))
line.foregroundColor = colorForLogType(entry.logType)
result += line
if index < entries.count - 1 {
result += AttributedString("\n")
}
}
displayedLogText = result
}

private func formatEntry(_ entry: HammerspoonLogEntry) -> String {
let date = entry.date.formatted(
.verbatim(
"\(year: .defaultDigits)-\(month: .twoDigits)-\(day: .twoDigits) \(hour: .twoDigits(clock: .twentyFourHour, hourCycle: .zeroBased)):\(minute: .twoDigits):\(second: .twoDigits)",
locale: .autoupdatingCurrent, timeZone: .autoupdatingCurrent, calendar: .autoupdatingCurrent
locale: locale, timeZone: timeZone, calendar: calendar
)
)
return "\(date) - \(entry.logType.asString): \(entry.msg)"
Expand Down Expand Up @@ -170,26 +197,30 @@ struct ConsoleView: View {
VStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(displayedEntries) { entry in
Text(formatEntry(entry))
.multilineTextAlignment(.leading)
.foregroundColor(colorForLogType(entry.logType))
.id(entry.id)
}
}
.textSelection(.enabled)
.fontDesign(.monospaced)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Text(displayedLogText)
.multilineTextAlignment(.leading)
.textSelection(.enabled)
.fontDesign(.monospaced)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()

Color.clear
.frame(height: 0)
.id("logBottom")
}
.onChange(of: displayedEntries) {
rebuildDisplayedLogText()
Comment thread
cmsj marked this conversation as resolved.
proxy.scrollTo("logBottom", anchor: .bottom)
}
// Timestamps are formatted with these, so a change while the console is open
// (e.g. the user changes time zone) must invalidate the cached text too —
// it won't happen to get rebuilt otherwise, since displayedEntries doesn't change.
.onChange(of: locale) { rebuildDisplayedLogText() }
.onChange(of: timeZone) { rebuildDisplayedLogText() }
.onChange(of: calendar) { rebuildDisplayedLogText() }
.task {
rebuildDisplayedLogText()
}
}

TextField(">", text: $evalString, selection: $textSelection, prompt: Text("Javascript: >"), axis: .vertical)
Expand Down
Loading