How to get keyboard inputs (non string) in Command Line Tool application in Swift?
Asked Answered
T

2

0

I am trying to build Tetris Game similar to javidx9 tutorial in C++, but using Swift Command Line Tool in Xcode. I do not plan to use any GUI and display game layout as String of characters. To play the game I need to read Arrow keys inputs while app is running in console. After long reading and googling for solutions I was only been able to find solutions that work only with GUI using keyDown function and NSEvent library.

I need help with getting keyboard inputs (arrow keys, Shift,Command key) in Command Line?

  • Please excuse my lack of experience, I am still in the learning process. I never had this problem using any other language (Python, Java, C++), I do not know why it is hard to find relevant resource for doing this in Swift and XCode*

Thank you!

Tanjatanjore answered 11/11, 2021 at 4:4 Comment(3)
Ask not what C++ can do for you. Ask what you can do for Process.Regulate
I think you’re looking for “ncurses”Rollin
No really. Look into ncurses. rderik.com/blog/…Rollin
A
1

I think you could use C API in swift to do that.

C code is from https://mcmap.net/q/119458/-how-to-avoid-pressing-enter-with-getchar-for-reading-a-single-character-only

This is just a swift version.

import Foundation

var old = termios()
var new = termios()
tcgetattr(STDIN_FILENO, &old)

new = old
new.c_lflag &= ~(UInt(ICANON))
new.c_lflag &= ~(UInt(ECHO))

tcsetattr(STDIN_FILENO, TCSANOW, &new)

while true {
    let c = getchar()
    if c == 113 {
        // quit on 'q'
        break
    } else {
        print(c)
    }
}

tcsetattr(STDIN_FILENO, TCSANOW, &old)

NSEvent.addGlobalMonitorForEvents can get all the key events too but it require user's permission.

import Cocoa
import Foundation

@discardableResult
func acquirePrivileges() -> Bool {
    
    let accessEnabled = AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)
    
    if accessEnabled != true {
        print("You need to enable the keylogger in the System Prefrences")
    }
    
    return accessEnabled
}

class AppDelegate: NSObject, NSApplicationDelegate {
    private var monitor: Any?
    func applicationDidFinishLaunching(_ notification: Notification) {
        acquirePrivileges()
        monitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
            print(event)
        }
    }
}

print(Bundle.main)

// Turn off echo of TTY
var old = termios()
var new = termios()
tcgetattr(STDIN_FILENO, &old)

new = old
new.c_lflag &= ~(UInt(ICANON))
new.c_lflag &= ~(UInt(ECHO))

tcsetattr(STDIN_FILENO, TCSANOW, &new)

let appDelegate = AppDelegate()
NSApplication.shared.delegate = appDelegate
NSApp.activate(ignoringOtherApps: true)
NSApp.run()

// restore tty
tcsetattr(STDIN_FILENO, TCSANOW, &old)

You can also take a look at this library, https://github.com/SkrewEverything/Swift-Keylogger I think it is another interesting way to do the job.

Administrate answered 11/11, 2021 at 6:16 Comment(3)
Thank you! I assume there is no native to swift I/0 access in console?Tanjatanjore
There is no easy way to do that. NSEvent.addGlobalMonitorForEvents could do the work, also HID API is another way, for your reference, github.com/SkrewEverything/Swift-Keylogger.Administrate
I don't think NSEvent.addGlobalMonitorForEvents is the right way to go. It's essentially a keylogger, which is totally unnecessary because the terminal emulator your program is running in already accepts input. I know for one that personally, I would absolutely deny this permission to an app that doesn't have a good reason to use it. It's shifty as heck.Rollin
C
1

For future readers, in pure swift:
Not my original code, modified from here.

import Foundation
import Cocoa


class AppDelegateFinal: NSObject, NSApplicationDelegate {

    func applicationDidFinishLaunching(_: Notification) {
        print("Starting...")

        if (checkPermissions() == false) {
            exit(1)
        }
        
        NSEvent.addGlobalMonitorForEvents(
            matching: [NSEvent.EventTypeMask.keyDown, NSEvent.EventTypeMask.keyUp],
            handler: self.printEvent
        )
    }
    
    func printEvent(event: NSEvent!) {
        switch event.type {
        case .keyDown:
            print("keyDown: " + event.characters!)
        case .keyUp:
            print("keyUp: " + event.characters!)
        default:
          break
        }
    }
    
    private func checkPermissions() -> Bool {
        if (AXIsProcessTrusted() == false) {
            print("Need accessibility permissions!")
            let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
            AXIsProcessTrustedWithOptions(options)
            
            return false;
        } else {
            print("Accessibility permissions active")
            return true;
        }
    }
}

let appDelegate = AppDelegateFinal()
NSApplication.shared.delegate = appDelegate
NSApp.activate(ignoringOtherApps: true)
NSApp.run()

Output:

Starting...
akeyDown: a
keyUp: a
akeyDown: a
keyUp: a
akeyDown: a
keyUp: a
AkeyDown: A
keyUp: A
akeyDown: a
keyUp: a
Program ended with exit code: 9

Re: Permissions See: https://mcmap.net/q/913056/-adding-a-global-monitor-with-nseventmaskkeydown-mask-does-not-trigger

California answered 17/2, 2023 at 23:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.