Git Product home page Git Product logo

termkit's Introduction

TermKit - Terminal UI Toolkit for Swift

This is a simple UI Toolkit for Swift, a port of my gui.cs library for .NET. While I originally wrote gui.cs, it has evolved significantly by the contributions of Charlie Kindel (@tig), @BDisp and various other contributors - this port is bringing their work.

This toolkit contains various controls for build text user interfaces using Swift.

You can checkout the documentation

Screen Shot 2021-03-13 at 12 44 05 PM

Running this

From the command line:

$ swift build
$ swift run

From Xcode, if you want to debug, it is best to make sure that the application that you want to Debug (in this project, the "Example" target is what you want) has its Scheme for Running configured like this:

 * Run/Info: Launch "Wait for Executable to be launched"

Then, when you run, switch to a console, and run the executable, I have my global settings for DerivedData to be relative to the current directory, so I can run it like this:

$ DerivedData/TermKit/Build/Products/Debug/Example

The location for where your executable is produced is configured in Xcode/Preferences/Locations, I just happen to like project-relative output like the example above shows.

Debugging

While debugging is useful, sometimes it can be obnoxious to single step or debug over code that is called too many times in a row, so printf-like debugging is convenient.

Except that prints go to the same console where your application is running, making this experience painful.

In that case, you can call Application.log with a message, and this message will use MacOS os_log, which you can then either look for in the Console.app, or you can monitor from a terminal window like this:

$ log stream --style compact --predicate 'subsystem == "termkit"'

termkit's People

Contributors

joematt avatar migueldeicaza avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

termkit's Issues

Toplevel/Application redesign ideas

I think it would be useful to redesign Toplevel, because:

  • It currently handles view-navigation, defaults like Control-L, control-Z, Control-C (not really)
  • Handles navigation in/out
  • Introduces "modal" flag, tracked by Application to determine whether to bubble up an event
  • Tracks menubar/statusbar if present, without being a full desktop.

Application:

  • Feels like the name is intended for things that this is not
  • Tracks the current toplevel
  • Is the place where events are dispatched from (via static method to the top-level list)
  • Tracks mouse grabbing
  • Display refresh is handled here
  • Application suspend/Resize

Currently, Window subclasses Toplevel.

So we could have something like this:

Toplevel:

  • Backing store for rendering
  • Default handlers for control-l, control-z, control-c
  • StandardToplevel, a Toplevel subclass with standard elements
    • Provide a standard dialect for menubar, statusbar, desktop
    • tiled windows
    • But not be the only way to run, I could for example want to run the toolkit, without hijacking the full screen

Question: what is the shared base class for Window and Application? Maybe TopLevel become just the host for the backing store and the modal attribute?

Application (not sure if this is the best name)

  • Refresh, resize, suspend logic gets moves here
  • Modality of the current
  • In addition, I want to introduce a "layer" backing store where views draw at some level, and that one feels like it should be "Window", or maybe a base class for Window. This is discussed here #30

TBD:

  • What is the "toplevel" equivalent in TermProgram? Because currently we use the toplevel.modal flag to determine whether to propagate the event.

Modality alternative: maybe making something modal creates a full-screen view that captures all mouse and keyboard events, but does not draw everything (or is transparent).

Attributed strings

I would like to introduce attributed strings, that can encode things like color, but the question is: how to support both the plain and attributed strings in the same API.

