• Stars
    star
    102
  • Rank 335,584 (Top 7 %)
  • Language
    Swift
  • Created almost 8 years ago
  • Updated about 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A framework for writing terminal applications in Swift.

Ashen

A framework for writing terminal applications in Swift. Based on The Elm Architecture.

As a tutorial of Ashen, let's consider an application that fetches some todo items and renders them as a list.

Example

Fully working app โ€“ย TheSheet

Old way

In a traditional controller/view pattern, views are created during initialization, and updated later as needed with your application data. Loading data from a server to load a list of views. Views are stored in instance variables and edited "in place", and the views/subviews are added/removed as events happen, so a lot of code is there to manage view state.

New way

What would this look like using Ashen or Elm or React? In these frameworks, rendering output is declarative; it is based the model, and you render all the views and their properties based on that state. Model goes in, View comes out.

func render(model: Model) -> View<Message> {
    guard
        let data = model.data
    else {
        // no data?  Show the spinner.
        return Spinner()
    }

    return Stack(.topToBottom, [
        Text("List of things"),
        ListView(dataList: data) { row in
            LabelView(text: row.title)
        }
        // ๐Ÿ‘† this view is similar to how UITableView renders cells - only
        // the rows that are visible will be rendered. rowHeight can also be
        // assigned a function, btw, to support dynamic heights.
        //
        // Also, this view is not done yet! Sorry - but it'll look something
        // like this.
    ])
}

So instead of mutating the isHidden property of views, or addSubview, we just render the views we need based on our model. SwiftUI has also adopted this model, so if you've been using it, Ashen will feel very familiar.

Commands and Messages

To fetch our data, we need to call out to the runtime to ask it to perform a background task, aka a Command, and then report the results back as a Message. Message is how your Components can tell your application about changes that might result in a change to your model. For instance, if someone types in a "name" text field you probably want to know about that so you can update the model's name property.

Sources of Messages include Views, Commands, and system event components (e.g. a KeyEvent message can be captured via the OnKeyPress component, which receives system-level events and maps those into an instance of your app's Message type).

Our application starts at the initial() method. We return our initial model and a command to run. We will return an Http command:

enum Message {
    case received(Result<(Int, Headers, Data), HttpError>)
}

func initial() -> Initial<Model, Message> {
    let url = URL(string: "http://example.com")!
    let cmd = Http.get(url) { result in
      Message.received(result)
    }

    return Initial(Model(), cmd)
}

When the Http request succeeds (or fails) the result will be turned into an instance of your application's Message type (usually an enum), and passed to the update() function that you provide.

To send multiple commands, group them with Command.list([cmd1, cmd2, ...])

Updating

In your application's update() function, you will instruct the runtime how the message affects your state. Your options are:

  • .noChange โ€” ignore the message
  • .update(model, command) โ€” return a model and a list of Commands to run
  • .quit โ€” graceful exit (usually means exit with status 0)
  • .quitAnd({ ... })โ€” graceful exit with a closure that runs just before the runtime is done cleaning up. You can also throw an error in that closure.

For convenience there are two helper "types":

  • .model(model) โ€” return just updated model, no commands (shortcut for .update(model, Command.none()))
  • .error(error) โ€” quit and raise an error.

Program

Here's a skeleton program template:

// This is usually an enum, but it can be any type.  Your app will respond
// to state changes by accepting a `Message` and returning a modified
// `Model`.
enum Message {
    case quit
}

// The entired state of you program will be stored here, so a struct is the
// most common type.
struct Model {
}

// Return your initial model and commands. if your app requires
// initialization from an API (i.eg. a loading spinner), use a
// `loading/loaded/error` enum to represent the initial state.  If you
// persist your application to the database you could load that here, either
// synchronously or via a `Command`.
func initial() -> Initial<Model, Message> {
    Initial(Model())
}

// Ashen will call this method with the current model, and a message that
// you use to update your model.  This will result in a screen refresh, but
// it also means that your program is very easy to test; pass a model to
// this method along with the message you want to test, and check the values
// of the model.
//
// The return value also includes a list of "commands".  Commands are
// another form of event emitters, like Components, but they talk with
// external services, either asynchronously or synchronously.
func update(model: Model, message: Message)
    -> State<Model, Message>
{
    switch message {
    case .quit:
        return .quit
    }
}

// Finally the render() method is given your model and you return
// an array of views. Why an array? I optimized for the common case: some key
// handlers, maybe some mouse events, and a "main" view.
func render(model: Model) -> [View<Message>] {
    [
        OnKeyPress(.enter, { Message.quit }),
        Frame(Spinner(), .alignment(.middleCenter)),
    ])
}

Running your Program

To run your program, pass your initial, update, view, and unmount functions to Ashen.Program and run it with ashen(program). It will return .quit or .error, depending on how the program exited.

do {
    try ashen(Program(initial, update, view))
    exit(EX_OK)
} catch {
    exit(EX_IOERR)
}

Important note: ALL Ashen programs can be aborted using ctrl+c. It is recommended that you support ctrl+\ to gracefully exit your program.

