Feat: Add statusBarSystemItems modifier for theme/appearance toggles

This commit is contained in:
phranck
2026-02-09 01:12:06 +01:00
parent 59f2fcc609
commit cef2483dbf
3 changed files with 254 additions and 107 deletions
@@ -0,0 +1,87 @@
// 🖥 TUIKit Terminal UI Kit for Swift
// StatusBarSystemItemsModifier.swift
//
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - StatusBarSystemItemsModifier
/// A modifier that configures which system items are shown in the status bar.
///
/// System items are the built-in shortcuts like quit (`q`), theme (`t`),
/// and appearance (`a`). By default, only quit is shown.
///
/// # Example
///
/// ```swift
/// @main
/// struct MyApp: App {
/// var body: some Scene {
/// WindowGroup {
/// ContentView()
/// }
/// .statusBarSystemItems(theme: true, appearance: true)
/// }
/// }
/// ```
struct StatusBarSystemItemsModifier<Content: View>: View {
/// The content view.
let content: Content
/// Whether to show the theme item (`t`).
let showTheme: Bool
/// Whether to show the appearance item (`a`).
let showAppearance: Bool
var body: Never {
fatalError("StatusBarSystemItemsModifier renders via Renderable")
}
}
// MARK: - Renderable
extension StatusBarSystemItemsModifier: Renderable {
func renderToBuffer(context renderContext: RenderContext) -> FrameBuffer {
let statusBar = renderContext.environment.statusBar
statusBar.showThemeItem = showTheme
statusBar.showAppearanceItem = showAppearance
return TUIkit.renderToBuffer(content, context: renderContext)
}
}
// MARK: - View Extension
public extension View {
/// Configures which system items are shown in the status bar.
///
/// System items are the built-in shortcuts:
/// - **quit** (`q`): Always shown by default
/// - **theme** (`t`): Cycles through available color themes
/// - **appearance** (`a`): Cycles through border appearances
///
/// # Example
///
/// ```swift
/// ContentView()
/// .statusBarSystemItems(theme: true, appearance: true)
/// ```
///
/// - Parameters:
/// - theme: Whether to show the theme switcher (`t theme`). Default is `false`.
/// - appearance: Whether to show the appearance switcher (`a appearance`). Default is `false`.
/// - Returns: A view with the configured system items.
func statusBarSystemItems(
theme: Bool = false,
appearance: Bool = false
) -> some View {
StatusBarSystemItemsModifier(
content: self,
showTheme: theme,
showAppearance: appearance
)
}
}
@@ -0,0 +1,67 @@
// 🖥 TUIKit Terminal UI Kit for Swift
// StatusBarSystemItemsModifierTests.swift
//
// Created by LAYERED.work
// License: MIT
import Testing
@testable import TUIkit
@Suite("StatusBarSystemItemsModifier")
struct StatusBarSystemItemsModifierTests {
@Test("Default shows only quit item")
func defaultShowsOnlyQuit() {
let statusBar = StatusBarState()
#expect(statusBar.showSystemItems == true)
#expect(statusBar.showThemeItem == false)
#expect(statusBar.showAppearanceItem == false)
let items = statusBar.currentSystemItems
#expect(items.count == 1)
#expect(items.first?.shortcut == "q")
}
@Test("Theme item can be enabled")
func themeItemEnabled() {
let statusBar = StatusBarState()
statusBar.showThemeItem = true
let items = statusBar.currentSystemItems
#expect(items.count == 2)
#expect(items.contains { $0.shortcut == "q" })
#expect(items.contains { $0.shortcut == "t" })
}
@Test("Appearance item can be enabled")
func appearanceItemEnabled() {
let statusBar = StatusBarState()
statusBar.showAppearanceItem = true
let items = statusBar.currentSystemItems
#expect(items.count == 2)
#expect(items.contains { $0.shortcut == "q" })
#expect(items.contains { $0.shortcut == "a" })
}
@Test("Both theme and appearance can be enabled")
func bothItemsEnabled() {
let statusBar = StatusBarState()
statusBar.showThemeItem = true
statusBar.showAppearanceItem = true
let items = statusBar.currentSystemItems
#expect(items.count == 3)
#expect(items.contains { $0.shortcut == "q" })
#expect(items.contains { $0.shortcut == "t" })
#expect(items.contains { $0.shortcut == "a" })
}
@MainActor
@Test("Modifier creates correct view")
func modifierCreatesView() {
let view = Text("Test")
.statusBarSystemItems(theme: true, appearance: true)
#expect(view is StatusBarSystemItemsModifier<Text>)
}
}
+100 -107
View File
@@ -293,7 +293,7 @@ show_help() {
echo ""
echo "Options:"
echo " git Initialize Git repository with initial commit"
echo " sqlite Include SQLiteData for database storage"
echo " sqlite Include GRDB for SQLite database storage"
echo " testing Include Swift Testing framework"
echo " xctest Include XCTest framework"
echo ""
@@ -365,19 +365,48 @@ if [ -z "$PROJECT_NAME" ]; then
exit 1
fi
# Header
echo -e "${GREEN}"
echo "╔════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ TUIkit Project Creator ║"
echo "║ ║"
echo "╚════════════════════════════════════════════════════╝"
echo -e "${NC}"
# Banner with grayscale block style (like opencode)
show_banner() {
# Grayscale ANSI colors (gradient from dark to light)
local C1='\033[38;5;240m' # Darkest (T)
local C2='\033[38;5;244m' # (U)
local C3='\033[38;5;248m' # (I)
local C4='\033[38;5;252m' # (k)
local C5='\033[38;5;254m' # (i)
local C6='\033[38;5;255m' # Brightest (t)
local D='\033[38;5;236m' # Dark inner
local R='\033[0m' # Reset
echo ""
# TUIkit in block letters with inner shadow effect
echo -e "${C1}█${D}█${C1}█████${D}█${R} ${C2}█${D}█${R} ${C2}█${D}█${R} ${C3}█${D}█${C3}█${R} ${C4}█${D}█${R} ${C4}█${D}█${R} ${C5}█${D}█${R} ${C6}█${D}█${C6}███${R}"
echo -e " ${C1}█${D}█${R} ${C2}█${D}█${R} ${C2}█${D}█${R} ${C3}█${D}█${R} ${C4}█${D}█${R} ${C4}█${D}█${R} ${C5}█${D}█${R} ${C6}█${D}█${R}"
echo -e " ${C1}█${D}█${R} ${C2}█${D}█${R} ${C2}█${D}█${R} ${C3}█${D}█${R} ${C4}█${D}█${C4}█${D}█${R} ${C5}█${D}█${R} ${C6}█${D}█${R}"
echo -e " ${C1}█${D}█${R} ${C2}█${D}█${R} ${C2}█${D}█${R} ${C3}█${D}█${R} ${C4}█${D}█${R} ${C4}█${D}█${R} ${C5}█${D}█${R} ${C6}█${D}█${R}"
echo -e " ${C1}█${D}█${R} ${C2}█${D}█${C2}██${D}█${C2}█${R} ${C3}█${D}█${C3}█${R} ${C4}█${D}█${R} ${C4}█${D}█${R} ${C5}█${D}█${R} ${C6}█${D}█${C6}█${R}"
echo ""
}
show_banner
# Separate path and project name
# If PROJECT_NAME contains a path, extract the directory and basename
if [[ "$PROJECT_NAME" == *"/"* ]]; then
# Expand ~ to home directory
PROJECT_PATH="${PROJECT_NAME/#\~/$HOME}"
PROJECT_BASE=$(basename "$PROJECT_PATH")
else
PROJECT_PATH="$(pwd)/$PROJECT_NAME"
PROJECT_BASE="$PROJECT_NAME"
fi
# Create project directory
echo -e "${GREEN}Creating project directory...${NC}"
mkdir -p "$PROJECT_NAME"
cd "$PROJECT_NAME"
mkdir -p "$PROJECT_PATH"
cd "$PROJECT_PATH"
# Use PROJECT_BASE for all file content (package name, etc.)
PROJECT_NAME="$PROJECT_BASE"
# Create Package.swift
echo -e "${GREEN}Creating Package.swift...${NC}"
@@ -386,14 +415,14 @@ echo -e "${GREEN}Creating Package.swift...${NC}"
DEPENDENCIES=" .package(url: \"https://github.com/phranck/TUIkit.git\", branch: \"main\")"
if [ "$INCLUDE_SQLITE" = true ]; then
DEPENDENCIES="${DEPENDENCIES},
.package(url: \"https://github.com/pointfreeco/sqlite-data.git\", from: \"0.1.0\")"
.package(url: \"https://github.com/groue/GRDB.swift.git\", from: \"7.0.0\")"
fi
# Build target dependencies
TARGET_DEPS=" .product(name: \"TUIkit\", package: \"TUIkit\")"
if [ "$INCLUDE_SQLITE" = true ]; then
TARGET_DEPS="${TARGET_DEPS},
.product(name: \"SQLiteData\", package: \"sqlite-data\")"
.product(name: \"GRDB\", package: \"GRDB.swift\")"
fi
# Build targets array
@@ -469,17 +498,29 @@ EOF
if [ "$INCLUDE_SQLITE" = true ]; then
cat > Sources/Database.swift << 'EOF'
import Foundation
import SQLiteData
import GRDB
/// Example model using SQLiteData's @Table macro
///
/// SQLiteData uses GRDB under the hood and provides
/// SwiftData-like macros for database models.
@Table
struct Item: Sendable {
let id: UUID
var title = ""
var isCompleted = false
/// Example model using GRDB
struct Item: Codable, FetchableRecord, PersistableRecord, Sendable {
var id: Int64?
var title: String
var isCompleted: Bool
// Define the database table
static let databaseTableName = "items"
}
// MARK: - Database Setup
extension Item {
/// Creates the items table
static func createTable(in db: Database) throws {
try db.create(table: databaseTableName, ifNotExists: true) { table in
table.autoIncrementedPrimaryKey("id")
table.column("title", .text).notNull()
table.column("isCompleted", .boolean).notNull().defaults(to: false)
}
}
}
EOF
fi
@@ -556,31 +597,34 @@ if [ "$INCLUDE_SQLITE" = true ]; then
## Database
This project includes [SQLiteData](https://github.com/pointfreeco/sqlite-data) for local data persistence.
This project includes [GRDB](https://github.com/groue/GRDB.swift) for local SQLite persistence.
**Quick Start:**
```swift
import SQLiteData
import GRDB
// Define a model
@Table
struct User: Sendable {
let id: UUID
var name = ""
// Open database
let dbQueue = try DatabaseQueue(path: "db.sqlite")
// Create table
try dbQueue.write { db in
try Item.createTable(in: db)
}
// Fetch all users
@FetchAll var users: [User]
// Insert
try dbQueue.write { db in
var item = Item(id: nil, title: "Task", isCompleted: false)
try item.insert(db)
}
// Insert a user
try database.write { db in
try User.insert { User(id: UUID(), name: "Alice") }
.execute(db)
// Fetch all
let items = try dbQueue.read { db in
try Item.fetchAll(db)
}
```
See [SQLiteData documentation](https://github.com/pointfreeco/sqlite-data) for more details.
See [GRDB documentation](https://github.com/groue/GRDB.swift) for more details.
EOF
fi
@@ -605,72 +649,11 @@ DerivedData/
.netrc
EOF
# Create .swiftpm directory with scheme
mkdir -p .swiftpm/xcode/xcshareddata/xcschemes
# Note: We don't create .swiftpm directory - Xcode generates it automatically
# and handles scheme/workspace settings better on its own
cat > .swiftpm/xcode/xcshareddata/xcschemes/${PROJECT_NAME}.xcscheme << EOF
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "$PROJECT_NAME"
BuildableName = "$PROJECT_NAME"
BlueprintName = "$PROJECT_NAME"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
viewDebuggingEnabled = "No">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "$PROJECT_NAME"
BuildableName = "$PROJECT_NAME"
BlueprintName = "$PROJECT_NAME"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
</Scheme>
EOF
cat > .swiftpm/xcode/xcshareddata/WorkspaceSettings.xcsettings << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Original</string>
</dict>
</plist>
EOF
# Configure Xcode preferences
echo -e "${BLUE}Configuring Xcode preferences...${NC}"
# Set Xcode preference for Swift packages to prefer macOS
defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM -bool YES 2>/dev/null || true
defaults write com.apple.dt.Xcode IDEPreferredPlatformForSwiftPackages -string "macosx" 2>/dev/null || true
# Initialize Git repository if requested
@@ -694,20 +677,30 @@ echo -e "${BLUE}Project: ${NC}${GREEN}$PROJECT_NAME${NC}"
echo -e "${BLUE}Location: ${NC}$(pwd)"
echo -e "${BLUE}Git Repository: ${NC}$([ "$INIT_GIT" = true ] && echo "Yes" || echo "No")"
echo -e "${BLUE}Test Framework: ${NC}$TEST_FRAMEWORK"
echo -e "${BLUE}SQLiteData: ${NC}$([ "$INCLUDE_SQLITE" = true ] && echo "Yes" || echo "No")"
echo -e "${BLUE}GRDB: ${NC}$([ "$INCLUDE_SQLITE" = true ] && echo "Yes" || echo "No")"
echo ""
read -p "Open project in Xcode? [Y/n] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
open Package.swift
open "$PROJECT_PATH/Package.swift"
echo -e "${GREEN}Opening in Xcode...${NC}"
# Show macOS alert about deployment target (only on macOS)
if [ "$(uname)" = "Darwin" ]; then
echo ""
echo -e "${YELLOW}╔════════════════════════════════════════════════════╗${NC}"
echo -e "${YELLOW}║ IMPORTANT: Set Run Destination to \"My Mac\" ║${NC}"
echo -e "${YELLOW}║ in Xcode's toolbar before building! ║${NC}"
echo -e "${YELLOW}╚════════════════════════════════════════════════════╝${NC}"
fi
else
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo " 1. cd $PROJECT_NAME"
echo " 1. cd $PROJECT_PATH"
echo " 2. open Package.swift"
echo " 3. Wait for dependencies to resolve"
echo " 4. Press Cmd+B to build"
echo " 3. Set Run Destination to \"My Mac\" in Xcode"
echo " 4. Wait for dependencies to resolve"
echo " 5. Press Cmd+B to build"
fi