0

I'm working on implementation of a swift package with an adaptive logger that can determine environment for output purposes. The logger supports all platforms (macOS, iOS, tvOS etc.) and can be used from Tests or Swift command line apps as well.

My question: how to determine that the Swift code is being run under Terminal app for instance when run swift test?

I've found that ProcessInfo's environment property has a "_" field with "/usr/bin/swift" or "/Users/.../TestApp" values when you use the Terminal app but I'm not sure that is a correct approach.

var isTerminal : Bool {
    return ProcessInfo.processInfo.environment["_"] != nil
}

Are there any other approaches to check this?

iUrii
  • 11,742
  • 1
  • 33
  • 48
  • 3
    This sounds like a bit of an [XY problem](https://meta.stackoverflow.com/q/254341); most likely, you don't need to detect whether you're in Terminal.app but rather whether your program is running in an interactive context, or whether specific features like color output are available. Why do you need to know what sort of terminal you're using? – NobodyNada Jul 21 '20 at 15:51
  • @NobodyNada yes, color output is one of why – iUrii Jul 21 '20 at 18:04

1 Answers1

2

Instead of trying to detect whether you're using Terminal.app (which could be error-prone if you're e.g. using a different terminal program or running on Linux), you should instead query the terminal to find out whether it supports specific features. This can be done easily with the ncurses library.

1. Add a CCurses target to your Swift package

We need to create a system library target so that the Swift Package Manager knows how to find and link against the ncurses library Add the target to your Package.swift:

let package = Package(
    // ...
    targets: [
        // ...
        .systemLibrary(name: "CCurses"),
    ],
    // ...
)

And create a new directory at Sources/CCurses with the following files:

curses.h

#include <curses.h>
#include <term.h>

module.modulemap

module CCurses [system] {
  header "curses.h"
  link "curses"
  export *
}

2. Check if the terminal supports color output

When you're setting up, query the terminfo database to check for color support:

import CCurses

// ...

var erret: Int32 = 0
if setupterm(nil, 1, &erret) != ERR {
    useColor = has_colors()
} else {
    useColor = false
}

Based on https://stackoverflow.com/a/7426284.

If you need to check for the existence of another feature, there's almost certainly an ncurses function for it -- just check the man page, search online, or ask here on SO.


The ncurses dylib is available on all platforms, but for some reason the headers only exist on macOS. You shouldn't need it on iOS, watchOS, or tvOS anyway, because those platforms don't have terminals. There are two ways to exclude ncurses from being built on iOS, etc: 1) you can #ifdef out the headers, or 2) with Swift 5.3 you can declare a conditional target dependency, which is slightly simpler and cleaner.

Approach 1: #ifdef the headers

Use the following Sources/CCurses/curses.h file instead:

#ifdef __APPLE__
    #include <TargetConditionals.h>
#endif

#if !(defined TARGET_OS_IPHONE || defined TARGET_OS_WATCH || defined TARGET_OS_TV)
    #include <curses.h>
    #include <term.h>
#endif

Then, whenever you use a function from curses in your Swift code, surround it with a build conditional:

#if os(iOS) || os(watchOS) || os(tvOS)
    useColor = false
#else
    var erret: Int32 = 0
    if setupterm(nil, 1, &erret) != ERR {
        useColor = has_colors()
    } else {
        useColor = false
    }
#endif

Approach 2: conditional target dependency (requires Swift 5.3)

No changes are necessary to the CCurses target; you only have to modify your dependency on CCurses in your Package.swift:

.target(
    name: "MyLib",
    dependencies: [
        .target(name: "CCurses", condition: .when(platforms: [.macOS, .linux]))
]),

And use a build condition whenever you import CCurses or use curses functions:

#if canImport(CCurses)
    import CCurses
#endif

// ...

#if canImport(CCurses)
    var erret: Int32 = 0
    if setupterm(nil, 1, &erret) != ERR {
        useColor = has_colors()
    } else {
        useColor = false
    }
#else
    useColor = false
#endif
NobodyNada
  • 7,529
  • 6
  • 44
  • 51
  • Your answer is great but unfortunately it works on Mac/Mac Catalyst only because on the other platforms the compiler can't find system header files curses.h and term.h. – iUrii Jul 21 '20 at 20:43
  • @iUrii See my edit. Apparently the headers are missing from the iOS, etc. SDKs, even though the library is still present. I assume you don't need to use terminal features on non-desktop platforms, so it's pretty simple to just disable `ncurses` altogether when building for those platforms. – NobodyNada Jul 21 '20 at 21:45
  • It works! The only notice that `TARGET_OS_IPHONE`, `TARGET_OS_WATCH` etc. are always defined for all platforms you should check for their values `#if !(TARGET_OS_IPHONE || TARGET_OS_WATCH ...` – iUrii Jul 22 '20 at 08:48