Views

  • Text() - display text or attributed text.
    Text("Some plain text")
    Text("Some underlined text".underlined())
  • Input() - editable text, make sure to pass .isResponder(true) to the active Input.
    enum Message {
        case textChanged(String)
    }
    Input("Editable text", Message.textChanged, .isResponder(true))
  • Box() - surround a view with a customizable border.
    Box(view)
    Box(view, .border(.double))
    Box(view, .border(.double), .title("Welcome".bold()))
  • Flow() - arrange views using a flexbox like layout.
    Flow(.leftToRight, [  // alias: .ltr
        (.fixed, Text(" ")),
        (.flex1, Text(Hi!).underlined()), // this view will stretch to fill the available space
        // .flex1 is a handy alias for .flex(1) - just like CSS flex: 1, you can use different flex
        // values to give more or less % of the available space to the subviews
        (.fixed, Text(" ")),
    ])
    Flow(.bottomToTop, views)
  • Columns() - arrange views horizontally, equally sized and taking up all space.
    Columns(views)
  • Rows() - arrange views vertically, equally sized and taking up all space.
    Rows(views)
  • Stack() - arrange views according to their preferred (usually smallest) size.
    Stack(.ltr, views)
  • Frame() - place a view inside a container that fills the available space, and supports alignment.
    Frame(Text("Hi!"), .alignment(.middleCenter))
  • Spinner() - show a simple 1x1 spinner animation
    Spinner()
  • Scroll(view, .offset(pt|x:|y:)) - Make a view scrollable. By default the scroll view does not respond to key or mouse events. To make the view scrollable via mouse, try this:
    enum Message {
        case scroll(Int)  // update model.scrollY by this value
    }
    OnMouseWheel(
        Scroll(Stack(.down, [...]), .offset(model.scrollY)),
        Message.scroll
    )
    Also consider adding a "listener" for the onResizeContent: message, which will pass a LocalViewport (which has the size of the entire scrollable area and the visible: Rect)
  • Repeating(view) - Useful for background drawing. By itself it has preferredSize: .zero, but will draw the passed view to fill the available area.
    // draw the text "Hi!" centered, then fill the rest of the background with red.
    ZStack([Frame(Text("Hi!".background(.red)), .alignment(.middleCenter)), Repeating(Text(" ".background(.red)))])

View Modifiers

Views can be created in a fluent syntax (these will feel much like SwiftUI, though not nearly that level of complexity & sophistication).

  • .size(preferredSize), .minSize(preferredSize), .maxSize(preferredSize) - ensures the view is at least, exactly, or at most preferredSize. See also .width(w), .minWidth(w), .maxWidth(w), .height(h), .minHeight(h), .maxHeight(h) to control only the width or height.
    Text("Hi!").width(5)
    Stack(.ltr, [...]).maxSize(Size(width: 20, height: 5))
  • .matchContainer(), .matchContainer(dimension: .width|.height) - Ignores the view's preferred size in favor of the size provided by the containing view.
  • .matchSize(ofView: view), .matchSize(ofView: view, dimension: .width|.height) - Ignores the view's preferred size in favor of another view (usually a sibling view, in a ZStack).
  • .fitInContainer(.width|.height) - Make sure the width or height is equal to or less than the containing view's width or height.
  • .compact() - Usually the containing view's size is passed to the view's render function, even if it's much more than the preferred size. This method renders the view using the preferredSize instead.
  • .padding(left:,top:,right:,bottom:) or .padding(Insets) - Increases the preferred size to accommodate padding, and renders the view inside the padded area. If you are interested in peaking into some simple rendering/masking code, this is a good place to start.
  • .styled(Attr) - After drawing the view, the rendered area is modified to include the Attr. See also: underlined(), bottomLined(), reversed(), bold(), foreground(color:), background(color:), reset()
    Text("Hi!".underlined()).background(color: .red)
    Stack(.ltr, [...]).reversed()
  • .border(BoxBorder) - Surrounds the view in a border.
    Text("Hi!").border(.single, .title("Message"))
  • .aligned(Alignment) - This is useful when you know a view will be rendered in an area much larger than the view's preferredSize. The Alignment options are topLeft, topCenter, topRight, middleLeft, middleCenter, middleRight, bottomLeft, bottomCenter, bottomRight.
    Text("Hi!").aligned(.middleCenter)
    See also .centered(), which is shorthand for .aligned(.topCenter), useful for centering text or a group of views.
  • .scrollable(offset: Point) - Wraps the view in Scroll(view, .offset(offset))

Events

  • OnKeyPress
  • OnTick
  • OnResize
  • OnNext
  • OnClick
  • OnMouseWheel
  • OnMouse
  • IgnoreMouse

More Repositories

1

teacup

This project has been sunset in favor of MotionKit
Ruby
604
star
2

motion-xray

An iOS Inspector that runs inside your app, so you can debug and analyze from your device in real-world situations.
Ruby
555
star
3

SublimeFileDiffs

