{"id":13465437,"url":"https://github.com/colinta/Ashen","last_synced_at":"2025-03-25T16:31:40.295Z","repository":{"id":141427980,"uuid":"83887632","full_name":"colinta/Ashen","owner":"colinta","description":"A framework for writing terminal applications in Swift.","archived":false,"fork":false,"pushed_at":"2022-10-07T13:34:00.000Z","size":402,"stargazers_count":103,"open_issues_count":2,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-09T17:26:00.658Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/colinta.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-03-04T11:10:20.000Z","updated_at":"2025-01-15T22:33:52.000Z","dependencies_parsed_at":"2024-01-02T23:55:04.776Z","dependency_job_id":null,"html_url":"https://github.com/colinta/Ashen","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinta%2FAshen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinta%2FAshen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinta%2FAshen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colinta%2FAshen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/colinta","download_url":"https://codeload.github.com/colinta/Ashen/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245500241,"owners_count":20625531,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T15:00:29.863Z","updated_at":"2025-03-25T16:31:40.004Z","avatar_url":"https://github.com/colinta.png","language":"Swift","funding_links":[],"categories":["Libs","Command Line","CLI and Server","Recently Updated","Table of Contents"],"sub_categories":["Command Line","Linter","[Feb 05, 2025](/content/2025/02/05/README.md)"],"readme":"# Ashen\n\nA framework for writing terminal applications in Swift. Based on [The Elm Architecture][elm].\n\nAs a tutorial of Ashen, let's consider an application that fetches some todo\nitems and renders them as a list.\n\n### Example\n\n[Fully working app – TheSheet](https://github.com/colinta/TheSheet)\n\n###### Old way\n\nIn a traditional controller/view pattern, views are created during\ninitialization, and updated later as needed with your application data. Loading\ndata from a server to load a list of views. Views are stored in instance\nvariables and edited \"in place\", and the views/subviews are added/removed as\nevents happen, so a lot of code is there to manage view state.\n\n###### New way\n\nWhat would this look like using Ashen or Elm or React? In these frameworks,\nrendering output is declarative; it is based the model, and you render _all_ the\nviews and their properties based on that state. Model goes in, View comes out.\n\n```swift\nfunc render(model: Model) -\u003e View\u003cMessage\u003e {\n    guard\n        let data = model.data\n    else {\n        // no data?  Show the spinner.\n        return Spinner()\n    }\n\n    return Stack(.topToBottom, [\n        Text(\"List of things\"),\n        ListView(dataList: data) { row in\n            LabelView(text: row.title)\n        }\n        // 👆 this view is similar to how UITableView renders cells - only\n        // the rows that are visible will be rendered. rowHeight can also be\n        // assigned a function, btw, to support dynamic heights.\n        //\n        // Also, this view is not done yet! Sorry - but it'll look something\n        // like this.\n    ])\n}\n```\n\nSo instead of mutating the `isHidden` property of views, or `addSubview`, we\njust render the views we need based on our model.  SwiftUI has also adopted this\nmodel, so if you've been using it, Ashen will feel very familiar.\n\n###### Commands and Messages\n\nTo fetch our data, we need to call out to the runtime to ask it to perform a\nbackground task, aka a `Command`, and then report the results back as a\n`Message`. `Message` is how your Components can tell your application about\nchanges that _might_ result in a change to your model. For instance, if someone\ntypes in a \"name\" text field you probably want to know about that so you can\nupdate the model's `name` property.\n\nSources of Messages include Views, Commands, and system event components\n(e.g. a `KeyEvent` message can be captured via the `OnKeyPress` component, which\nreceives system-level events and maps those into an instance of your app's\n`Message` type).\n\nOur application starts at the `initial()` method. We return our initial model\nand a command to run. We will return an `Http` command:\n\n```swift\nenum Message {\n    case received(Result\u003c(Int, Headers, Data), HttpError\u003e)\n}\n\nfunc initial() -\u003e Initial\u003cModel, Message\u003e {\n    let url = URL(string: \"http://example.com\")!\n    let cmd = Http.get(url) { result in\n      Message.received(result)\n    }\n\n    return Initial(Model(), cmd)\n}\n```\n\nWhen the Http request succeeds (or fails) the result will be turned into an\ninstance of your application's `Message` type (usually an enum), and passed to\nthe `update()` function that you provide.\n\nTo send multiple commands, group them with `Command.list([cmd1, cmd2, ...])`\n\n###### Updating\n\nIn your application's `update()` function, you will instruct the runtime how the\nmessage affects your state. Your options are:\n\n-   `.noChange` — ignore the message\n-   `.update(model, command)` — return a model and a list of Commands to run\n-   `.quit` — graceful exit (usually means exit with status 0)\n-   `.quitAnd({ ... })`— graceful exit with a closure that runs just before the runtime\n    is done cleaning up. You can also throw an error in that closure.\n\nFor convenience there are two helper \"types\":\n\n-   `.model(model)` — return just updated model, no commands (shortcut for `.update(model, Command.none())`)\n-   `.error(error)` — quit and raise an error.\n\n# Program\n\nHere's a skeleton program template:\n\n```swift\n// This is usually an enum, but it can be any type.  Your app will respond\n// to state changes by accepting a `Message` and returning a modified\n// `Model`.\nenum Message {\n    case quit\n}\n\n// The entired state of you program will be stored here, so a struct is the\n// most common type.\nstruct Model {\n}\n\n// Return your initial model and commands. if your app requires\n// initialization from an API (i.eg. a loading spinner), use a\n// `loading/loaded/error` enum to represent the initial state.  If you\n// persist your application to the database you could load that here, either\n// synchronously or via a `Command`.\nfunc initial() -\u003e Initial\u003cModel, Message\u003e {\n    Initial(Model())\n}\n\n// Ashen will call this method with the current model, and a message that\n// you use to update your model.  This will result in a screen refresh, but\n// it also means that your program is very easy to test; pass a model to\n// this method along with the message you want to test, and check the values\n// of the model.\n//\n// The return value also includes a list of \"commands\".  Commands are\n// another form of event emitters, like Components, but they talk with\n// external services, either asynchronously or synchronously.\nfunc update(model: Model, message: Message)\n    -\u003e State\u003cModel, Message\u003e\n{\n    switch message {\n    case .quit:\n        return .quit\n    }\n}\n\n// Finally the render() method is given your model and you return\n// an array of views. Why an array? I optimized for the common case: some key\n// handlers, maybe some mouse events, and a \"main\" view.\nfunc render(model: Model) -\u003e [View\u003cMessage\u003e] {\n    [\n        OnKeyPress(.enter, { Message.quit }),\n        Frame(Spinner(), .alignment(.middleCenter)),\n    ])\n}\n```\n\n## Running your Program\n\nTo run your program, pass your `initial`, `update`, `view`, and `unmount`\nfunctions to `Ashen.Program` and run it with `ashen(program)`. It will return\n`.quit` or `.error`, depending on how the program exited.\n\n```swift\ndo {\n    try ashen(Program(initial, update, view))\n    exit(EX_OK)\n} catch {\n    exit(EX_IOERR)\n}\n```\n\n*Important note*: ALL Ashen programs can be aborted using `ctrl+c`. It is\n_recommended_ that you support `ctrl+\\` to gracefully exit your program.\n\n# Views\n\n- `Text()` - display text or attributed text.\n    ```swift\n    Text(\"Some plain text\")\n    Text(\"Some underlined text\".underlined())\n    ```\n- `Input()` - editable text, make sure to pass `.isResponder(true)` to the active `Input`.\n    ```swift\n    enum Message {\n        case textChanged(String)\n    }\n    Input(\"Editable text\", Message.textChanged, .isResponder(true))\n    ```\n- `Box()` - surround a view with a customizable border.\n    ```swift\n    Box(view)\n    Box(view, .border(.double))\n    Box(view, .border(.double), .title(\"Welcome\".bold()))\n    ```\n- `Flow()` - arrange views using a flexbox *like* layout.\n    ```swift\n    Flow(.leftToRight, [  // alias: .ltr\n        (.fixed, Text(\" \")),\n        (.flex1, Text(Hi!).underlined()), // this view will stretch to fill the available space\n        // .flex1 is a handy alias for .flex(1) - just like CSS flex: 1, you can use different flex\n        // values to give more or less % of the available space to the subviews\n        (.fixed, Text(\" \")),\n    ])\n    Flow(.bottomToTop, views)\n    ```\n- `Columns()` - arrange views horizontally, equally sized and taking up all space.\n    ```swift\n    Columns(views)\n    ```\n- `Rows()` - arrange views vertically, equally sized and taking up all space.\n    ```swift\n    Rows(views)\n    ```\n- `Stack()` - arrange views according to their preferred (usually smallest) size.\n    ```swift\n    Stack(.ltr, views)\n    ```\n- `Frame()` - place a view inside a container that fills the available space, and supports alignment.\n    ```swift\n    Frame(Text(\"Hi!\"), .alignment(.middleCenter))\n    ```\n- `Spinner()` - show a simple 1x1 spinner animation\n    ```swift\n    Spinner()\n    ```\n- `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:\n    ```swift\n    enum Message {\n        case scroll(Int)  // update model.scrollY by this value\n    }\n    OnMouseWheel(\n        Scroll(Stack(.down, [...]), .offset(model.scrollY)),\n        Message.scroll\n    )\n    ```\n    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`)\n- `Repeating(view)` - Useful for background drawing. By itself it has `preferredSize: .zero`, but will draw the passed `view` to fill the available area.\n    ```swift\n    // draw the text \"Hi!\" centered, then fill the rest of the background with red.\n    ZStack([Frame(Text(\"Hi!\".background(.red)), .alignment(.middleCenter)), Repeating(Text(\" \".background(.red)))])\n    ```\n\n## View Modifiers\n\nViews can be created in a fluent syntax (these will feel much like SwiftUI, though not nearly that level of complexity \u0026 sophistication).\n\n- `.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.\n    ```swift\n    Text(\"Hi!\").width(5)\n    Stack(.ltr, [...]).maxSize(Size(width: 20, height: 5))\n    ```\n- `.matchContainer(), .matchContainer(dimension: .width|.height)` - Ignores the view's preferred size in favor of the size provided by the containing view.\n- `.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).\n- `.fitInContainer(.width|.height)` - Make sure the width or height is equal to or less than the containing view's width or height.\n- `.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.\n- `.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.\n- `.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()`\n    ```swift\n    Text(\"Hi!\".underlined()).background(color: .red)\n    Stack(.ltr, [...]).reversed()\n    ```\n- `.border(BoxBorder)` - Surrounds the view in a border.\n    ```swift\n    Text(\"Hi!\").border(.single, .title(\"Message\"))\n    ```\n- `.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`.\n    ```swift\n    Text(\"Hi!\").aligned(.middleCenter)\n    ```\n    See also `.centered()`, which is shorthand for `.aligned(.topCenter)`, useful for centering text or a group of views.\n- `.scrollable(offset: Point)` - Wraps the view in `Scroll(view, .offset(offset))`\n\n# Events\n\n- `OnKeyPress`\n- `OnTick`\n- `OnResize`\n- `OnNext`\n- `OnClick`\n- `OnMouseWheel`\n- `OnMouse`\n- `IgnoreMouse`\n\n[elm]: http://elm-lang.org\n[react]: https://facebook.github.io/react/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcolinta%2FAshen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcolinta%2FAshen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcolinta%2FAshen/lists"}