Options:

  • Expose dual constructors/properties (Label (plain: "Hello"), Label ("[yellow]H[/yellow]ello")
  • Rather than using strings, using "DrawableString" and then users manually construct either one: Label (Drawable (plain: "Hello") or Label (Drawable ("[yellow]H[/yellow]ello"))".

The markup could be bbcode, as used in "rich":

https://github.com/willmcgugan/rich

SwiftUI-like API

Once the toolkit is operational, I want to add a "builder" API similar to the SwiftUI, that might necessitate a different namespace, I am thinking 'TermUI'.

Would be useful to implement this:

#18

What I think would be useful is to have a different name space, "TermKitUI" for that API.

Label - support for linking to a target

Labels should support hotkeys, and also a way of linking to a target

This:

var username = TextField ()
var login = Label ("_Login", target: username)

Would make it so that the "Login" has the "L" highlighted and treated like a hotkey, but when activated, it activates the target.

Painter and ScrollViews

The current painter is useful to track the cursor position, but it could be augmented to support clipping, and it could be augmented to solve the problem with ScrollView, to allow arbitrary controls to be embedded into a ScrollView and behave the same way a control works inside a UIScrollView - that is, they do not know that they are being scrolled.

I have a patch that passes a "painter' argument to every redraw function, and in ScrollView, a new ContentView that can pass a Painter that has been altered to deal with a content offset (drawing before this point does nothing).

For this to work though, it is not enough for the top-level control to take a modifier painter, but Painter objects need to be created with a parent link, so rather than just calling Painter (from: view), it would need to use Painter (from: view, on: currentPainter).

That is one option.

Another option would be to make the Painter method allocate a buffer for the bounds, and upon completion of the rendering, it would copy the data to the right location. This has the advantage that we could properly clip everything.

Still thinking about this.

This patch contains the wiring up of Painter, but does not do the double buffering described here:

https://gist.github.com/migueldeicaza/954c2c64be08d8bb7849f2374759e25c

That would also make it uglier to debug with sync

ScrollView tasks

  • should have three modes for showing the scrollbar: never, auto, always
  • should figure out auto-focus, and scroll the view to the right place based on what is focused
  • view.clear() does not work well with contentOffset, as it does not cover the entire region
  • view.redraw() is getting the full size, not the visible size of the ScrollView region affected (so everything has to draw from 0, even if not needed). A child view of say 100x100 inside a 10x10 where contenOffset=30,30 should get a region with (30-30, w:10, h:10) so it can optimize its rendering.
  • Find a way to delegate positionCursor to a child, but if it is out of bounds, snap to bounds. Will likely need positionCursor to be changed, and perhaps return the position, and the caller snap to the boundary.

Terminal vs main loop

Turns out that the FileHandle.readability is running on a background thread, so I need to move that to the main thread.

Additionally, need to move the SwiftTerm processing to a dedicated thread, rather than having that on the main thread.

Curse-less Unix terminal driver

Currently, TermKit relies on ncurses, which poses a number of problems:

  • Sometimes the library has not been updated in years, so we need to resort to fallbacks, or avoid newer capabilities
  • Sometimes it has been compiled with different options or modern capabilities were not enabled at build time.

Some of the very concrete examples include Apple's version that does not even support bold colors in ncurses, let alone 256 colors or true color.

So it might be time to implement a native driver, both one that can work with arbitrary terminfo databases, as well as one for "true" xterm emulators, where we can leverage the entire set of xterm capabilities.

Platform-specific Keybindings

The Emacs keybindings and the Windows keybindings can not be supported at the same time, it looks like I should have a keybinding system to support those.

For example the Windows style is C-c is copy, C-x is cut, C-v is paste. The "c-x" is likely going to clash with other Unix idioms, as is "c-v" used for scrolling in Emacs mode.

Embedded Terminal Issues with bash (zsh is fine)

bash bugs, do not happen with zsh

  • Control-C for some reason is not being processed by bash, yet, if you type "cat" and send c-c you can see "^c" being shown, but does not terminate the process - even if the stty -a shows that intr is set to ^C.
  • mc fails to startup, stuck in spawning the child process, not sure what (feed_subshell), so something is odd or funky, perhaps the prompt? This works with /bin/zsh, only bash seems to be getting stuck.

Implement clipping

Both TermKit and gui.cs relied on "good behavior" from controls in terms of staying within their "frame" when drawing. This is generally doable, but every once in a while, a control escapes that does not take this into account and messes up the display.

More troublesome are user controls that do not want to be bothered with these details, and are a source of problems.

The design proposed for gui.cs should be replicated here: the addCh/addStr methods should not be exposed by the driver, but by the view, and the view should enforce the clipping, as well as a "logical" cursor position

Idea: Support Keymaps/Chords

I would like to support keymaps/chords so users could define things like "C-x C-f" as "Find file" for example.

Porting code from gui.cs Tracking Issue

This issue tracks my notes on the porting bits that I have done while moving code from gui.cs written in C# and TermKit written in Swift.

Generally, TermKit uses the same model of gui.cs, but has been adapted to the Swift language, and among other things, takes advantage of "Char" being a proper Unicode character.

While I have been porting most of the code from gui.cs to TermKit, in a few instances in the core, I have either not embraced a particular change, either because I do not like what is there on the C# code base, or because I am planning on using something different in TermKit. Some of those are documented here, and I will update as I audit or remember more things

Design Differences

While gui.cs embraced a more powerful View by default, one that contains text and rendering options, I chose to not put this into TermKit for now.

To deal with boundaries and the cursor location, in TermKit, I am using a drawing context that keeps track of this during the drawing stage, so it does not take space on the View itself to track.

Future Directions

Currently, TermKit is using ncurses for its Unix backend, but I am going to provide a direct version that speaks directly to the terminal, because (a) it is not that hard to write, (b) we would have a lot better control what what ncurses offer, but (c) would help me avoid dealing with the various versions and configurations of ncurses that ship in the wild that are nothing but a source of pain (specially dealing with colors, timeouts and unicode).

Audit Results

I have recently completed an Audit (as of Jan 2021) and the following pieces of code are either fully ported, or I consider them done for the purposes of the port. The checked options mean completed, others are open tasks:

  • Dim
  • Pos
  • Core/Driver
    • Recently introduced capability to send KeyDown before ProcessKeyEvent, followed by KeyUp. This looks useful in Windows where we can tell the difference, and in particular is leveraged by MenuBar.OnKeyDown which checks for “Alt” key being set to close the menu
  • Core/Application
    • Not convinced I need this:
      • HeightAsBuffer
      • AlwaysSetPosition
    • Application/postProcessEvent
      • This is where I do layout/redraw, and I think this could be different, rather than being done ad-hoc after processing one keystroke, we could do this after we process several. This is there because we do not really “own” the main loop, so we basically do the layout/paint here. What we should do instead is any layoutNeeded/setDisplayNeeded need to set a flag that triggers an execution before going back to reading more data from input, so that we process all of these at once.
    • ProcessKeyDownEvent/ProcessKeyUpEvent
      • Currently not implemented, used in Windows.
    • RequestStop() is much simpler in C# that Swift - audit why
  • Core/TopLevel
    • Whether we should have an ISupportInitialize support (DECIDED: no)
    • Events: Loaded/Ready/Unloaded
    • StatusBar
    • Needs support in Application for “requestStop”, which should trigger something in DispatchIO to trigger Swift code to terminate the current top-level
    • PostProcessEvent - needs shortcut lookup, FindAndOpenByShortcut
  • Core/ConsoleDrier
  • Core/Event
  • Core/MainLoop
  • Core/Responder
  • Core/ShortcutHelper
    • Not clear to me how this ShortcutHelper is used across the codebase
  • Core/TextFormatter
  • Core/View
    • UPCOMING: layoutSubviews JUST ÇOMPLETED
    • Pending:
      • PENDING: Move the various Draw methods out
      • PENDING: remove “MoveTo”
      • PENDING: Draw event
      • Pending: raise keyboard events using Combine
      • Pending: if we are raising events, we should have a way of stopping events, which for the mouse, I currently do not do/
      • Data on the fence about this payload
    • Debatable:
      • AutoSize relies on the Text being part of the View, which I didn't make it. I need to think whether AutoSize should be a property, or if it should be the default. I suspect that views that change internally need to have their layout recomputed, but need to think about this.
      • ShortcutHelper if I were to add this, I would probably introduce a protocol, and introduce a default implementation, rather than putting this on View.
      • HotKey, HotKeySpecified is not supported, because I do not think that the View should have this. This probably should be a protocol, or specific to a View.
      • Shortcut, ShortcutTag, ShortcutAction: same rationale as HotKey above.
      • Will ignore HotKeySpecifier
  • Core/Window

Views are now tracked as individual bugs

Clipping:

  • Currently the clipping when creating a painter will draw outside the screen if the terminal is bigger.

Rendering:

  • View.drawFrame should be rewritten with Painter
  • View.drawHotString should be rewritten with Painter

Some of these controls either exist, but have not been synced to the fixes and features of gui.cs, or I have not ported them yet:

Most of the Dialogs:

  • FileDialog
  • MessageBox
  • Make driver.constants for characters part of the painter.

Control: need to implement TableView.

The debate is whether it should be a TableView like in UIKit with delegates and all the ceremony that comes with it, or something a lot simpler, based perhaps on SwiftUI.

Terminal Wish list

  • Depending on what our driver supports, we should surface that as the capabilities supported to the subprocess. For example, if running with only 16 colors, do not set xterm-color256, currently, hardwired in the demo.
  • Perhaps show a status bar "Use Control-Q to quote commands to the container".
  • Add mouse event support

CanFocus design upgrade

Need to have two separate properties for canFocus - the one that the view itself sets, and the one that is set when children are added to it (and consumed by the navigation system(

Mouse events

Need better support to relinquish the mouse with old ncurses.

View.positionCursor

Like the painter, this needs to be virtualized, so that ScrollView children can use this.

New Control: ComboBox

I have a partial implementation and idea, so I do not want a straight port of the C# codebase.

HotKey handling

Drop the manual HotKey support in the views, and instead use the HotKeySupport protocol, and wire up in Application.cs the probe for hotkeys, and invoking that method directly, rather than having each view implement their own hotkey support.

Swift Package Manager doesn't see m_cchar_t

I'm trying to use this toolkit in my Swift Package Manager project; however, I can't figure out a way to import Curses-Bridging-Header.h and therefore m_cchar_t remains undefined, which makes this line error:

typealias add_wch_def = @convention(c) (UnsafeMutablePointer<m_cchar_t>) -> CInt

with

(!) '(<<error type>>) -> CInt' (aka '(<<error type>>) -> Int32') is not representable in Objective-C, so it cannot be used with '@convention(c)'
(!) Use of undeclared type 'm_cchar_t'

Is there a way for me to somehow define m_cchar_t such that add_wch_def functions correctly?

Xcode woes

I tried updating this stuff after almost a year dormant, and for some reason my "schemes" were gone.

Anyways, I tried creating the schemes again, and even tried a fresh clean project, and tried the "Run in Terminal" option in Xcode:

image

But when I try to launch the application, it calls into Terminal, but does not seem to pass all the arguments necessary to start the program:

Last login: Tue Dec 22 21:39:16 on ttys006
/bin/bash -c 'arch -arch x86_64 '/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/darwin-debug' --unix-socket=/tmp/NCUei7 --arch=x86_64 --working-dir '/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug' --disable-aslr --env='__XCODE_BUILT_PRODUCTS_DIR_PATHS=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug' --env='MallocNanoZone=0' --env='CA_DEBUG_TRANSACTIONS=0' --env='COMMAND_MODE=unix2003' --env='__CFBundleIdentifier=com.apple.dt.Xcode' --env='NSUnbufferedIO=YES' --env='CA_ASSERT_MAIN_THREAD_TRANSACTIONS=0' --env='PWD=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug' --env='DYLD_LIBRARY_PATH=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug:/usr/lib/system/introspection' --env='LD_LIBRARY_PATH=/Applications/Xcode.app/Contents/Developer/../SharedFrameworks/' --env='__XPC_DYLD_LIBRARY_PATH=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicin
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
macpro$ /bin/bash -c 'arch -arch x86_64 '/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/darwin-debug' --unix-socket=/tmp/NCUei7 --arch=x86_64 --working-dir '/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug' --disable-aslr --env='__XCODE_BUILT_PRODUCTS_DIR_PATHS=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug' --env='MallocNanoZone=0' --env='CA_DEBUG_TRANSACTIONS=0' --env='COMMAND_MODE=unix2003' --env='__CFBundleIdentifier=com.apple.dt.Xcode' --env='NSUnbufferedIO=YES' --env='CA_ASSERT_MAIN_THREAD_TRANSACTIONS=0' --env='PWD=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug' --env='DYLD_LIBRARY_PATH=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicinex/Build/Products/Debug:/usr/lib/system/introspection' --env='LD_LIBRARY_PATH=/Applications/Xcode.app/Contents/Developer/../SharedFrameworks/' --env='__XPC_DYLD_LIBRARY_PATH=/Users/miguel/DerivedData/TermKit-bmrjuxzzsgqpzwgkdqvtvmicin
NAME
    darwin-debug -- posix spawn a process that is stopped at the entry point
                    for debugging.

SYNOPSIS
    darwin-debug --unix-socket=<SOCKET> [--arch=<ARCH>] [--working-dir=<PATH>] [--disable-aslr] [--no-env] [--setsid] [--help] -- <PROGRAM> [<PROGRAM-ARG> <PROGRAM-ARG> ....]

DESCRIPTION
    darwin-debug will exec itself into a child process <PROGRAM> that is
    halted for debugging. It does this by using posix_spawn() along with
    darwin specific posix_spawn flags that allows exec only (no fork), and
    stop at the program entry point. Any program arguments <PROGRAM-ARG> are
    passed on to the exec as the arguments for the new process. The current
    environment will be passed to the new process unless the "--no-env"
    option is used. A unix socket must be supplied using the
    --unix-socket=<SOCKET> option so the calling program can handshake with
    this process and get its process id.

EXAMPLE
   darwin-debug --arch=i386 -- /bin/ls -al /tmp

In fact, the actual invocation is not triggered, I manually have to press the return key and I get the above error.

I found about this option on this StackOverflow question:

https://stackoverflow.com/questions/21998706/terminal-window-inside-xcode

While researching why this did not work, I ran into this blog post from Erica Sadun that covers this technique here:

https://ericasadun.com/2020/07/15/executing-command-line-directly-from-xcode/

And she says in a section aptly named "Xcode’s Crazy Terminal Option"

This feature is buggy as hell, produces ridiculous amounts of excess text (see this), can take a significant time to launch, and even more time for Xcode to realize the process has finished. It is impossible to use with paths that use spaces (“warning: working directory doesn't exist: '/Volumes/Kiku/Xcode/Derived'“).

While I do not have spaces on my invocation, once this state is triggered, Xcode can not stop the process, and can not even quit. You must force quit Xcode.

It does not look like a happy path to get this thing going, one alternative is probably what I recall I did last year:

  • Configure somewhere "wait for launch"
  • Build
  • Run
  • Switch to console, manually invoke
  • Wait for Xcode to attach.

Which is a bit of an unpleasant experience for third parties attempting to look into this.

Terminal Capability

Currently the code is using a few too many unicode characters, we should have at least two layers of terminal configuration, if UTF8 is supported, we can use those unicode characters, otherwise, for older terminals, we should use just the drawing characters supported by curses.

Investigate redraw across TopLevel Boundaries + Redraw currently Redraws it all

I suspect that the expose system does not cross the Toplevel boundary, to determine affected regions.

So if a top-level damages the contents below it, we do not send the paint request (right now it might be, we might be overdrawing something, or there might have been a hack I added recently). But I suspect that the redraw logic does not cross toplevel boundaries right now.

Other issues include:

  • Menu from a lower TopLevel pops up on the bottom layer, and I think it should pop up on the top-level, to ensure that it is properly displayed, or to create a new transparent Toplevel where to insert itself, to render on top of everything else.

We need a proper system to queue "pendingOperations" and not "postProcessEvent", which is currently called on every input key, but also, from the Terminal emulator as a band-aid.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.