refactor: Quick wins — deduplicate Alert presets, remove renderView duplicate, refactor Color/Focus, replace magic values (A.9, A.11, A.13, A.14, C.5, F.3, F.4)

This commit is contained in:
phranck
2026-01-30 13:48:45 +01:00
parent e59c271ac5
commit dfcbbdc744
10 changed files with 67 additions and 85 deletions
+15 -12
View File
@@ -216,15 +216,7 @@ public struct Color: Sendable, Equatable {
/// - Parameter amount: The amount to lighten (0-1, default 0.2).
/// - Returns: A lighter color.
public func lighter(by amount: Double = 0.2) -> Color {
guard case .rgb(let red, let green, let blue) = value else {
return self
}
let newRed = UInt8(min(255, Double(red) + 255 * amount))
let newGreen = UInt8(min(255, Double(green) + 255 * amount))
let newBlue = UInt8(min(255, Double(blue) + 255 * amount))
return .rgb(newRed, newGreen, newBlue)
adjusted(by: amount)
}
/// Returns a darker version of this color.
@@ -232,13 +224,24 @@ public struct Color: Sendable, Equatable {
/// - Parameter amount: The amount to darken (0-1, default 0.2).
/// - Returns: A darker color.
public func darker(by amount: Double = 0.2) -> Color {
adjusted(by: -amount)
}
/// Adjusts brightness by a signed amount.
///
/// Positive values lighten, negative values darken.
///
/// - Parameter amount: The adjustment amount (-1 to 1).
/// - Returns: The adjusted color, or self if not an RGB color.
private func adjusted(by amount: Double) -> Color {
guard case .rgb(let red, let green, let blue) = value else {
return self
}
let newRed = UInt8(max(0, Double(red) - 255 * amount))
let newGreen = UInt8(max(0, Double(green) - 255 * amount))
let newBlue = UInt8(max(0, Double(blue) - 255 * amount))
let shift = 255 * amount
let newRed = UInt8(min(255, max(0, Double(red) + shift)))
let newGreen = UInt8(min(255, max(0, Double(green) + shift)))
let newBlue = UInt8(min(255, max(0, Double(blue) + shift)))
return .rgb(newRed, newGreen, newBlue)
}
+1 -18
View File
@@ -202,24 +202,7 @@ extension EnvironmentModifier: Renderable {
// Render content with modified environment
return EnvironmentStorage.shared.withEnvironment(modifiedEnvironment) {
renderView(content, context: modifiedContext)
TUIKit.renderToBuffer(content, context: modifiedContext)
}
}
}
// MARK: - Internal Rendering Helper
/// Internal helper to render a view (avoids name collision with Renderable.renderToBuffer).
private func renderView<V: View>(_ view: V, context: RenderContext) -> FrameBuffer {
if let renderable = view as? Renderable {
return renderable.renderToBuffer(context: context)
}
if V.Body.self != Never.self {
return renderView(view.body, context: context)
}
return FrameBuffer()
}
+24 -19
View File
@@ -148,24 +148,23 @@ public final class FocusManager: @unchecked Sendable {
/// Moves focus to the next focusable element.
public func focusNext() {
guard !focusables.isEmpty else { return }
let availableFocusables = focusables.filter { $0.canBeFocused }
guard !availableFocusables.isEmpty else { return }
if let currentID = focusedID,
let currentIndex = availableFocusables.firstIndex(where: { $0.focusID == currentID }) {
// Move to next (wrap around)
let nextIndex = (currentIndex + 1) % availableFocusables.count
focus(availableFocusables[nextIndex])
} else {
// Focus first available
focus(availableFocusables[0])
}
moveFocus(direction: .forward)
}
/// Moves focus to the previous focusable element.
public func focusPrevious() {
moveFocus(direction: .backward)
}
/// The direction in which focus moves.
private enum FocusDirection {
case forward, backward
}
/// Moves focus in the specified direction (wrapping around).
///
/// - Parameter direction: The direction to move focus.
private func moveFocus(direction: FocusDirection) {
guard !focusables.isEmpty else { return }
let availableFocusables = focusables.filter { $0.canBeFocused }
@@ -173,12 +172,18 @@ public final class FocusManager: @unchecked Sendable {
if let currentID = focusedID,
let currentIndex = availableFocusables.firstIndex(where: { $0.focusID == currentID }) {
// Move to previous (wrap around)
let prevIndex = currentIndex == 0 ? availableFocusables.count - 1 : currentIndex - 1
focus(availableFocusables[prevIndex])
let targetIndex: Int
switch direction {
case .forward:
targetIndex = (currentIndex + 1) % availableFocusables.count
case .backward:
targetIndex = currentIndex == 0 ? availableFocusables.count - 1 : currentIndex - 1
}
focus(availableFocusables[targetIndex])
} else {
// Focus last available
focus(availableFocusables[availableFocusables.count - 1])
// No current focus: forward → first, backward → last
let fallbackIndex = direction == .forward ? 0 : availableFocusables.count - 1
focus(availableFocusables[fallbackIndex])
}
}
+6 -1
View File
@@ -640,12 +640,16 @@ public struct GeneratedTheme: Theme, Sendable {
/// Predefined hue values for common generated themes.
public enum Hue {
/// Green hue (120°).
public static let green: Double = 120
/// Violet hue (270°).
public static let violet: Double = 270
}
// MARK: - Presets
/// A green generated theme — for direct comparison with GreenPhosphorTheme.
public static let green = GeneratedTheme(name: "Gen. Green", hue: Hue.green)
/// A violet generated theme.
public static let violet = GeneratedTheme(name: "Violet", hue: Hue.violet)
}
@@ -656,9 +660,10 @@ public struct GeneratedTheme: Theme, Sendable {
public struct ThemeRegistry {
/// All available themes in cycling order.
///
/// Order: Green → Amber → White → Red → NCurses → Violet (generated)
/// Order: Green → Gen. Green → Amber → White → Red → NCurses → Violet (generated)
public static let all: [Theme] = [
GreenPhosphorTheme(),
GeneratedTheme.green,
AmberPhosphorTheme(),
WhitePhosphorTheme(),
RedPhosphorTheme(),
@@ -34,7 +34,7 @@ extension BorderedView: Renderable {
// Reduce available width for content by 2 (left + right border)
var contentContext = context
contentContext.availableWidth = max(1, context.availableWidth - 2)
contentContext.availableWidth = max(1, context.availableWidth - BorderRenderer.borderWidthOverhead)
// Render content with reduced width
let buffer = TUIKit.renderToBuffer(content, context: contentContext)
@@ -16,6 +16,9 @@
/// - **Block**: half-block characters (▄ █ ▀) for smooth visual edges
public enum BorderRenderer {
/// The total width consumed by left + right border characters (1 + 1 = 2).
public static let borderWidthOverhead = 2
// MARK: - Standard Style (Box-Drawing Characters)
/// Renders a plain top border line.
@@ -155,7 +158,7 @@ public enum BorderRenderer {
innerWidth: Int,
color: Color
) -> String {
let line = String(repeating: "▄", count: innerWidth + 2)
let line = String(repeating: BorderStyle.block.horizontal, count: innerWidth + 2)
return ANSIRenderer.colorize(line, foreground: color)
}
@@ -171,7 +174,7 @@ public enum BorderRenderer {
innerWidth: Int,
color: Color
) -> String {
let line = String(repeating: "▀", count: innerWidth + 2)
let line = String(repeating: BorderStyle.blockBottomHorizontal, count: innerWidth + 2)
return ANSIRenderer.colorize(line, foreground: color)
}
@@ -191,7 +194,7 @@ public enum BorderRenderer {
sectionColor: Color
) -> String {
let paddedLine = content.padToVisibleWidth(innerWidth)
let sideBorder = ANSIRenderer.colorize("█", foreground: sectionColor)
let sideBorder = ANSIRenderer.colorize(String(BorderStyle.block.vertical), foreground: sectionColor)
let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: sectionColor)
return sideBorder + styledContent + ANSIRenderer.reset + sideBorder
}
@@ -203,13 +206,13 @@ public enum BorderRenderer {
///
/// - Parameters:
/// - innerWidth: The content width (separator width = innerWidth + 2).
/// - character: The separator character (`"▀"` for header→body, `"▄"` for body→footer).
/// - character: The separator character (`.blockBottomHorizontal` for header→body, `.blockFooterSeparator` for body→footer).
/// - foregroundColor: The FG color (the section being transitioned from or to).
/// - backgroundColor: The BG color (the adjacent section).
/// - Returns: The separator line.
public static func blockSeparator(
innerWidth: Int,
character: Character = "▀",
character: Character = BorderStyle.blockBottomHorizontal,
foregroundColor: Color,
backgroundColor: Color
) -> String {
+6 -3
View File
@@ -31,10 +31,13 @@ public let tuiKitVersion = "0.1.0"
/// ```
///
/// - Parameter content: A ViewBuilder closure that defines the view to render.
@discardableResult
public func renderOnce<Content: View>(@ViewBuilder content: () -> Content) -> Int {
/// Renders a view hierarchy once and prints the result to standard output.
///
/// This is useful for simple CLI tools that don't need a full App lifecycle.
///
/// - Parameter content: A ViewBuilder closure that defines the view to render.
public func renderOnce<Content: View>(@ViewBuilder content: () -> Content) {
let view = content()
let renderer = ViewRenderer()
renderer.render(view)
return 0 // Line count not tracked by ViewRenderer
}
+4 -24
View File
@@ -256,41 +256,21 @@ extension Alert {
extension Alert where Actions == EmptyView {
/// Creates a warning-style alert without actions.
public static func warning(title: String = "Warning", message: String) -> Alert<EmptyView> {
Alert<EmptyView>(
title: title,
message: message,
borderColor: .yellow,
titleColor: .yellow
)
Alert.warning(title: title, message: message) { EmptyView() }
}
/// Creates an error-style alert without actions.
public static func error(title: String = "Error", message: String) -> Alert<EmptyView> {
Alert<EmptyView>(
title: title,
message: message,
borderColor: .red,
titleColor: .red
)
Alert.error(title: title, message: message) { EmptyView() }
}
/// Creates an info-style alert without actions.
public static func info(title: String = "Info", message: String) -> Alert<EmptyView> {
Alert<EmptyView>(
title: title,
message: message,
borderColor: .cyan,
titleColor: .cyan
)
Alert.info(title: title, message: message) { EmptyView() }
}
/// Creates a success-style alert without actions.
public static func success(title: String = "Success", message: String) -> Alert<EmptyView> {
Alert<EmptyView>(
title: title,
message: message,
borderColor: .green,
titleColor: .green
)
Alert.success(title: title, message: message) { EmptyView() }
}
}
+1 -1
View File
@@ -330,7 +330,7 @@ extension ContainerView: Renderable {
if let footerBuf = footerBuffer, !footerBuf.isEmpty {
if style.showFooterSeparator {
lines.append(BorderRenderer.blockSeparator(
innerWidth: innerWidth, character: "▄",
innerWidth: innerWidth, character: BorderStyle.blockFooterSeparator,
foregroundColor: headerFooterBg, backgroundColor: bodyBg
))
}
+1 -1
View File
@@ -896,7 +896,7 @@ extension StatusBar: Renderable {
/// Renders the bordered style using the current appearance's border style.
private func renderBordered(itemStrings: [String], width: Int, context: RenderContext) -> FrameBuffer {
let innerWidth = width - 2 // Account for left and right border
let innerWidth = width - BorderRenderer.borderWidthOverhead
let content = alignContent(itemStrings: itemStrings, width: innerWidth)
// Check if we're using block appearance for special rendering