Show diffs between the current file, or selection(s) in the current file, and clipboard, another file, or unsaved changes.
Python
511
star
4

SublimeStringEncode

Converts characters from one "encoding" to another using a transformation (think HTML entities, not character encodings)
Python
149
star
5

ApacheConf.tmLanguage

Adapted from text mate bundle by GreyWyvern
141
star
6

SublimeChangeQuotes

Converts single and double quotes, and re-escapes quotes within the string
Python
123
star
7

zenburn

SublimeText, iterm, xcode, css, and textmate themes modeled after the vim zenburn theme.
SCSS
109
star
8

sweettea

Joins sugarcube and teacup, for great good. Adds handlers, more shortcuts, and some silly classes, to make RubyMotion a total gas.
Ruby
80
star
9

SublimeMarkAndMove

Allows for keyboard-only multiple selections. Select some stuff, mark it, then move the cursor around and add more marks, recall marks, or move between marks.
Python
74
star
10

motion-wiretap

A wrapper for KVO, gestures, UIControl events, and procs. Inspired by ReactiveCocoa.
Ruby
61
star
11

decent-swift-syntax

Uses the new `sublime-syntax` format, which is super easy to work with.
Swift
61
star
12

Mechy

C++
54
star
13

StrangeCase

It's yet another static site generator. Have you seen jekyll? hyde? Yup. Like those.
Python
49
star
14

SublimeCalculate

Select a formula and evaluate it using python.
Python
36
star
15

SublimeMoveText

moves text, similar to TextMate's โŒ˜โŒƒโ†,โ†’,โ†‘,โ†“
Python
34
star
16

SublimeBracketeer

I don't like how Sublime Text indents when I surround existing code in "{}"s. This plugin surrounds and indents sensibly.
Python
29
star
17

AVClub

Ruby
25
star
18

SublimeClipboardManager

A version of the Sublime Text 2 plugin at <http://www.sublimetext.com/forum/viewtopic.php?f=5&t=2260&start=0> that makes for TextMate-like clipboard history.
Python
23
star
19

SublimeScreencastDirector

Python
23
star
20

graymatter

This is a grab-bag collection of UIView subclasses, UIViewController modules, and random tools.
Ruby
21
star
21

dbt

A tool to detect and declare dependencies, and help with debugging.
Ruby
19
star
22

htmlkup

Converts html to coffeekup
JavaScript
14
star
23

Scorerrest

Swift
13
star
24

typewriter

UIView that makes grids and left-to-right layouts easy in RubyMotion.
Ruby
12
star
25

thoughts

The source code companion to my RubyMotion thoughts
Ruby
10
star
26

chomsky

Python
9
star
27

derailed

Ruby
8
star
28

SublimeQuickFind

Does a forward or reverse literal search. Makes it easy to jump to some text.
Python
8
star
29

SublimeSimpleMovements

Adds caret moving and newline entering commands.
Python
8
star
30

einstein-enum

Enums with associated values and polymorphic behavior, inspired by Swift.
Ruby
8
star
31

f

a function tracking system
Shell
7
star
32

TheSheet

Swift
7
star
33

SublimeTextFormatting

Python
6
star
34

SublimeGroupy

Stores and opens named file groups
Python
5
star
35

android-xml

Ruby
5
star
36

Mainframe

A very odd calculator app.
Swift
4
star
37

SublimeTransposeCharacter

transposes characters or lines.
Python
4
star
38

p

Command line password manager written in Python using SQLite
Python
4
star
39

plywood

A template language grammar that looks, feels, and works like Python
Python
4
star
40

SublimeDuplicateSelections

If you have an equal number of empty selections (cursors) and selections, this command will place each selection at the corresponding cursor location.
Python
4
star
41

nsnulldammit

RubyMotion automatically converts NSNull.null instances to nil making it... difficult to store an NSNull instance. This class and category on NSObject might help!
Objective-C
3
star
42

SublimeSetSyntax

Some generic keyboard bindings for 'Set Syntax:'
Python
3
star
43

FreeMem

Wanna know how much memory you've got left? Well OK then.
Objective-C
3
star
44

rabblescay

A word finder.
JavaScript
3
star
45

SublimeTodoodles

Python
2
star
46

tempest

Tempest-inspired shooter for Arduboy
C++
2
star
47

ansible

Ruby
2
star
48

sublime_packages

Sublime packages by colinta
Shell
2
star
49

SkiaPlayground

An easy way to get started with Skia
Java
1
star
50

goodbye-xcode

1
star
51

SublimeTextDebugging

Python
1
star
52

UpNext

Swift
1
star
53

pwman

Arduino
1
star
54

_SublimePackageTemplate_

template to start new SublimePackage projects
Python
1
star
55

ssed

**fed** up with trying to remember sed's weird regex support? Me, too.
JavaScript
1
star
56

punt

Execute shell command when file(s) change
Python
1
star
57

SublimeTextAndroid

Python
1
star
58

ExpoSkiaSandbox

TypeScript
1
star
59

Suss

Swift
1
star