mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
fix: Linux compatibility for Terminal, SignalManager, and AppStorage
- Replace #if os(Linux) with #if canImport(Glibc/Musl/Darwin) import guards - Add TermFlag typealias to fix termios flag width mismatch (UInt vs UInt32) - Add platform import guard to SignalManager for POSIX signal visibility - Replace DispatchQueue.global with Task.detached in AppStorage - Add XDG_CONFIG_HOME fallback for config directory resolution - Add Linux Build & Test CI job (swift:6.0 container on ubuntu-latest)
This commit is contained in:
@@ -30,6 +30,21 @@ jobs:
|
||||
- name: Run SwiftLint
|
||||
run: swiftlint
|
||||
|
||||
linux-build:
|
||||
name: Linux Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swift:6.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: swift build
|
||||
|
||||
- name: Test
|
||||
run: swift test
|
||||
|
||||
build-docs:
|
||||
name: Build DocC
|
||||
needs: lint
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#elseif canImport(Darwin)
|
||||
import Darwin
|
||||
#endif
|
||||
|
||||
// MARK: - Signal Flags
|
||||
|
||||
/// Flag set by the SIGWINCH signal handler to request a re-render.
|
||||
|
||||
@@ -24,12 +24,36 @@ public protocol StorageBackend: Sendable {
|
||||
func synchronize()
|
||||
}
|
||||
|
||||
// MARK: - Config Directory
|
||||
|
||||
/// Returns the app-specific configuration directory.
|
||||
///
|
||||
/// Resolves the directory in this order:
|
||||
/// 1. `$XDG_CONFIG_HOME/<appName>` (Linux convention)
|
||||
/// 2. `~/.config/<appName>` (fallback)
|
||||
///
|
||||
/// This ensures correct behavior on Linux where `$XDG_CONFIG_HOME`
|
||||
/// may differ from `~/.config`.
|
||||
private func appConfigDirectory() -> URL {
|
||||
let appName = ProcessInfo.processInfo.processName
|
||||
|
||||
if let xdgConfig = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], !xdgConfig.isEmpty {
|
||||
return URL(fileURLWithPath: xdgConfig)
|
||||
.appendingPathComponent(appName)
|
||||
}
|
||||
|
||||
return FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config")
|
||||
.appendingPathComponent(appName)
|
||||
}
|
||||
|
||||
// MARK: - JSON File Storage
|
||||
|
||||
/// A storage backend that persists data to a JSON file.
|
||||
///
|
||||
/// This is the default storage backend for TUIKit apps.
|
||||
/// Data is stored in `~/.config/[appName]/settings.json`.
|
||||
/// Data is stored in `$XDG_CONFIG_HOME/[appName]/settings.json`
|
||||
/// or `~/.config/[appName]/settings.json` as fallback.
|
||||
public final class JSONFileStorage: StorageBackend, @unchecked Sendable {
|
||||
/// The shared instance.
|
||||
public static let shared = JSONFileStorage()
|
||||
@@ -45,10 +69,7 @@ public final class JSONFileStorage: StorageBackend, @unchecked Sendable {
|
||||
|
||||
/// Creates a JSON file storage with default location.
|
||||
public init() {
|
||||
let appName = ProcessInfo.processInfo.processName
|
||||
let configDir = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config")
|
||||
.appendingPathComponent(appName)
|
||||
let configDir = appConfigDirectory()
|
||||
|
||||
// Create directory if needed
|
||||
try? FileManager.default.createDirectory(at: configDir, withIntermediateDirectories: true)
|
||||
@@ -125,7 +146,7 @@ public final class JSONFileStorage: StorageBackend, @unchecked Sendable {
|
||||
}
|
||||
|
||||
private func saveToDiskAsync() {
|
||||
DispatchQueue.global(qos: .utility).async { [weak self] in
|
||||
Task.detached(priority: .utility) { [weak self] in
|
||||
self?.saveToDiskSync()
|
||||
}
|
||||
}
|
||||
@@ -327,10 +348,7 @@ public struct SceneStorage<Value: Codable>: @unchecked Sendable {
|
||||
|
||||
/// Scene-specific storage file.
|
||||
private static var sceneStorage: JSONFileStorage {
|
||||
let appName = ProcessInfo.processInfo.processName
|
||||
let configDir = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config")
|
||||
.appendingPathComponent(appName)
|
||||
let configDir = appConfigDirectory()
|
||||
|
||||
try? FileManager.default.createDirectory(at: configDir, withIntermediateDirectories: true)
|
||||
|
||||
|
||||
@@ -7,12 +7,24 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(Linux)
|
||||
#if canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#elseif canImport(Darwin)
|
||||
import Darwin
|
||||
#endif
|
||||
|
||||
/// Platform-specific type for `termios` flag fields.
|
||||
///
|
||||
/// Darwin uses `UInt` (64-bit), Linux uses `tcflag_t` (`UInt32`).
|
||||
/// This typealias ensures flag bitmask operations compile on both.
|
||||
#if os(Linux)
|
||||
private typealias TermFlag = UInt32
|
||||
#else
|
||||
private typealias TermFlag = UInt
|
||||
#endif
|
||||
|
||||
/// Represents the terminal and controls input and output.
|
||||
///
|
||||
/// `Terminal` is the central interface to the terminal. It provides:
|
||||
@@ -57,7 +69,7 @@ public final class Terminal: @unchecked Sendable {
|
||||
public func getSize() -> (width: Int, height: Int) {
|
||||
var windowSize = winsize()
|
||||
|
||||
#if os(Linux)
|
||||
#if canImport(Glibc) || canImport(Musl)
|
||||
let result = ioctl(STDOUT_FILENO, UInt(TIOCGWINSZ), &windowSize)
|
||||
#else
|
||||
let result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &windowSize)
|
||||
@@ -94,7 +106,7 @@ public final class Terminal: @unchecked Sendable {
|
||||
// ICANON: Canonical mode (line by line)
|
||||
// ISIG: Ctrl+C/Ctrl+Z signals
|
||||
// IEXTEN: Ctrl+V
|
||||
raw.c_lflag &= ~(UInt(ECHO | ICANON | ISIG | IEXTEN))
|
||||
raw.c_lflag &= ~TermFlag(ECHO | ICANON | ISIG | IEXTEN)
|
||||
|
||||
// Disable:
|
||||
// IXON: Ctrl+S/Ctrl+Q software flow control
|
||||
@@ -102,13 +114,13 @@ public final class Terminal: @unchecked Sendable {
|
||||
// BRKINT: Break signal
|
||||
// INPCK: Parity check
|
||||
// ISTRIP: Strip 8th bit
|
||||
raw.c_iflag &= ~(UInt(IXON | ICRNL | BRKINT | INPCK | ISTRIP))
|
||||
raw.c_iflag &= ~TermFlag(IXON | ICRNL | BRKINT | INPCK | ISTRIP)
|
||||
|
||||
// Disable output processing
|
||||
raw.c_oflag &= ~(UInt(OPOST))
|
||||
raw.c_oflag &= ~TermFlag(OPOST)
|
||||
|
||||
// Set character size to 8 bits
|
||||
raw.c_cflag |= UInt(CS8)
|
||||
raw.c_cflag |= TermFlag(CS8)
|
||||
|
||||
// Set timeouts: VMIN=0, VTIME=1 (100ms timeout)
|
||||
// c_cc is a tuple in Swift, so we need to use withUnsafeMutablePointer
|
||||
|
||||
Reference in New Issue
Block a user