mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 16:24:32 +00:00
- Moved prole-tools-app to prole-app at the project root to make it self-contained for transition to its own repository. - Created prole-tools-app/dist/ directory to host build artifacts. - Generated distribution artifacts (Prole Tools.app and Prole Tools.zip) using prole-app/build.sh package. - Checked in the generated artifacts to prole-tools-app/dist/ (bypassing .gitignore for temporary release process). Changes Summary • Renamed directory prole-tools-app/ to prole-app/. • Populated prole-tools-app/dist/ with the latest build output from prole-app/build.sh. • Staged all changes, including the forced addition of ignored artifacts in prole-tools-app/dist/.
248 lines
10 KiB
Swift
248 lines
10 KiB
Swift
import AppKit
|
|
|
|
// DBStatusWindowController displays the output of "kubecolor cnpg status prole-db".
|
|
// Uses identical style and behavior as MainWindowController.
|
|
final class DBStatusWindowController: NSWindowController {
|
|
struct Actions {
|
|
let onMinimizeToStatusBar: () -> Void
|
|
}
|
|
|
|
private let outputTextView: NSTextView = {
|
|
let tv = NSTextView()
|
|
tv.isEditable = false
|
|
tv.isSelectable = true
|
|
tv.isRichText = false
|
|
tv.usesFindBar = true
|
|
tv.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
|
|
tv.textColor = .labelColor
|
|
tv.backgroundColor = .textBackgroundColor
|
|
tv.translatesAutoresizingMaskIntoConstraints = true
|
|
tv.isVerticallyResizable = true
|
|
tv.isHorizontallyResizable = false
|
|
tv.minSize = NSSize(width: 0, height: 0)
|
|
tv.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
|
tv.textContainerInset = NSSize(width: 6, height: 8)
|
|
if let tc = tv.textContainer {
|
|
tc.widthTracksTextView = true
|
|
tc.containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
|
}
|
|
let m = NSMenu(title: "Context")
|
|
let copyItem = NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
|
copyItem.keyEquivalentModifierMask = [.command]
|
|
copyItem.target = nil
|
|
m.addItem(copyItem)
|
|
m.addItem(NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
|
|
tv.menu = m
|
|
return tv
|
|
}()
|
|
private let scrollView: NSScrollView = {
|
|
let sv = NSScrollView()
|
|
sv.hasVerticalScroller = true
|
|
sv.hasHorizontalScroller = false
|
|
sv.autohidesScrollers = true
|
|
sv.translatesAutoresizingMaskIntoConstraints = false
|
|
return sv
|
|
}()
|
|
private let intervalPopup: NSPopUpButton = {
|
|
let p = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
p.translatesAutoresizingMaskIntoConstraints = false
|
|
p.addItems(withTitles: ["10s", "30s", "1 min", "5 min"])
|
|
p.selectItem(withTitle: "30s")
|
|
p.toolTip = "Auto-refresh interval"
|
|
return p
|
|
}()
|
|
private var refreshTimer: Timer?
|
|
private let actions: Actions
|
|
|
|
init(actions: Actions) {
|
|
self.actions = actions
|
|
let style: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
|
|
let initialRect = NSRect(x: 0, y: 0, width: 965, height: 630)
|
|
let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false)
|
|
super.init(window: window)
|
|
|
|
window.isReleasedWhenClosed = false
|
|
window.title = "Prole Database Status"
|
|
window.level = .normal
|
|
window.collectionBehavior = [.canJoinAllSpaces]
|
|
window.appearance = NSAppearance(named: .aqua)
|
|
window.delegate = self
|
|
|
|
let content = NSView()
|
|
content.translatesAutoresizingMaskIntoConstraints = false
|
|
window.contentView = content
|
|
|
|
let headerLabel: NSTextField = {
|
|
let tf = NSTextField(labelWithString: "Prole — Database Status")
|
|
tf.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
|
|
tf.translatesAutoresizingMaskIntoConstraints = false
|
|
return tf
|
|
}()
|
|
content.addSubview(headerLabel)
|
|
|
|
scrollView.borderType = .noBorder
|
|
scrollView.drawsBackground = true
|
|
scrollView.backgroundColor = .textBackgroundColor
|
|
scrollView.documentView = outputTextView
|
|
outputTextView.frame = scrollView.contentView.bounds
|
|
outputTextView.autoresizingMask = [.width, .height]
|
|
content.addSubview(scrollView)
|
|
content.addSubview(intervalPopup)
|
|
|
|
let controls = NSStackView()
|
|
controls.orientation = .horizontal
|
|
controls.spacing = 8
|
|
controls.alignment = .centerY
|
|
controls.translatesAutoresizingMaskIntoConstraints = false
|
|
content.addSubview(controls)
|
|
|
|
let minimizeButton = NSButton(title: "_", target: self, action: #selector(didTapMinimize))
|
|
minimizeButton.bezelStyle = .texturedRounded
|
|
minimizeButton.toolTip = "Minimize to Status Bar"
|
|
controls.addArrangedSubview(minimizeButton)
|
|
|
|
let guide = window.contentLayoutGuide as? NSLayoutGuide
|
|
let topAnchorRef = guide?.topAnchor ?? content.topAnchor
|
|
let leadingAnchorRef = guide?.leadingAnchor ?? content.leadingAnchor
|
|
let trailingAnchorRef = guide?.trailingAnchor ?? content.trailingAnchor
|
|
let bottomAnchorRef = guide?.bottomAnchor ?? content.bottomAnchor
|
|
|
|
NSLayoutConstraint.activate([
|
|
headerLabel.topAnchor.constraint(equalTo: topAnchorRef, constant: 12),
|
|
headerLabel.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 12),
|
|
headerLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchorRef, constant: -12),
|
|
|
|
intervalPopup.centerYAnchor.constraint(equalTo: headerLabel.centerYAnchor),
|
|
intervalPopup.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -12),
|
|
|
|
scrollView.topAnchor.constraint(equalTo: headerLabel.bottomAnchor, constant: 8),
|
|
scrollView.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8),
|
|
scrollView.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -8),
|
|
scrollView.bottomAnchor.constraint(equalTo: bottomAnchorRef, constant: -44),
|
|
|
|
controls.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8),
|
|
controls.bottomAnchor.constraint(equalTo: bottomAnchorRef, constant: -8)
|
|
])
|
|
|
|
intervalPopup.target = self
|
|
intervalPopup.action = #selector(didChangeInterval)
|
|
|
|
runAndDisplay()
|
|
resetTimer()
|
|
DispatchQueue.main.async { [weak self] in self?.adjustFontToFit() }
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
func show() {
|
|
window?.makeKeyAndOrderFront(nil)
|
|
if let tv = outputTextView.window { tv.makeFirstResponder(outputTextView) }
|
|
}
|
|
|
|
func hide() { window?.orderOut(nil) }
|
|
var isVisible: Bool { window?.isVisible ?? false }
|
|
|
|
func position(relativeTo refWindow: NSWindow, offset: CGPoint = CGPoint(x: -20, y: -20)) {
|
|
guard let this = window else { return }
|
|
let refFrame = refWindow.frame
|
|
var newFrame = this.frame
|
|
newFrame.origin.x = refFrame.origin.x + offset.x
|
|
newFrame.origin.y = refFrame.origin.y + offset.y
|
|
this.setFrame(newFrame, display: true, animate: false)
|
|
}
|
|
|
|
@objc private func didTapMinimize() { actions.onMinimizeToStatusBar() }
|
|
@objc private func didChangeInterval() { resetTimer() }
|
|
|
|
private func selectedIntervalSeconds() -> TimeInterval {
|
|
switch intervalPopup.titleOfSelectedItem {
|
|
case "10s": return 10
|
|
case "30s": return 30
|
|
case "1 min": return 60
|
|
case "5 min": return 300
|
|
default: return 30
|
|
}
|
|
}
|
|
|
|
private func resetTimer() {
|
|
refreshTimer?.invalidate()
|
|
let interval = selectedIntervalSeconds()
|
|
refreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
|
self?.runAndDisplay()
|
|
}
|
|
RunLoop.main.add(refreshTimer!, forMode: .common)
|
|
}
|
|
|
|
private func runAndDisplay() {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.outputTextView.string = "Loading database status…"
|
|
self?.outputTextView.scrollToBeginningOfDocument(nil)
|
|
}
|
|
|
|
DispatchQueue.global(qos: .utility).async {
|
|
let res = PFScriptBridge.dbStatus()
|
|
let now = ISO8601DateFormatter().string(from: Date())
|
|
var text = res.out + (res.err.isEmpty ? "" : (res.out.isEmpty ? res.err : "\n" + res.err))
|
|
if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
text = "(no output received)"
|
|
}
|
|
if res.code != 0 { text = "[exit code: \(res.code)]\n" + text }
|
|
let final = "# kubecolor cnpg status prole-db — status (\(now))\n\n" + text
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.outputTextView.string = final
|
|
self?.outputTextView.scrollToBeginningOfDocument(nil)
|
|
self?.adjustFontToFit()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DBStatusWindowController: NSWindowDelegate {
|
|
func windowDidResize(_ notification: Notification) { adjustFontToFit() }
|
|
}
|
|
|
|
private extension DBStatusWindowController {
|
|
func adjustFontToFit() {
|
|
guard let font = outputTextView.font,
|
|
let tc = outputTextView.textContainer,
|
|
let lm = outputTextView.layoutManager else { return }
|
|
|
|
let availableHeight = scrollView.contentView.bounds.height
|
|
guard availableHeight > 0 else { return }
|
|
|
|
func contentHeight(for size: CGFloat) -> CGFloat {
|
|
outputTextView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
|
lm.invalidateLayout(forCharacterRange: NSRange(location: 0, length: lm.numberOfGlyphs), actualCharacterRange: nil)
|
|
lm.ensureLayout(for: tc)
|
|
let used = lm.usedRect(for: tc)
|
|
return used.height + outputTextView.textContainerInset.height * 2.0
|
|
}
|
|
|
|
var minSize: CGFloat = 9
|
|
var maxSize: CGFloat = 12
|
|
let cap: CGFloat = 16
|
|
|
|
var best = maxSize
|
|
var h = contentHeight(for: best)
|
|
if h > availableHeight {
|
|
var s = best
|
|
while s > minSize {
|
|
let next = max(minSize, s - 0.5)
|
|
let nh = contentHeight(for: next)
|
|
if nh <= availableHeight { best = next; break }
|
|
s = next
|
|
}
|
|
} else {
|
|
var s = best
|
|
while s < cap {
|
|
let next = min(cap, s + 0.5)
|
|
let nh = contentHeight(for: next)
|
|
if nh > availableHeight { break }
|
|
best = next; s = next
|
|
}
|
|
}
|
|
outputTextView.font = NSFont.monospacedSystemFont(ofSize: best, weight: .regular)
|
|
outputTextView.scrollToBeginningOfDocument(nil)
|
|
}
|
|
}
|