├── .gitignore ├── .gitmodules ├── IDEAS.org ├── LICENSE ├── README.md ├── TAGS.md ├── build.zig ├── design_docs ├── README.md ├── client_architecture.md ├── configuration.md ├── cursor.md ├── events.md ├── extensions.md ├── formats.org ├── highlighting.md ├── keybindings.md ├── search.md ├── server_architecture.md ├── sqlite.md ├── text_display.md ├── text_manipulation.md └── windowing.md ├── not_good_enough ├── build.zig ├── docs │ ├── README.md │ ├── ROADMAP.html │ ├── ROADMAP.org │ ├── assets │ │ ├── architecture.drawio │ │ ├── architecture.png │ │ └── styles.css │ ├── colorize.nim │ ├── docs.nimble │ ├── increase_assets_version.nim │ ├── index.html │ ├── index.js │ ├── index.nim │ ├── jsonrpc_schema.nim │ ├── nim.cfg │ ├── poormanmarkdown.nim │ └── utils.nim ├── kisarc.zzz └── src │ ├── buffer_api.zig │ ├── config.zig │ ├── highlight.zig │ ├── jsonrpc.zig │ ├── keys.zig │ ├── kisa.zig │ ├── main.zig │ ├── rpc.zig │ ├── state.zig │ ├── terminal_ui.zig │ ├── text_buffer_array.zig │ ├── transport.zig │ └── ui_api.zig ├── poc ├── ipc_dgram_socket.zig ├── ipc_pipes.zig ├── ipc_seqpacket_socket.zig ├── nonblocking_io.zig └── poll_sockets.zig ├── src ├── client.zig ├── db_setup.sql ├── kisa.zig └── terminal_ui.zig └── tests ├── keypress.zig ├── lines.nim └── longlines.txt /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | /zig-out/ 3 | 4 | /tests/manylines.txt 5 | 6 | /not_good_enough/docs/app.html 7 | /not_good_enough/docs/app.js 8 | 9 | /kisa.db 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/zig-sqlite"] 2 | path = deps/zig-sqlite 3 | url = https://github.com/greenfork/zig-sqlite.git 4 | -------------------------------------------------------------------------------- /IDEAS.org: -------------------------------------------------------------------------------- 1 | * Highlighting 2 | ** VSCode 3 | - https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide 4 | ** TextMate 5 | - https://macromates.com/manual/en/language_grammars 6 | - https://www.apeth.com/nonblog/stories/textmatebundle.html 7 | ** Themes 8 | - https://protesilaos.com/emacs/modus-themes - example 9 | - https://github.com/ogdenwebb/emacs-kaolin-themes - example 10 | - https://github.com/super3ggo/kuronami - example 11 | - https://lists.gnu.org/archive/html/emacs-devel/2022-03/msg00099.html - 12 | considerations regarding defining colors which are specific to different 13 | extensions 14 | ** Code structure 15 | - https://github.com/tonyaldon/posts - Have you ever wondered how org-mode 16 | toggles the visibility of headings? 17 | * Incremental parsing 18 | - https://www.cs.umd.edu/~hammer/adapton/adapton-pldi2014.pdf 19 | - https://tree-sitter.github.io/tree-sitter/ 20 | * Editor design 21 | - https://www.mattkeeter.com/projects/futureproof/ 22 | - https://github.com/jamii/focus 23 | * Diffing 24 | - https://github.com/Wilfred/difftastic - structural syntax-aware diffs 25 | - https://gitlab.com/ideasman42/emacs-diff-ansi - uses external tools and 26 | converts ANSI escape codes to text colors inside editor 27 | - https://github.com/tomhoule/zig-diff - library just for that 28 | * Editing, modes 29 | - https://dahu.github.io/vim_waz_ere/1_editor_fundamentals.html 30 | - https://thevaluable.dev/vim-expert/ - modal editing 31 | - https://github.com/countvajhula/rigpa - tower of modes 32 | - https://countvajhula.com/2021/09/25/symex-el-edit-lisp-code-in-a-vim-like-way/ - parenthesis 33 | - https://countvajhula.com/2021/09/25/the-animated-guide-to-symex/ - parenthesis 34 | - https://github.com/ashok-khanna/parevil - parenthesis 35 | - https://github.com/meow-edit/meow - modal editing 36 | - https://github.com/AmaiKinono/puni - soft deletion 37 | - https://andreyorst.gitlab.io/posts/2022-02-20-what-if-structural-editing-was-a-mistake/ - parenthesis 38 | * Indentation 39 | - https://github.com/sogaiu/jsf-indent 40 | - https://github.com/clojure-emacs/clojure-mode/blob/e1dc7caee76d117a366f8b8b1c2da7e6400636a8/clojure-mode.el#L777-L953 41 | - https://github.com/semenInRussia/simple-indentation.el - generic rules for indentation 42 | * Filter search act 43 | - https://karthinks.com/software/avy-can-do-anything/ 44 | - https://github.com/minad/vertico 45 | - https://github.com/oantolin/orderless 46 | * State 47 | - https://lists.gnu.org/archive/html/emacs-devel/2021-12/msg00463.html 48 | * Projects 49 | - https://gitlab.com/ideasman42/emacs-bookmark-in-project - we need project namespace for all the little things 50 | - https://github.com/otavioschwanck/harpoon.el - same, bookmarks which are project-scoped 51 | * GUI TUI interface 52 | - https://github.com/zenith391/zgt - GUI, early stage 53 | - https://github.com/xyaman/mibu - terminal manipulation, we don't need to implement it from scratch 54 | - https://github.com/ziglibs/ansi-term - lots of interesting stuff on colors and styles 55 | - https://github.com/janet-lang/janet/blob/master/src/mainclient/shell.c - key parsing example 56 | - https://github.com/mawww/kakoune/blob/master/src/terminal_ui.cc - key parsing example 57 | - https://github.com/neovim/libtermkey - key parsing example 58 | - https://sw.kovidgoyal.net/kitty/keyboard-protocol/ - key parsing design 59 | - https://git.sr.ht/~leon_plickat/zig-spoon - TUI in Zig 60 | * I18n 61 | - https://github.com/elias94/accent - accented characters 62 | * Searching 63 | - https://github.com/ziglibs/string-searching - different algorithms for searching in Zig 64 | * Git 65 | - https://github.com/Artawower/blamer.el - git blame on the same line 66 | - https://ianyepan.github.io/posts/emacs-git-gutter/ - git gutter 67 | * State-of-the-art 68 | - https://github.com/brotzeit/rustic - Rust integration with TRAMP considerations and more 69 | - https://gitlab.com/gopiandcode/gopcaml-mode - OCaml structural editing 70 | - https://github.com/caerwynj/acme-sac - ACME Plan9 editor 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) 2021-2022, Dmitry Matveyev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kisa 2 | 3 | Kisa is a hackable and batteries-included text editor of the new world. 4 | 5 | Home repository is on [sourcehut] but there's also a mirror on [GitHub]. 6 | 7 | [sourcehut]: https://git.sr.ht/~greenfork/kisa/ 8 | [GitHub]: https://github.com/greenfork/kisa 9 | 10 | Kisa is in its early stage and it is not usable at the moment. See [roadmap] 11 | for the current progress. 12 | 13 | [roadmap]: https://greenfork.github.io/kisa/ROADMAP.html 14 | 15 | There's a growing set of [design documents](design_docs/), beware most 16 | of it is not implemented. 17 | 18 | ## Purpose 19 | 20 | I, greenfork, the one who started this project, would like to have a 21 | supreme code editor. I want to edit code with pleasure, I want to know 22 | that whenever I feel something is not right - I have enough power to fix it, 23 | but with great power comes great responsibility. I shall wield this power 24 | with caution and I shall encourage my peers and empower them to follow 25 | my steps and eventually let them lead me instead of simply being led. 26 | 27 | ## Zen 28 | 29 | * Programmer must be able to perfect their tool. 30 | * Choice is burden. 31 | * Choice is freedom. 32 | 33 | ## Goals 34 | 35 | * Provide a powerful and flexible code editor - obvious but worth saying, 36 | we should not provide anything less than that. 37 | * Identify common workflows and set them in stone - text editing has become 38 | quite sophisticated in this day and age, we have already discovered a lot 39 | of editing capabilities. Now is the time to make them easy to use and fully 40 | integrated with the rest of the features of the editor, not rely on 41 | third-party plugins to emulate the necessary features. 42 | * Adhere to hybrid Unix/Apple philosophy - programs must be able to communicate 43 | with each other, the editor must make integrations with other tools possible, 44 | this is from Unix philosophy. At the same time the editor must be built from 45 | ground-up and have full control of all its core features to provide a 46 | single and uniform way of doing things, this is from Apple philosophy. 47 | * Make it infinitely extensible by design, no hard assumptions - the only types of 48 | unimplementable features are those which were not accounted for from the 49 | very beginning and got hardblocked by design decisions which are interleaved 50 | with the rest of the editor, so changing it is not feasible. The solution 51 | is simple - layers and layers of abstractions, assumptions are strictly 52 | kept to minimum by careful thinking about the public API design of each layer. 53 | * Make it hackable - I believe there are several key points to make an editor 54 | hackable: interesting design, clean code, extensive development documentation, 55 | friendly attitude to anyone trying. 56 | 57 | ## Communication 58 | 59 | * <~greenfork/kisa-announce@lists.sr.ht> - readonly mailing list for rare 60 | announcements regarding this project, [web archive][announce-list]. Subscribe 61 | to this list by sending any email to 62 | <~greenfork/kisa-announce+subscribe@lists.sr.ht>. 63 | * <~greenfork/kisa-devel@lists.sr.ht> - mailing list for discussions and 64 | sending patches, [web archive][devel-list] 65 | * - my personal email address 66 | * [Discord] - real-time chatting experience 67 | * [Twitch] - occasional streams including editor development 68 | * [YouTube] - recordings of past streams and other related videos 69 | 70 | Please be kind and understanding to everyone. 71 | 72 | Are you new to mailing lists? Please check out [this tutorial](https://man.sr.ht/lists.sr.ht/). 73 | There's also the [in-detail comparison video](https://youtu.be/XVe9SD3kSR0) of pull requests 74 | versus patches. 75 | 76 | [announce-list]: https://lists.sr.ht/~greenfork/kisa-announce 77 | [devel-list]: https://lists.sr.ht/~greenfork/kisa-devel 78 | [Discord]: https://discord.gg/p5892XNmAk 79 | [Twitch]: https://www.twitch.tv/greenfork_gf 80 | [YouTube]: https://www.youtube.com/channel/UCinLbIxD_iIrByWR9fvO2kQ/videos 81 | 82 | ## Contributing 83 | 84 | Ideas are very welcome. At this stage of the project the main task is to 85 | shape its design and provide proof-of-concept implementations of these ideas. 86 | Code contributions without previous discussions are unlikely to be accepted 87 | so please discuss the design first. Ideas should be in-line with the current 88 | goals and values of this editor. Many ideas will likely be rejected since not 89 | all goals and values are identified, but nevertheless they will help us to 90 | shape the editor. 91 | 92 | For structured discussions please use <~greenfork/kisa-devel@lists.sr.ht> mailing list. 93 | 94 | ## How to build 95 | 96 | Currently it is only relevant for the development, there's no usable 97 | text editor (just yet). 98 | 99 | Requirements: 100 | - Zig master, currently 101 | - git 102 | 103 | ``` 104 | $ git clone --recurse-submodules https://github.com/greenfork/kisa 105 | $ cd kisa 106 | $ zig build test 107 | $ zig build run 108 | ``` 109 | 110 | ## Is this a task for a mere mortal? 111 | 112 | Code editor is a big project. I have a habit of abandoning projects, I moderately 113 | lose interest to them. I am not religious but God give me strength. 114 | 115 | In the interview on [Zig Showtime] Andreas Kling, the author of [SerenityOS], 116 | talks about how important it is to lay just one brick at a time. Let's try that. 117 | 118 | [Zig Showtime]: https://www.youtube.com/watch?v=e_hCJI__q_4 119 | [SerenityOS]: https://github.com/SerenityOS/serenity 120 | -------------------------------------------------------------------------------- /TAGS.md: -------------------------------------------------------------------------------- 1 | - not-good-enough - old version, too generic, too similar to everything else, it 2 | will be submerged by the sheer amount of other similar ideas. Why even bother 3 | if this is so simple. Minimal? Not usable. Maximal? There's Vim and Emacs 4 | already. Revolutionary? Maybe this should be our choice, the revolution. The 5 | risks are high and it can fail. But this is the only way. We need a bit of 6 | luck, that's all. 7 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | var target = b.standardTargetOptions(.{}); 5 | const mode = b.standardReleaseOptions(); 6 | 7 | const exe = b.addExecutable("kisa", "src/client.zig"); 8 | exe.use_stage1 = true; 9 | exe.addPackagePath("kisa", "src/kisa.zig"); 10 | 11 | const sqlite = b.addStaticLibrary("sqlite", null); 12 | sqlite.addCSourceFile("deps/zig-sqlite/c/sqlite3.c", &[_][]const u8{"-std=c99"}); 13 | sqlite.linkLibC(); 14 | exe.linkLibrary(sqlite); 15 | exe.addPackagePath("sqlite", "deps/zig-sqlite/sqlite.zig"); 16 | exe.addIncludeDir("deps/zig-sqlite/c"); 17 | 18 | target.setGnuLibCVersion(2, 28, 0); 19 | exe.setTarget(target); 20 | exe.setBuildMode(mode); 21 | exe.install(); 22 | 23 | const run_cmd = exe.run(); 24 | run_cmd.step.dependOn(b.getInstallStep()); 25 | if (b.args) |args| { 26 | run_cmd.addArgs(args); 27 | } 28 | 29 | const run_step = b.step("run", "Run the app"); 30 | run_step.dependOn(&run_cmd.step); 31 | 32 | const test_all = b.step("test", "Run tests"); 33 | { 34 | const test_cases = b.addTest("src/client.zig"); 35 | test_cases.use_stage1 = true; 36 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 37 | test_cases.linkLibrary(sqlite); 38 | test_cases.addPackagePath("sqlite", "deps/zig-sqlite/sqlite.zig"); 39 | test_cases.addIncludeDir("deps/zig-sqlite/c"); 40 | test_cases.setTarget(target); 41 | test_cases.setBuildMode(mode); 42 | test_all.dependOn(&test_cases.step); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /design_docs/README.md: -------------------------------------------------------------------------------- 1 | Don't believe what you see. 2 | 3 | Ideas are just nothing without implementation. Implementation is the truth. 4 | -------------------------------------------------------------------------------- /design_docs/client_architecture.md: -------------------------------------------------------------------------------- 1 | # Client design 2 | 3 | ## Server vs Client responsibility 4 | 5 | Since we have Client and Server who communicate with each other, we have a 6 | constant question: "Who is responsible for X?" This X includes but is not 7 | limited to: 8 | * Line numbering 9 | * Line wrapping 10 | * Git blame output 11 | * LSP/linter output 12 | * Indentation guides 13 | 14 | Each case has its own pros and cons. 15 | 16 | ### Line numbering 17 | 18 | Figures (usually at the left) of the main window with text which indicate the 19 | number of the corresponding line. 20 | 21 | On the Server advantages: 22 | * Client doesn't need to handle different options and settings. For example, 23 | there are several ways to display line numbers: absolute and relative values. 24 | This setting won't have to be implemented on each client, the server has to 25 | only implement it once and for all. 26 | 27 | On the Client advantages: 28 | * Different clients might have different ideas about how to display the data. 29 | Making implementation client-specific adds more configurability and more 30 | choices to the implementators of clients which might make it more appealing to 31 | develop a third-party frontend. 32 | 33 | Conclusion: on the **Server**. Even though Clients have more freedom otherwise, 34 | it is not the most particularly interesting thing to implement in my opinion. We 35 | would win more from uniform data, Clients can handle different display options 36 | still. Later we can optionally provide the data in 2 separate streams: line 37 | numbers and text buffer, - so that clients have full control over it, but for 38 | the start we should minimize our efforts. 39 | 40 | ### Line wrapping 41 | 42 | When text in the main window is too long and we decide to wrap the text line to 43 | the next display line so that we don't need to scroll horizontally. 44 | 45 | On the Server advantages: 46 | * With soft wrapping of lines moving the cursor up and down is implemented on 47 | the server, it is easier for Clients. This is a hard problem to solve since 48 | proper calculation may include calculating grapheme clusters which is not 49 | trivial at all. 50 | 51 | On the Client advantages: 52 | * Data to draw is more uniform, there's no special case for wrapped lines. 53 | 54 | Conclusion: on the **Server**. Calculating grapheme clusters is really not the 55 | domain of a Client, it is horrible of the Server to ask the Client to implement 56 | it. Server must send the data where the soft-wrapping occurs. 57 | 58 | ### Git blame output 59 | 60 | `git blame` is a Git version control command which displays the meta-data of 61 | commits corresponding to each line in the file. 62 | 63 | Conclusion: calculate on the **Server**, display control is fully on the 64 | **Client**. We will have to communicate with the `git` command-line program and 65 | this is better done on the Server. But the Server will send the data separately 66 | from the draw data, so the Client will be able to display it however it 67 | pleases. There's a problem that, for example, line wrapping interferes with this 68 | functionality if the Client decides to display blame data side-by-side with the 69 | code. How to solve it? Client must send to the Server updated main window width 70 | after it will display the blame data, so the Server will do the wrapping with an 71 | updated window width. 72 | 73 | ### LSP/linter output 74 | 75 | Language server protocol and different linters provide contextual information 76 | which is usually attached to the lines of code and displayed on the left or as 77 | underline of the relevant piece of code. 78 | 79 | Conclusion: calculate on the **Server**, display control is fully on the 80 | **Client**. Reasoning is same as for **Git blame output** since the Server does 81 | communication with the LSP/linter and Client only sends the correct dimensions 82 | to the Server. 83 | 84 | ### Indentation guides 85 | 86 | Indentation guides are lines or colored blocks inside the main window which 87 | indicate the current indentation level of code. This is especially useful for 88 | languages with significant indentation such as Python, Nim, but it is also 89 | useful for any language because they all keep correct indentation as a good 90 | syntax rule. 91 | 92 | Conclusion: on the **Client**. Since the only thing we have to do is to replace 93 | spaces with colored blocks or some Unicode characters which are going to 94 | indicate the indentation level, there's not much work to do. The Server doesn't 95 | need to know about it at all. 96 | 97 | ## Communicating changes to the server 98 | 99 | Client will send each key press to the server and wait for response back. 100 | This might seem slow and inefficient but there's no way to implement some 101 | features without it like autocompletion. Nowadays computers should be 102 | fast enough to handle it. It is also the way how remote terminals work 103 | in some implementations. 104 | 105 | This can make it unusable for network editing when the server is somewhere 106 | on the internet. And it can also make a difference for low-end hardware, 107 | where the problem could be a visible lag between key presses. 108 | 109 | But we have a solution. The solution is to provide a "threaded" mode which will 110 | only handle a single pair Client-Server without any way to attach additional 111 | Clients to the Server. In "threaded" mode Client-Server pair will be just 112 | different threads and not different processes. This will allow them to use some 113 | other form of IPC instead of TCP sockets. But this is still very far in the 114 | future. Are we even going to hit the barrier when the editor is going to be slow 115 | and communication is going to be the slowest part? I don't know. Let's try to 116 | move to this direction once it happens. But "threaded" mode is still better for 117 | development since it is easier to debug a single-process application. Would be 118 | even better to have a single-threaded application but the Server architecture 119 | does not really allow for it that easily. IPC - let's leave it for later, for 120 | now TCP sockets are our friends. 121 | 122 | Another solution is to go full async: process updates asynchrounously but it has 123 | a really big spike in complexity, mainly because we need Operational 124 | Transformation or CRDT or another smart word to resolve the difference between 125 | the changes the client has made so far with the data that the server knows 126 | about. 127 | 128 | It is wise to structure the client and the server in a way which will 129 | allow to eventually switch to this approach but right now it is definitely 130 | not the right time to implement it. 131 | 132 | See [xi editor revelations](https://github.com/xi-editor/xi-editor/issues/1187#issuecomment-491473599). 133 | 134 | ### Terminal display and input library 135 | 136 | Editors written in other than C languages such as Go ([micro], [qedit]) or Rust 137 | ([helix], [amp]) use their own library which implements terminal display 138 | routines. C/C++ based editors largely use [ncurses] library ([vis], [neovim]), 139 | but there's a good exception to this rule which is [kakoune]. Since current 140 | editor's language of choice is Zig, there are 2 choices: port ncurses library 141 | and write our own. I tried to [port the ncurses library] but eventually gave up 142 | because of infinite confusion with it. The code is also quite and quite hard to 143 | understand, there's an [attempt to make it better] but it is sadly not packaged 144 | at least on Arch Linux distribution which could be a problem. I decided that we 145 | should implement the library that is going to provide just the necessary for us 146 | features. But here we are not alone, a fellow _zigger_ presented the [mibu] 147 | library which implements low-level terminal routines, just what we need. In any 148 | case, we can always fork and extend it. 149 | 150 | Terminal input story is similar, other than C languages implement their own 151 | libraries which seems necessary for them anyway. The C land has [libtermkey] 152 | which is contrary to ncurses has pretty good source code, it is used at least by 153 | [neovim] and [vis]. But the state of this library is a little bit questionable, 154 | end-of-life was declared for it at least since 2016 and the original author 155 | advertises their new [libtickit] library which tries to be an alternative 156 | library to ncurses but as I see it didn't get wide adoption. Libtermkey is alive 157 | as a [neovim fork] however so this could be a viable option nonetheless. But 158 | again, implementing this library seems rather straightforward as demonstrated by 159 | [kakoune] and there are some new ideas about the full and proper representation 160 | of keypresses, see [keyboard terminal extension] by the kitty terminal. I think 161 | it is okay to try to port the libtermkey library at first and see how it goes, 162 | but further down the road I think it is still valuable to have our own library, 163 | in Zig. 164 | 165 | We will do everything in Zig (eventually). Hooray. 166 | 167 | [ncurses]: https://en.wikipedia.org/wiki/Ncurses 168 | [libtermkey]: http://www.leonerd.org.uk/code/libtermkey/ 169 | [port the ncurses library]: https://github.com/greenfork/zig-ncurses 170 | [libtickit]: http://www.leonerd.org.uk/code/libtickit/ 171 | [neovim fork]: https://github.com/neovim/libtermkey 172 | [keyboard terminal extension]: https://sw.kovidgoyal.net/kitty/keyboard-protocol.html 173 | [attempt to make it better]: https://github.com/sabotage-linux/netbsd-curses 174 | [mibu]: https://github.com/xyaman/mibu 175 | 176 | [Kakoune]: https://github.com/mawww/kakoune 177 | [amp]: https://github.com/jmacdonald/amp 178 | [vis]: https://github.com/martanne/vis 179 | [micro]: https://github.com/zyedidia/micro 180 | [vy]: https://github.com/vyapp/vy 181 | [neovim]: https://github.com/neovim/neovim 182 | [helix]: https://github.com/helix-editor/helix 183 | [xi]: https://xi-editor.io/ 184 | [qedit]: https://github.com/fivemoreminix/qedit 185 | [kilo]: https://github.com/antirez/kilo 186 | [moe]: https://github.com/fox0430/moe 187 | [paravim]: https://github.com/paranim/paravim 188 | [focus]: https://github.com/jamii/focus 189 | [Emacs]: https://www.gnu.org/software/emacs/ 190 | [joe]: https://joe-editor.sourceforge.io/ 191 | [TextMate grammar]: https://macromates.com/manual/en/language_grammars 192 | 193 | 194 | ### Where is it going to run? 195 | 196 | Since the initial implementation is going to be a terminal-based client, we 197 | will strive for the highest common denominator of all terminals, some popular 198 | choices: 199 | 200 | * [Foot] 201 | * [Alacritty] 202 | * [Kitty] 203 | * [Urxvt] 204 | * [iTerm2] 205 | * [tmux] 206 | * [xterm]... oops sorry, [this is the one] 207 | * [cygwin] - this is a terminal for Windows, a hard battle for sure 208 | 209 | [Foot]: https://codeberg.org/dnkl/foot 210 | [Alacritty]: https://github.com/alacritty/alacritty 211 | [Kitty]: https://sw.kovidgoyal.net/kitty/ 212 | [Urxvt]: https://wiki.archlinux.org/title/Rxvt-unicode 213 | [iTerm2]: https://iterm2.com/ 214 | [tmux]: https://github.com/tmux/tmux 215 | [xterm]: https://github.com/xtermjs/xterm.js/ 216 | [this is the one]: https://invisible-island.net/xterm/ 217 | [cygwin]: https://www.cygwin.com/ 218 | -------------------------------------------------------------------------------- /design_docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration design 2 | 3 | There are several possible levels of varying configurability: 4 | 5 | 1. No configuration at all 6 | 2. Configuration of builtin options with true/false, numbers and strings 7 | 3. Provide "hooks" to execute scripts when certain events are fired 8 | 4. Expose editor API via an embedded language like Lua 9 | 10 | Let's forget about the first option, I don't want to be the only user of 11 | this editor even if I write it for myself. 12 | 13 | Second option looks like a good start. It is also possible to merge it 14 | with other options but I have a feeling that these should be separate things 15 | in separate places. 16 | 17 | Third option, the concept of a "hook" as a general idea of an executable piece 18 | of code which will be run after a certain event was fired like inserting a 19 | character or 20 | switching panes. This is a nice approach but the problem I see here is that 21 | we will need a language that is going to be executed in this "hook" and we 22 | can't really leave it as a choice to the user. Let's read my sad story 23 | about [Kakoune]: 24 | 25 | Kakoune is an example with minimal own language for configuration, main idea is 26 | to use integrations written in any language the user wants to, and the "Kakoune 27 | language" just enables easier interoperation. The result is 28 | that most of the scripts are written in Bash (: And the more complicated ones 29 | use a myriad of languages such as Guile, Perl, Python, Crystal, Rust. Although 30 | it is feasible to use them, the most common denominator is Bash and this is sad. 31 | 32 | Fourth option, the API. The holy grail of programming. I program my editor, I am 33 | in the command. But am I really? I will still be able to program things which 34 | the editor carefully exposed to me via its API. And once I want to do something 35 | more significant, I will have to do it in another language, Zig, with different 36 | set of abstractions and everything. The main idea of embedding a scripting 37 | language is that it is easy to hop in but it always fails whenever the user 38 | desires a more sophisticated ability to extend the code. At this point the 39 | complexity of an extension language can be comparable to the source language. 40 | 41 | At the same time the embedded language looks as the only viable option if we 42 | want others to be able to provide lightweight plugins. Including everything 43 | in the main editor code can quickly blow up the complexity and worsen 44 | maintainability by a large amount. And it is likely to happen since different 45 | people would want different things in their editor. 46 | 47 | Another use case for an embedded language is to program a part of the editor 48 | in this very language. It is a popular practice in gamedev world, main benefit 49 | is the ease of programming and dynamic environment (usually embedded languages 50 | are highly dynamic). And the main downside is the slowness of execution when 51 | the amount of this embedded language becomes critically large. Today there 52 | are languages and tools which try to maximize benefits and minimize 53 | disadvantages. 54 | 55 | See further design of the embedded language in 56 | [Extensions design](EXTENSIONS_DESIGN.md). 57 | 58 | ## File format 59 | 60 | Currently the format for file configuration is [zzz], it is a YAML-like 61 | syntax with a representation like a tree, allows duplicated keys and 62 | carries no distinction between the keys and values. It is flexible enough 63 | to meet all our needs for now but we may reconsider it in the future. 64 | 65 | You are advised to skim through [zzz] spec since there are some unusual 66 | syntactic structures which we will abuse. But also feel free to reference 67 | it later once you see any uncomprehensible black magic in the config example. 68 | 69 | [zzz]: https://github.com/gruebite/zzz 70 | 71 | Configuration files are searched and applied in the following order, 72 | later take precedence over former: 73 | 1. `etc/kisa/kisarc.zzz` 74 | 2. `$HOME/.kisarc.zzz` 75 | 3. `$XDG_CONFIG/kisa/kisarc.zzz` 76 | 4. `$(pwd)/kisarc.zzz` 77 | 78 | where in (3) `$XDG_CONFIG` is usually `$HOME/.config` and `$(pwd)` means 79 | current working directory. 80 | 81 | File is read from top to bottom, since settings can be duplicated, settings 82 | below in the file take precedence over settings above. Duplicated keys 83 | are resolved in the following fashion: 84 | * Top-level keys are always strictly defined, duplicated top-level keys 85 | have their values appended 86 | * All the other keys are replaced according to precedence rules 87 | 88 | A list of allowed top-level keys, all other keys are forbidden: 89 | * `keymap` 90 | * `scopes` 91 | * name of a scope 92 | * `settings` 93 | 94 | All the top-level keys are allowed either at the top level or right under a 95 | "name of a scope" key. Name of a scope can not be inside another name of 96 | a scope. 97 | 98 | Configuration can be separated into different categories and they all can 99 | be approached differently. 100 | 101 | ## Keymap, bindings 102 | 103 | We need to consider these points: 104 | 1. There are different modes (Normal, Insert, ...) 105 | 2. There are mini-modes, see KEYBINDINGS_DESIGN.md 106 | 3. There are global bindings and filetype-specific bindings 107 | 4. There are modifier keys, such as Ctrl, Shift, Alt 108 | 5. Keys must resolve to a defined set of commands 109 | 6. Some commands may take an argument 110 | 7. Some keys might be in a foreign language 111 | 8. Keys can execute multiple commands at a time 112 | 9. Multiple commands executed with a single key might need to have special 113 | handling in cases when we want to repeat or undo the command 114 | 10. Keys can have documentation for interactive help 115 | 116 | Point (3) will be addressed in Scopes section. All other points are 117 | demonstrated below using zzz file format: 118 | ``` 119 | keymap: # top-level key 120 | normal: # mode name 121 | default: nop # default if no key matches 122 | j: move_down # usual keybinding 123 | h: move_left; l: move_right # on the same line using `;` to move up 1 level 124 | л: move_up # Cyrillic letter 125 | b: minimode: buffers # start a mini-mode and give it a name 126 | doc: Buffer manipulations # documentation for the key `b` 127 | n: buffer_next # n will only be available in the mini-mode 128 | p: buffer_prev 129 | n: 130 | search_forward # command can be on a separate line 131 | select_word # multiple commands can be executed for 1 key 132 | doc: Search and select # documentation for the key `n` 133 | N: search_backward # optionally shift-n 134 | # command `open_buffer` takes an argument `*scratch*` 135 | S: open_buffer: *scratch* 136 | insert: 137 | default: insert_character 138 | ctrl-n: move_down # keybinding with a modifier key 139 | ctrl-p: move_up 140 | ``` 141 | 142 | TODO: describe the parsing algorithm 143 | 144 | ## Scopes 145 | 146 | There are some cases where we only want to bind the keys for a specific 147 | programming language file like Ruby or Rust. Scopes make this dream come 148 | true. Settings in the scope are kept in a separate from a general config 149 | bucket but otherwise they follow all the precedence rules described 150 | above. 151 | 152 | Scopes have 2 different syntaxes for 2 use cases: scope definition and 153 | scope usage. 154 | 155 | ### Scope definition 156 | 157 | Scopes are tried in order from top to bottom, first matched scope is 158 | applied, all later ones are not tried. Scope name can be any name 159 | except for a reserved set of top-level keys. 160 | 161 | ``` 162 | scopes: # top-level key 163 | ruby: # scope name 164 | filename: .*\.rb, Gemfile # matching on the name with regex, must match 165 | # the whole file name 166 | mimetype: text/x-ruby # matching with file metadata 167 | bash: 168 | filename: .*\.sh, .*\.bash 169 | # multiple matching on file contents, full string match 170 | contents: #!/usr/bin/bash, #!/usr/bin/sh, #!/usr/bin/env bash 171 | xz: 172 | contents: 7zXZ # matching on binary data (why on Earth) 173 | ``` 174 | 175 | ### Scope usage 176 | 177 | Scope names can only appear at the top level, they can be repeated multiple 178 | times throughout the config file. 179 | 180 | ``` 181 | ruby: 182 | keymap: # imagine it's the top level here 183 | normal: 184 | r: # start a mini-mode 185 | minimode: rubocop # give it a name 186 | l: shell_command: rubocop # execution of a shell command `rubocop` 187 | # documentation for the key `a` by using `;;` to jump 2 levels up 188 | a: shell_command: rubocop -a;; doc: Run rubocop safe autocorrect 189 | A: shell_command: rubocop -A;; doc: Run rubocop unsafe autocorrect 190 | settings: 191 | tab_width: 2 192 | ``` 193 | 194 | ## Settings 195 | 196 | Settings for things like indentation, line numbering, et cetera. Expressed 197 | as a simple key-value. 198 | 199 | TODO: document all settings 200 | 201 | Example: 202 | ``` 203 | settings: 204 | line_numbers: absolute # none, absolute, relative 205 | shell: /bin/sh # any valid path to executable 206 | insert_final_newline: true # true, false 207 | trim_spaces: on_save # on_save, on_switch_to_normal, false 208 | tab_width: 8 # positive number 209 | expand_tab: smart # true, false, smart 210 | ``` 211 | -------------------------------------------------------------------------------- /design_docs/cursor.md: -------------------------------------------------------------------------------- 1 | # Cursor design 2 | 3 | Cursor is always a part of a selection, where cursor is at the head of a 4 | selection and anchor is at the end of a selection. There's always at least 5 | a single selection present, called "primary" selection. Selection is 6 | a range of characters between the cursor and the anchor. In the simplest 7 | case cursor and anchor are at the same position, so the selection is only 8 | 1 character. 9 | 10 | Here `&` is an anchor, `|` is a cursor, `.` are any characters in between them: 11 | ``` 12 | &...| 13 | ^ anchor 14 | ^ cursor 15 | ^~~~~ selection of 5 characters 16 | ``` 17 | 18 | Initially cursor and anchor are at the same position and move together but the 19 | selection can be "anchored", then only cursor's position is updated. 20 | 21 | ## Movement 22 | 23 | Moving up and down one line should do what is says if there's a character 24 | displayed one line above or below. If there's no character right above/below 25 | the cursor, the cursor should move to the last character of the line. 26 | 27 | During the whole movement of up and down, the cursor should save the column 28 | at which it started moving, meaning that the column shouldn't be reset to 1 29 | if the there's an empty line above and cursor makes movement up, down. The 30 | column should only be reset when moving left, right and doing any other 31 | command that changes its position. 32 | 33 | Following considerations apply when the cursor is at the end of a line: 34 | - At the position of a newline "\n" character - cursor should move to the 35 | newline character above or below if the current line is longer. Option 36 | `track-eol` makes the cursor always follow the end of line, no matter 37 | the line length. 38 | - At the position of the last character in a line, right before the newline 39 | "\n" character - cursor should move to the last character of the line if 40 | the current line is longer. 41 | 42 | Moving up and down should move one line above and below, where line means 43 | an array of bytes ending with a newline "\n" character. But there should 44 | also be a special "display movement" which moves up/down one "displayed" 45 | line which takes effect when line wrapping is enabled. 46 | 47 | ### Examples 48 | 49 | In format: initial state, commands, final state, where `|` describes the 50 | character position. 51 | 52 | 1. 53 | ``` 54 | 1 first line 55 | 2 second long lin|e 56 | ``` 57 | `up` 58 | ``` 59 | 1 first lin|e 60 | 2 second long line 61 | ``` 62 | 63 | 2. 64 | ``` 65 | 1 first line 66 | 2 second long line| 67 | ``` 68 | `up` 69 | ``` 70 | 1 first line| 71 | 2 second long line 72 | ``` 73 | 74 | 3. 75 | ``` 76 | 1 lo|ng long long wrapped 77 | line 78 | 2 second line 79 | ``` 80 | `down` 81 | ``` 82 | 1 long long long wrapped 83 | line 84 | 2 se|cond line 85 | ``` 86 | 87 | 4. 88 | ``` 89 | 1 lo|ng long long wrapped 90 | line 91 | 2 second line 92 | ``` 93 | `display down` 94 | ``` 95 | 1 long long long wrapped 96 | li|ne 97 | 2 second line 98 | ``` 99 | 100 | ## Multiple cursors 101 | 102 | Multiple cursors - an interactive way of executing substitute/delete/add 103 | operations. 104 | 105 | One and only one cursor is always "primary" which means that it gets a 106 | special treatment when we add or delete new "secondary" cursors. 107 | 108 | There are 3 main ways of creating multiple cursors: 109 | 1. Create a cursor above current primary cursor - primary cursor moves 1 line up, 110 | secondary cursor is placed in its previous place. 111 | 2. Create a cursor below current primary cursor - primary cursor moves 1 line down, 112 | secondary cursor is placed in its previous place. 113 | 3. Create cursors which match the entered expression - in the selected area, or 114 | in the whole file (line?) if nothing is selected, run a regex expression and place 115 | 1 cursor at the beginning of each matched string. 116 | 117 | Cursor movement described above also applies to cursor creation (1) and (2). 118 | There's no way to create multiple cursors at `display down` location. When 119 | doing reverse operations, for example after (1) follows (2), the previous 120 | operation should be cancelled. In simpler terms it means that the primary 121 | cursor "eats" secondary cursors in this case. 122 | 123 | After executing (3) the primary cursor is placed at the very last match of a 124 | selection/file, all previous cursors are secondary. This may also include 125 | cases when secondary cursors are off the visible area of the screen. The 126 | screen display always follows the primary cursor. 127 | 128 | ### Examples 129 | 130 | In format: initial state, commands, final state, where `|` describes the 131 | primary cursor and `&` describes secondary cursors. 132 | 133 | 1. 134 | ``` 135 | 1 fir|st line 136 | 2 second long line 137 | 3 third line 138 | ``` 139 | `create-cursor-down` 140 | ``` 141 | 1 fir&st line 142 | 2 sec|ond long line 143 | 3 third line 144 | ``` 145 | `create-cursor-down` 146 | ``` 147 | 1 fir&st line 148 | 2 sec&ond long line 149 | 3 thi|rd line 150 | ``` 151 | `create-cursor-up` 152 | ``` 153 | 1 fir&st line 154 | 2 sec|ond long line 155 | 3 third line 156 | ``` 157 | 158 | 2. 159 | ``` 160 | 1 fir|st line 161 | 2 second long line 162 | 3 third line 163 | ``` 164 | `select l[io]n[eg]` 165 | ``` 166 | 1 first &line 167 | 2 second &long &line 168 | 3 third |line 169 | ``` 170 | -------------------------------------------------------------------------------- /design_docs/extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions design 2 | 3 | Configuration with a full API exposure can be considered as a plugin/script 4 | extension. My idea is that we should have these things separate for a 5 | number of reasons: 6 | 1. For large programs it could be very important to run asynchronously, so 7 | that a slow Ruby program would not freeze your editor while it tries to 8 | lint the code. 9 | 2. Large programs are more likely to invest into more sophisticated transport 10 | such as JSON-RPC via pipes or whatever we have. Thus we will not be subjected 11 | to implementing an "easy to use" interface via our config files. 12 | 13 | Asynchrony traditionally adds a lot of complexity to the application. We will 14 | explore whether we are able to solve this problem in our specific case and 15 | not in general by providing a limited set of API endpoints to plugins. Otherwise 16 | we might find ourselves solving the problem of simultaneous edits by 17 | multiple users. 18 | 19 | For additional notes see [xi article on plugins]. 20 | 21 | [xi article on plugins]: https://xi-editor.io/docs/plugin.html 22 | 23 | As a side note there are some languages I considered as an embedded 24 | extension language: 25 | * [Lua] 26 | * [Squirrel] 27 | * [Wren] 28 | * [PocketLang] 29 | * [Chibi Scheme] 30 | * [Guile] 31 | * [mruby] 32 | * [bog] 33 | * [Fennel] 34 | * [Janet] 35 | 36 | [Lua]: https://www.lua.org/ 37 | [Wren]: https://wren.io/ 38 | [PocketLang]: https://github.com/ThakeeNathees/pocketlang 39 | [Chibi Scheme]: https://github.com/ashinn/chibi-scheme 40 | [Guile]: https://www.gnu.org/software/guile/ 41 | [Squirrel]: http://squirrel-lang.org/ 42 | [mruby]: http://mruby.org/ 43 | [bog]: https://github.com/Vexu/bog 44 | [Fennel]: https://fennel-lang.org/ 45 | [Janet]: https://janet-lang.org/index.html 46 | -------------------------------------------------------------------------------- /design_docs/formats.org: -------------------------------------------------------------------------------- 1 | * Formats 2 | 3 | The editor is an event machine which produces events and consumes different 4 | inputs to produce a final output - what you see on the screen. Actions are just 5 | modifications of inputs from one stage to another. In this paradigm it is 6 | paramount to agree on the format of input, how different steps will process 7 | their inputs and produce output that validates as the input to the next step. 8 | 9 | ** Displaying text on the screen 10 | 11 | A data structure to display the text on the screen. Work in progress, we will 12 | start simple and progress from there. 13 | 14 | *** Just text 15 | 16 | Some form of structured text that is divided by lines. 17 | 18 | #+begin_src js 19 | { 20 | lines: [ 21 | { 22 | number: 1, 23 | contents: "Hello" 24 | }, 25 | { 26 | number: 2, 27 | contents: "world!" 28 | } 29 | ] 30 | } 31 | #+end_src 32 | 33 | *** Text with style 34 | 35 | This time text can have styles. 36 | 37 | #+begin_src js 38 | { 39 | lines: [ 40 | { 41 | number: 1, 42 | segments: [ 43 | { 44 | contents: "Hello", 45 | style: { 46 | foreground: "default", 47 | background: "default", 48 | font_style: 0, // int-encoded styles: BOLD | ITALIC 49 | } 50 | } 51 | ] 52 | }, 53 | { 54 | number: 2, 55 | segments: [ 56 | { 57 | contents: "w", 58 | style: { 59 | foreground: "red", 60 | background: "default", 61 | font_style: 0 62 | } 63 | }, 64 | { 65 | contents: "o", 66 | style: { 67 | foreground: "green", 68 | background: "default", 69 | font_style: 0 70 | } 71 | }, 72 | { 73 | contents: "rld!", 74 | style: { 75 | foreground: "blue", 76 | background: "default", 77 | font_style: 0 78 | } 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | #+end_src 85 | -------------------------------------------------------------------------------- /design_docs/highlighting.md: -------------------------------------------------------------------------------- 1 | # Highlighting design 2 | 3 | "Every modern and old code editor includes some kind of coloring for the text 4 | so the programmer is generally more perceptive of the code structure and 5 | it makes code comprehension better" - this claim is unsupported, and as I 6 | understand, completely unsupported. 7 | 8 | We should still provide a familiar user experience, even if the claim is 9 | unsupported, the most comfortable environment for learning is when little 10 | things change from the previous editor. And it looks nice. 11 | 12 | ## Color schemes 13 | 14 | Let's put it out of the way. Color scheme is just a bunch of variables and 15 | colors assigned to them. This editor will support every conceivable color 16 | scheme in this universe which has under 2^32 color variants (let's not 17 | have 2^32-1 colors though please). Good color schemes are preferred to 18 | bad ones, this editor can ship a number of popular choices and some 19 | opinionated choices. Choosing the "correct" coloring scheme is left to 20 | the user of this editor and the user will be provided good interface 21 | to tweak everything. 22 | 23 | ## Traditional coloring scheme 24 | 25 | As a default there will be a standard syntax highlighting which is implemented 26 | in most editors where we use fancy colors to highlight different tokens based 27 | on their syntactic meaning. 28 | 29 | ## Semantic and structural highlighting 30 | 31 | A reasonable claim about colors is that they attract our attention to them 32 | and they better convey a meaningful information. I don't necessarily think 33 | that syntax information is that important for us but there are reasons to 34 | think that semantic or structural information is more useful. 35 | 36 | * https://buttondown.email/hillelwayne/archive/46a17205-e38c-44f9-b048-793c180c6e84 37 | * https://www.crockford.com/contextcoloring.html 38 | 39 | "Semantic" means conveying the meaning, some knowledge about the code, for 40 | example knowing which variables are local and which are global is a semantic 41 | knowlege. Semantic highlighting is not necessarily related to "Semantic tokens" 42 | from the Language Server Protocol (LSP) but carries a similar idea. 43 | 44 | "Structural" means conveying the overall structure of a code, for example 45 | knowing where one function begins and another function ends. 46 | 47 | There could be different kinds of semantic highlighting, each emphasizing 48 | and communicating just a single thing or looking at the code from a single 49 | perspective. In order to combine and compose them, they will be applied in 50 | order from highest to lowest, we can call them "highlighting tower", for 51 | instance: 52 | 1. Rainbow delimiters 53 | 2. Imported symbols 54 | 3. Not matched delimiters 55 | 56 | In this case we will color things in this order: 57 | 1. Start with default foreground on background color, just 2 colors 58 | 2. Color delimiters like parenthesis according to their level of nesting 59 | 3. Color imported symbols from other files, no conflict with the previous one 60 | 4. Conflict not matching delimiters like parenthesis. Now let's imagine that 61 | we indeed have a not matching parenthesis and this highliter colors them 62 | bright red. This means that since this highlighter is below, it is applied 63 | later and we will re-color the paren from step 1 to the bright red color. 64 | 65 | Of course some coloring modes may conflict in a sense they they color only 66 | a part of the previously colored region or in some other bizarre ways. 67 | These conflicts are left for the user to decide and tweak. 68 | Though this editor will provide a convenient interface to manipualte 69 | different highlighting levels at run-time. 70 | 71 | A note should be taken that not all highlighting levels are general-purpose. 72 | Some of them will have to be implemented individually for each language, 73 | some will not make sense for other languages at all, some of them are too 74 | ambitious to implement efficiently while retaining the low latency of an 75 | editor. 76 | 77 | Below are some ideas which will be implemented as separate levels of the 78 | highlighting tower. Not everything is guaranteed to be implemented. 79 | 80 | ### Rainbow delimiters 81 | 82 | Have a set of 7 colors (in my world rainbow has 7 colors, duh) and apply it 83 | to each level of delimiters, cycling from the first color if there are more 84 | than 7 nesting levels. For example, this will have 4 nesting levels, each 85 | level is prefixed with a number, in Emacs Lisp: 86 | 87 | ``` 88 | 1(use-package elpher 89 | :hook 90 | 2(elpher-mode . 3(lambda 4() 4(text-scale-set 1)))) 91 | ``` 92 | 93 | Delimiters mean more than just brackets: also quotes for string literals, 94 | html tags, do..end syntax structures in some languages. Not every delimiter 95 | is reasonable to implement though. 96 | 97 | * Vim https://github.com/luochen1990/rainbow 98 | * Emacs https://www.emacswiki.org/emacs/RainbowDelimiters 99 | 100 | ### Rainbow indentation 101 | 102 | Similar idea, have a look above at how many colors a rainbow has, this time 103 | we color the indentation level each with new color. It can be useful for 104 | indentation-based syntax languages like Python and for others since not using 105 | indentation is socially inappropriate nowadays. Example, where `r` means 106 | colored with read and `b` means colored with blue, in Python: 107 | 108 | ``` 109 | def add(a, b): 110 | rrrrif (True): 111 | rrrrbbbbprint(2) 112 | ``` 113 | 114 | * VSCode https://github.com/oderwat/vscode-indent-rainbow 115 | 116 | ### Imported symbols 117 | 118 | **Language-specific** 119 | 120 | General use case is analyzing a particular file for dependencies. It would be 121 | good to see all the functions/variables/types used from different 122 | files/packages. For example, imagine that everything between asterisks `*` 123 | is colored green and between pipes `|` is colored yellow, in Nim: 124 | 125 | ``` 126 | import *strutils*, |times| 127 | 128 | if "mystring".*endsWith*("ing"): 129 | echo |now|() 130 | ``` 131 | 132 | ### Bound variables, scopes of variables 133 | 134 | **Language-specific** **Opinionated** 135 | 136 | We would like to highlight each symbol that is bound to global scope, 137 | to local scope or to function argument. We don't necessarily care about 138 | typos (which might be a convenient thing to have for fast typers, but 139 | ultimately it is well-catched by the compilers/interpreters), and more 140 | about conveying the information about which particular scopes are used 141 | at the current place of code. For example, imagine that everything between 142 | asterisks `*` is colored red (global variable), between pipes `|` is colored 143 | yellow (function or loop variable) and between underscores `_` is colored 144 | blue (local variable), in TypeScript: 145 | 146 | ``` 147 | const *listener* = Deno.listen({ port: 8000 }); 148 | for await (const |conn| of *listener*) { 149 | (async () => { 150 | const _requests_ = Deno.serveHttp(|conn|); 151 | for await (const { |respondWith| } of _requests_) { 152 | |respondWith|(new Response("Hello world")); 153 | } 154 | })(); 155 | } 156 | ``` 157 | 158 | 159 | Meaning will probably vary a lot between different languages and there could 160 | be opinionated implementations of this feature. Hopefully there's an option 161 | to implement it once but make it configurable enough. 162 | 163 | ### Error handling 164 | 165 | **Language-specific** 166 | 167 | There are a lot of different approaches to implement this. It is almost 168 | opinionated, but the approaches don't seem to conflict with each other 169 | so there's no serious conflict. Some choices: 170 | * Highlight all `raise` or `throw` or `try` keywords 171 | * Highlight all functions which `raise` but don't handle the raised error 172 | in any way 173 | 174 | Basically any way we can have a perspective at how "safe" the program is. 175 | For example, imagine that everything between `*` is green (good, `try` has 176 | a matching `errdefer`) and between `&` is red (bad `try` does not have a 177 | matching `errdefer`), in Zig: 178 | ``` 179 | // somewhere inside the function 180 | var text_buffer = *try* self.createTextBuffer(text_buffer_init_params); 181 | *errdefer* self.destroyTextBuffer(text_buffer.data.id); 182 | var display_window = &try& self.createDisplayWindow(); 183 | var window_pane = *try* self.createWindowPane(window_pane_init_params); 184 | *errdefer* self.destroyWindowPane(window_pane.data.id); 185 | // ... 186 | ``` 187 | 188 | ### Memory handling 189 | 190 | **Language-specific** **Opinionated** 191 | 192 | For low-level languages it is usually important to correctly do memory 193 | handling which can be allocate/free analysis (which is hard to do and 194 | probably is still a research project), deciding whether the memory is 195 | being allocated on the stack or on the heap. So there are some ideas: 196 | * Highlight all the functions that do memory allocations on the heap 197 | * Highlight all the functions with unsafe memory access 198 | 199 | For example, we can have a list of all the C functions that make memory 200 | allocations and color them red (easy option) as well as transitive 201 | functions that use them (hard option). Same goes for Rust, we can 202 | highlight functions that are _definitely_ safe versus functions that 203 | directly or transitively use `unsafe` blocks. 204 | 205 | ### Tests integration 206 | 207 | **Language-specific** **Opinionated** 208 | 209 | Right after running the test, in addition to jumping to the errored test line: 210 | * Highlight the line which exactly failed the test and show the condensed 211 | error message (not so exciting and probably more about the preference) 212 | * Highlight all the functions which caused the fail 213 | 214 | Test-driven development folks will be just astonished, I'm sure. Maybe even 215 | make every now-passed-previously-failed test green. For example, imagine that 216 | `=` is dark red highlighted line and `*` is a function highlighted bright red, 217 | in Ruby: 218 | ``` 219 | # Test file 220 | test "should not send cookie emails" do 221 | cookie = cookies(:cinnamon) 222 | 223 | perform_enqueued_jobs do 224 | ====assert_no_emails=do=============================== 225 | cookie.*sendChocolateEmails* 226 | end 227 | end 228 | end 229 | 230 | # `Cookie` class file 231 | class Cookie 232 | def *sendChocolateEmails* 233 | chocolate_cookies.each(&:send_email) 234 | end 235 | end 236 | ``` 237 | 238 | ## Architecture 239 | 240 | TODO 241 | 242 | - https://lobste.rs/s/jembrx 243 | - https://xi-editor.io/docs/rope_science_11.html 244 | 245 | ## Overview of existing solutions 246 | 247 | ### Kakoune 248 | 249 | [Kakoune] uses regex with slight modifications to allow code reuse and nested structures. 250 | Looks almost good enough: usually it is hard to edit and some rare constructs 251 | seem hard to implement (I couldn't implement highlighting for [slim], but 252 | maybe I'm not that smart). 253 | 254 | [slim]: https://github.com/slim-template/slim 255 | 256 | ### amp 257 | 258 | [amp] reuses [TextMate grammar] syntax highlighting configurations. 259 | The config files look as a mix of regex and some kind of pushdown automata. 260 | Extremely interesting option but needs more research. 261 | 262 | ### vis 263 | 264 | [vis] it has direct integration with Lua and uses Parsing Grammar Expressions (PEG) 265 | in combination with the Lua language features. Looks very neat and powerful. 266 | 267 | ### Emacs 268 | 269 | [Emacs] it doesn't have anything in particular, just uses some regex together with 270 | the power of Emacs-Lisp language. The editor is the Lisp machine, it doesn't 271 | really need anything special. 272 | 273 | ### joe 274 | 275 | [joe] uses a full-blown description of a state machine that parses the text with 276 | simplified grammar rules. Quite large files, for example C grammar takes 277 | 300 loc, Ruby grammar takes 600 loc (Ruby has complicated grammar). Although 278 | the grammar is correct (e.g. Kakoune grammar is not 100% correct), it takes 279 | some dedication to create such a grammar file. 280 | 281 | [Kakoune]: https://github.com/mawww/kakoune 282 | [amp]: https://github.com/jmacdonald/amp 283 | [vis]: https://github.com/martanne/vis 284 | [micro]: https://github.com/zyedidia/micro 285 | [vy]: https://github.com/vyapp/vy 286 | [neovim]: https://github.com/neovim/neovim 287 | [helix]: https://github.com/helix-editor/helix 288 | [xi]: https://xi-editor.io/ 289 | [qedit]: https://github.com/fivemoreminix/qedit 290 | [kilo]: https://github.com/antirez/kilo 291 | [moe]: https://github.com/fox0430/moe 292 | [paravim]: https://github.com/paranim/paravim 293 | [focus]: https://github.com/jamii/focus 294 | [Emacs]: https://www.gnu.org/software/emacs/ 295 | [joe]: https://joe-editor.sourceforge.io/ 296 | [TextMate grammar]: https://macromates.com/manual/en/language_grammars 297 | -------------------------------------------------------------------------------- /design_docs/keybindings.md: -------------------------------------------------------------------------------- 1 | # Keybindings design 2 | 3 | Keybindings should allow the user of the editor to quickly navigate and edit 4 | file as well as provide capabilities for composing green field projects 5 | efficiently. Modal text editing is in generally more 6 | convenient for modifying and navigation whereas modeless editing 7 | is more convenient for when one needs to write a lot of text 8 | [[Merle F. Poller and Susan K. Garter. 1983. A comparative study of moded and modeless text editing by experienced editor user]]. 9 | This is a modal text editor, hence we should excel at modifying and navigating 10 | the text, this is the main priority. Consequently making writing projects from 11 | scratch convenient is not the highest priority but there's still a lot of room 12 | for improvement upon the simplest "can insert in Insert mode" case. 13 | 14 | Next we define several modes 15 | of user input which we use to compactly and intuitively assign keys to actions: 16 | * Action - just as simple as it sounds, a key press results in an 17 | immediate action. 18 | * Activate/enable mini-mode - pressing the key enters a mini-mode 19 | which has its own keybindings and the next key press (or all next keys pressed 20 | until exited if we "enable" it, not just "activate") is interpreted 21 | according to the mini-mode keymap. 22 | * Argument: 23 | * Pressing numbers creates a numeric argument which will be 24 | passed to the next command if that command accepts such an argument. 25 | * Several other options are for considerations: letter argument for 26 | manipulation with registers, boolean "universal" argument for slight 27 | modifications of the original command. 28 | * Prompt - pressing the key expects further input, longer than 1 character. 29 | Example can be searching or entering a command. 30 | 31 | The logic for assigning keys to actions is as follows: the most frequently 32 | used actions must be the easiest ones to type. There are some keys which we 33 | can inherit from the Vim editor like cursor movement with hjkl. Keys which 34 | activate mini-modes must be mnemonic as opposed to keys which do just a single 35 | action - these keys are generally learned and it is only beneficial to make 36 | them mnemonic for learning purposes. 37 | 38 | Examples of a mini-mode keybindings (not necessarily used in this editor), 39 | where consequent key presses are divided with minus `-`: 40 | * b-n - switch to next buffer 41 | * b-p - switch to previous buffer 42 | * space-w-l - switch to left window 43 | * space-w-h - switch to right window 44 | 45 | **I don't have information regarding the validity of the following claim, 46 | only empirical evidence**. 47 | There are expected to be a lot of 2-key combination for commands from the 48 | sequence "activate mini-mode"->"action", it is beneficial to make them 49 | easy to type. General strategies are: 50 | * 2 buttons are pressed by different hands with the second hand using point 51 | finger or middle finger. 52 | * 2 buttons are pressed by the same hand with first finger being the point 53 | finger. 54 | * 1st button is a space, then hands can conveniently press any other button. 55 | This is the only universal solution which works for Qwerty, Dvorak, Workman, 56 | Colemak and whatnot other English language keyboard layout. 57 | 58 | Modifier keys (Ctrl, Shift, Alt) should have minimal usage in Normal mode, 59 | 2 button combination via mini-mode should be preferred. The reason is that 60 | modifier key can be counted as a key press but in addition to 2 button 61 | combination you also have to keep the button pressed which is suboptimal 62 | for ergonomics. But in Insert mode modifier keys can be used for cursor 63 | movement (readline-style or emacs-style) and different operations since 64 | there's really no other choice. Some examples for Insert mode: 65 | * Ctrl-n - move cursor down 66 | * Ctrl-p - move cursor up 67 | * Ctrl-f - move cursor forward 68 | * Ctrl-b - move cursor backward 69 | 70 | Some considerations on using Shift modifier key in Normal mode: it is used 71 | to define an "opposite" or "reversed" action for the small-letter counterpart. 72 | Examples, but not necessarily current keybindings: 73 | * u, U - undo, redo 74 | * n, N - search forward, search backward 75 | * s, S - save, save as 76 | * w, W - move forward one `word`, move forward one `WORD`, same for b, B 77 | * z, Z - activate mini-mode, enable mini-mode 78 | 79 | Exceptions: 80 | * /, ? - should probably mean different things. So the rule only works for 81 | letters. 82 | 83 | Other than English languages can have their keymaps too. We don't need to 84 | support it extensively but there's a neat solution in Vis editor (maybe others) 85 | with command `:langmap ролд hjkl` which maps 4 not English characters to their 86 | English counterpart. Alternatively we can allow non-ascii characters in keymap 87 | config file. These solutions are not applicable to all languages and maybe 88 | for now this is alright. 89 | 90 | [Merle F. Poller and Susan K. Garter. 1983. A comparative study of moded and modeless text editing by experienced editor user]: https://doi.org/10.1145/800045.801603 91 | 92 | ## Frequent actions 93 | Will be assigned a single key press. 94 | 95 | * Move cursor up 96 | * Move cursor down 97 | * Move cursor left 98 | * Move cursor right 99 | * ... 100 | 101 | ## Not so frequent actions 102 | Will be grouped together into a mini-mode where possible, used with a Shift 103 | modifier or assigned one of ~!@#$%^&*()_+ buttons (also with Shift :) ). 104 | And also anything that requires press the Space first. 105 | 106 | * Move cursor to the end of line 107 | * Move cursor to the start of line 108 | * Move cursor to the first non-space character of a line 109 | * Move cursor to the first line of a buffer 110 | * Move cursor to the last line of a buffer 111 | * List buffers 112 | * Switch to buffer [buffer] 113 | * Switch to last opened buffer 114 | * Switch to next buffer 115 | * Switch to previous buffer 116 | * Switch to scratch buffer 117 | * Switch to debug buffer 118 | * Delete current buffer 119 | * Delete buffer [buffer] 120 | * Delete all buffers but current 121 | * Edit file [path] 122 | * ... 123 | 124 | ## Infrequent actions 125 | Will only be available via a manual command prompt where you have to type the 126 | name of the command in order to execute it. This is also more flexible since 127 | some commands may require an argument. Although it is possible to pass an 128 | argument to a command in the config file. 129 | 130 | All the commands above should be available via manual prompt of a command 131 | (as when pressin `:` in Vim). Any additional actions are available here 132 | as well as available to be bound to a key in the config file. 133 | -------------------------------------------------------------------------------- /design_docs/search.md: -------------------------------------------------------------------------------- 1 | # Search design 2 | 3 | Searching is referred to searching for a string inside a file or multiple 4 | files. 5 | 6 | There are several types of standard searching algorithms: 7 | * Exact - search string should appear exactly in the file, most simple. 8 | * Regex - use full power of regular expressions for searching. 9 | * PEG - more powerful than regex but could be harder to use and is generally 10 | less popular. This needs more research. 11 | 12 | But there are more ideas: 13 | * Exact with word boundaries - exact search but it must not be surrounded by 14 | any other characters, so `content` does not match `contents` because of the 15 | "s" at the end. 16 | * Camel-Kebab-Pascal-Snake-case-insensitive (very-case-insensitive) - 17 | `text_buffer` search items will match all of `text_buffer`, `TextBuffer`, 18 | `textBuffer`, `text-buffer`. 19 | 20 | In GUI editors the search is often coupled with "replace" functionality and 21 | looks like for a good reason. So maybe this is a good idea to do search 22 | first and then provide an option to move from here to replace these things? 23 | * Exact - exact replacement, nothing fancy. 24 | * Regex - provide handles for groups such as `\1` or `$1` for the first group. 25 | * Exact with word boundaries - same as Exact, exact replacement. 26 | * Very-case-insensitive - replace with the same style as the original style is. 27 | 28 | How to do exact but case-insensitive search? Probably with regex, popular 29 | engines use `(?i)` prefix. This could also be same as very-case-insensitive 30 | in some cases. 31 | 32 | ## Keymap configuration 33 | 34 | We will probably settle at the following configuration: 35 | ``` 36 | settings: 37 | default_search: exact 38 | 39 | keymap: 40 | normal: 41 | /: search_default 42 | : 43 | s: 44 | e: search_exact 45 | r: search_regex 46 | b: search_exact_with_boundaries 47 | i: search_camel_kebab_pascal_snake_case_insensitive 48 | search: # this mode is entered on pressing / 49 | default: insert_character 50 | ctrl-r: search_and_replace 51 | ``` 52 | 53 | So there's a way to choose a default search kind and use it by hitting `/` and 54 | if one needs another kind of search, one can access it by hitting in sequence 55 | Space-s and choose the preferred way. And while searching, one can hit Ctrl-r 56 | to enter a replace mode. 57 | 58 | TODO: spec out replace mode. 59 | 60 | ## Project-wide searching 61 | 62 | Another case is project-wide searching. It is probably best achieved by the 63 | combination of external tools such as `ripgrep` + `fzf` so for now the 64 | decision is to be limited by these external tools. We can write one for 65 | ourselves if we really need to, I definitely see a case for project-wide 66 | search-and-replace of Camel-Kebab-Pascal-Snake-case-insensitive words. 67 | -------------------------------------------------------------------------------- /design_docs/server_architecture.md: -------------------------------------------------------------------------------- 1 | # Server architecture 2 | 3 | ## Architecture 4 | 5 | This section is volatile and may change frequently. 6 | 7 | ![Architecture diagram](docs/assets/architecture.png) 8 | 9 | Examples of a Generic Plugin: 10 | - Language Server Protocol 11 | - Autocompletion 12 | - Pair autoinsertion 13 | - Jumping inside a file 14 | 15 | ### Why client-server architecture? 16 | 17 | Short answer: because it's fun, more opportunities, and it doesn't promise 18 | to be too overwhelming. Longer answer: 19 | 20 | * Frontends must only speak JSON, they can be written in any language. 21 | * Commandline tools can interact with a running Server session in the same way 22 | as Clients do, it is already present. 23 | * Switching to Client-Server architecture later is almost equal to a complete 24 | rewrite of the system, so why not just do it from the start. 25 | 26 | Also see: 27 | * [neovim-remote] 28 | * [foot server daemon mode] 29 | 30 | [neovim-remote]: https://github.com/mhinz/neovim-remote 31 | [foot server daemon mode]: https://codeberg.org/dnkl/foot#server-daemon-mode 32 | 33 | ## Request-Respond loop 34 | 35 | Server is implemented as a traditional while true loop. It listens on a Unix 36 | domain socket and waits for Client connections. Once the Client is connected, 37 | the Server initializes some state for this Client and keeps its socket, 38 | listening for both incoming Clients and message from the new Client. Every 39 | Client has a consistent connection to the Server and it only interrupts when the 40 | Client session ends. 41 | 42 | Server speaks JSON-RPC 2.0 protocol but it does not solely act as a "Server", 43 | meaning that it does not _only_ accepts requests, it can also initiate 44 | requests to the Client, such as "the current file being edited has changed" 45 | if program change the file. So the Server expects the Client to also listen 46 | for any requests. In this sense both Client and Server both send and respond to 47 | requests. 48 | 49 | Every request from the Client is resolved asynchronously. 50 | ``` 51 | Client requests --> 52 | Client waits 53 | <-- Server responds 54 | Client receives the response and continues 55 | ``` 56 | 57 | In theory this should be fast enough and in worst case there could be a 58 | visible lag. Unfortunately we will only experience the lag when we 59 | implement it. And at that point we will be too deep into the design so 60 | we will have to either optimize everything out of its guts which may 61 | increase the complexity exponentially, or reimplement everything from 62 | scratch, resuing some existing code and decisions. I really hope we 63 | won't have to go this route. 64 | 65 | Every request from the Server is resolved in 3 phases, with the first one 66 | being a non-blocking request to the Client, and then the Client does its 67 | usual synchronous request. 68 | ``` 69 | Server requests, non-blocking -> 70 | Client receives, does not respond 71 | Client requests --> 72 | Client waits 73 | <-- Server responds 74 | Client receives the response and continues 75 | ``` 76 | 77 | The idea is that in order to send something in a non-blocking way, that 78 | something must be all fit into `SO_SNDBUF` which is OS-dependent. On my 79 | machine for Unix domain sockets it is 208KB which is very enough but we 80 | probably shouldn't rely on this fact. So the idea is to send something 81 | small to the Client, so the the Client can ask for that something. 82 | This is how we do non-blocking send. 83 | 84 | Now, why do we need non-blocking send? This is to avoid deadlocks. On the Server 85 | we always expect some requests from Clients and if we end up in a situation when 86 | the Server and the Client simultaneously send their request, we have a deadlock 87 | where both are waiting for a response from both sides at the same time. The 88 | non-blocking send resolves this situation since at no point in time we block on 89 | the call to `send` in user space. 90 | 91 | ### Implications 92 | 93 | Client and Server just _have_ to speak through this JSON interface. And because 94 | of while-true loops it is impossible to run in a single thread since they both 95 | have their own while-true loop. However we can do cooperative multitasking in a 96 | single thread where each of them seizes the to the other loop via an "event 97 | loop" mechanism. But currently this mechanism is in "beta" state in the Zig 98 | standard library and it is not a priority. But it is a viable option, should we 99 | have such requirements. 100 | 101 | Another idea is to remove the while-true loop for the Client altogether so the 102 | Server is the only while-true loop and in the end of this loop it gives control 103 | to the Client so it does its drawing things. Though this significantly impairs 104 | the available functionality. 105 | -------------------------------------------------------------------------------- /design_docs/sqlite.md: -------------------------------------------------------------------------------- 1 | # SQLite 2 | 3 | [SQLite] is a C-language library that implements a small, fast, self-contained, 4 | high-reliability, full-featured, SQL database engine. 5 | 6 | [SQLite]: https://www.sqlite.org/index.html 7 | 8 | Why algorithms? Why careful memory management, specialized data structures? Just 9 | write some SQL. 10 | 11 | Code is data. 12 | 13 | Data is data. 14 | -------------------------------------------------------------------------------- /design_docs/text_display.md: -------------------------------------------------------------------------------- 1 | # Text display 2 | 3 | ## Numbering 4 | 5 | * Absolute line numbers. 6 | * Relative line numbers. 7 | * Visual absolute line numbers which account for folding. 8 | * Visual relative line numbers which account for folding. 9 | 10 | Some considerations: 11 | * A special case of folding is narrowing, when only a specific part of the file 12 | is visible in the buffer. However it doesn't add a new type of numbering. 13 | 14 | ## Wrapping 15 | 16 | * Don't wrap, display indicator that the line is longer than the screen. 17 | * Wrap at the visual line end. 18 | * Wrap at the nearest to the line end blank character. 19 | * Wrap at the nearest to the line end blank character but also redefine 20 | various commands to operate on visual lines instead of logical lines. 21 | 22 | Some considerations: 23 | * There could be a setting which inserts newlines when the line becomes too long. 24 | 25 | ### Questions 26 | * Which commands operate on logical lines and which commands operate on visual 27 | lines? In Vim there's a special sequence for up/down keys to operate on visual 28 | lines. In Emacs up/down by default operates on visual lines but all other 29 | commands always operate on logical lines. Are there any others commands which 30 | make sense to also customize with different operation modes? 31 | -------------------------------------------------------------------------------- /design_docs/text_manipulation.md: -------------------------------------------------------------------------------- 1 | # Text manipulation design 2 | 3 | There are 3 commands to directly manipulate the selection of 4 | text in the buffer (even if the selection is of size 1): 5 | * Delete - just remove the text from the buffer 6 | * Cut - remove the text from the buffer and insert it into a cut history 7 | * Paste - put the text in the buffer from the cut history 8 | 9 | Normally you would want to cut the text instead of deleting so that it 10 | is saved in the history and can be later used for insertion. 11 | 12 | After pasting the text there's another command Paste-Cycle which changes 13 | the inserted text to the older entry in the cut history. 14 | 15 | ## Clipboard integration 16 | 17 | Clipboard integration is active by default but it can be deactivated. 18 | 19 | Each expansion/detraction of the selection with the size of more than 1 20 | also copies this text to the "primary" selection of the system if that 21 | selection is supported. On X system this selection can be pasted to other 22 | applications by clicking a mouse middle button. 23 | 24 | Delete command does not interact with the system clipboard. 25 | 26 | Cut command also puts the text into the "clipboard" selection of the system. 27 | This selection can be normally inserted with the Ctrl-v shortcut. 28 | 29 | Paste command first checks the "clipboard" selection of the system, and if 30 | there's text present, it is inserted to the buffer. If there's none, then 31 | the text is inserted from the cut history. 32 | -------------------------------------------------------------------------------- /design_docs/windowing.md: -------------------------------------------------------------------------------- 1 | # Windowing design 2 | 3 | In some work flows it is desirable to have several editor windows open 4 | side-by-side: either implementation and test file, or different parts 5 | of the same file in case it's 10k lines long. Certainly there are tools 6 | to help with that such as a list of opened files and switching between 7 | current and last opened file, but sometimes it doesn't cut all the cases. 8 | 9 | I see 2 main approaches to windowing: leaving it to the third-party tools 10 | and integrating it into the editor. 11 | 12 | ## Third-party tool approach 13 | 14 | There are a lot of approaches: 15 | * Delegating it to the window manager, probably tiling, from [dwm] and [i3wm] 16 | for X to [Sway] and [river] for Wayland and million others, pick your poison 17 | * Delegating it to the terminal multiplexer such as [tmux], [screen] or [dvtm] 18 | * Delegating it to the virtual terminal emulator such as [kitty], [Terminator] 19 | or [Tilix] 20 | 21 | [dwm]: https://dwm.suckless.org/ 22 | [i3wm]: https://i3wm.org/ 23 | [Sway]: https://swaywm.org/ 24 | [river]: https://github.com/ifreund/river 25 | [tmux]: https://github.com/tmux/tmux 26 | [dvtm]: https://www.brain-dump.org/projects/dvtm/ 27 | [screen]: https://www.gnu.org/software/screen/ 28 | [kitty]: https://sw.kovidgoyal.net/kitty/ 29 | [Terminator]: https://gnome-terminator.org/ 30 | [Tilix]: https://gnunn1.github.io/tilix-web/ 31 | 32 | The main requirement is to be able to hook the new editor session into the 33 | existing session. Since this editor has a client-server architecture, this 34 | is no problem, we just need to supply a server identifier. This approach 35 | is easy and mostly solved by others, we can as well capitalize on their 36 | success. 37 | 38 | ## Builtin windowing functionality 39 | 40 | There are several use cases for having windowing functionality builtin into 41 | the editor: 42 | * Temporary windows with error messages or help windows 43 | * There are interactions such as searching which can be applied across 44 | several windows 45 | * Some people prefer all-in-one experience for a text editor, such as 46 | [VSCode] 47 | * GUI clients won't have most of the niceties which are available for 48 | terminal clients 49 | 50 | [VSCode]: https://code.visualstudio.com/ 51 | 52 | Looking at it from this point there are definite benefits of integrating 53 | windows into the editor. Even with all the third-party tools in the world 54 | it would be convenient to have temporary windows for tool integrations 55 | and better communication with the user of the editor about errors or 56 | help messages. 57 | 58 | A note on tabs. I have no idea how to use tabs more than just switching 59 | between them. This is a sign that tabs are probably not integral to the 60 | editor and can be off-handed to external tools: for terminal clients it 61 | would be a large ecosystem of all the solutions listed above, for GUI 62 | clients unfortunately they will have to implement this functionality 63 | themselves. 64 | 65 | There's also a choice whether the server needs to know about windows or 66 | if the windows should be fully offloaded to the client. This is likely 67 | to be determined during implementation, TODO. 68 | 69 | There are several types of interactions we would like to have with the 70 | windows, let's enumerate them. 71 | 72 | ### Consistent windows with information 73 | 74 | Consistent windows are only closed when the user specifically issues 75 | a command to close the window. Examples: 76 | * Another window for editing and opening files 77 | * grep search buffer 78 | * git diff 79 | * Help buffer 80 | 81 | These are just usual windows which don't require any specific functionality. 82 | 83 | ### Temporary info windows 84 | 85 | Temporary info windows appear and close based on some events happening 86 | in the editor. Examples: 87 | * Compilation errors 88 | * Test failures 89 | 90 | These windows will have to be somehow identified and remembered when they 91 | are created and later closed as a consequence of processing events on the 92 | server. Most of the complexity of implementing these windows lies in 93 | the event processing but should be doable. 94 | 95 | ### Temporary prompt windows 96 | 97 | Temporary prompt windows require some user action before they are closed, 98 | typically the result of an action is then somehow used. Examples: 99 | * Fuzzy search with a third-party utility like [fzf] or [skim] and then 100 | opening of a file 101 | * Writing commit messages with version control tool 102 | 103 | [fzf]: https://github.com/junegunn/fzf 104 | [skim]: https://github.com/lotabout/skim 105 | 106 | The window should be opened which gives full control to the third-party tool 107 | and the result is later piped into the editor and used appropriately. This 108 | is the most complicated implementation but at the same time it should be 109 | worth it. Alternative implementations have to either [rely on 110 | extension language], they have to be [overly complicated] or they 111 | have to use [scary bash scripts]. Every option seems subpar for such a common 112 | workflow. 113 | 114 | [rely on extension language]: https://git.sr.ht/~mcepl/vis-fzf-open/tree/master/item/init.lua 115 | [overly complicated]: https://github.com/andreyorst/fzf.kak/blob/master/rc/fzf.kak 116 | [scary bash scripts]: https://github.com/greenfork/dotfiles/blob/efeeda144639cbbd11e3fe68d3e78145080be47a/.config/kak/kakrc#L180-L188 117 | 118 | It is worth saying that [fzf] and similar programs provide quite a core 119 | workflow of finding specific files in the project either by content or 120 | by filename. 121 | -------------------------------------------------------------------------------- /not_good_enough/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const mode = b.standardReleaseOptions(); 6 | 7 | const exe = b.addExecutable("kisa", "src/main.zig"); 8 | exe.addPackagePath("zzz", "libs/zzz/src/main.zig"); 9 | exe.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); 10 | exe.addPackagePath("ziglyph", "libs/ziglyph/src/ziglyph.zig"); 11 | exe.addPackagePath("kisa", "src/kisa.zig"); 12 | exe.setTarget(target); 13 | exe.setBuildMode(mode); 14 | exe.install(); 15 | 16 | const run_cmd = exe.run(); 17 | run_cmd.step.dependOn(b.getInstallStep()); 18 | if (b.args) |args| { 19 | run_cmd.addArgs(args); 20 | } 21 | 22 | const run_step = b.step("run", "Run the app"); 23 | run_step.dependOn(&run_cmd.step); 24 | 25 | const run_terminal_ui = b.step("run-terminal-ui", "Run demonstration of terminal UI"); 26 | const terminal_ui = b.addExecutable("terminal_ui", "src/terminal_ui.zig"); 27 | terminal_ui.addPackagePath("kisa", "src/kisa.zig"); 28 | terminal_ui.setTarget(target); 29 | terminal_ui.setBuildMode(mode); 30 | const run_terminal_ui_cmd = terminal_ui.run(); 31 | run_terminal_ui.dependOn(&run_terminal_ui_cmd.step); 32 | 33 | const run_ui = b.step("run-ui", "Run demonstration of UI"); 34 | const ui = b.addExecutable("ui", "src/ui_api.zig"); 35 | ui.addPackagePath("kisa", "src/kisa.zig"); 36 | ui.setTarget(target); 37 | ui.setBuildMode(mode); 38 | const run_ui_cmd = ui.run(); 39 | run_ui.dependOn(&run_ui_cmd.step); 40 | 41 | const run_highlight = b.step("run-highlight", "Run demonstration of highlight"); 42 | const highlight = b.addExecutable("highlight", "src/highlight.zig"); 43 | highlight.addPackagePath("kisa", "src/kisa.zig"); 44 | highlight.setTarget(target); 45 | highlight.setBuildMode(mode); 46 | const run_highlight_cmd = highlight.run(); 47 | run_highlight.dependOn(&run_highlight_cmd.step); 48 | 49 | const test_all = b.step("test", "Run tests"); 50 | const test_main = b.step("test-main", "Run tests in main"); 51 | const test_main_nofork = b.step("test-main-nofork", "Run tests in main without forking"); 52 | const test_state = b.step("test-state", "Run tests in state"); 53 | const test_buffer = b.step("test-buffer", "Run tests in buffer API"); 54 | const test_config = b.step("test-config", "Run tests in config"); 55 | const test_jsonrpc = b.step("test-jsonrpc", "Run tests in jsonrpc"); 56 | const test_transport = b.step("test-transport", "Run tests in transport"); 57 | const test_rpc = b.step("test-rpc", "Run tests in rpc"); 58 | const test_keys = b.step("test-keys", "Run tests in keys"); 59 | const test_ui = b.step("test-ui", "Run tests in UI"); 60 | const test_highlight = b.step("test-highlight", "Run tests in highlight"); 61 | 62 | { 63 | var test_cases = b.addTest("src/main.zig"); 64 | test_cases.setFilter("main:"); 65 | test_cases.addPackagePath("zzz", "libs/zzz/src/main.zig"); 66 | test_cases.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); 67 | test_cases.addPackagePath("ziglyph", "libs/ziglyph/src/ziglyph.zig"); 68 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 69 | test_cases.setTarget(target); 70 | test_cases.setBuildMode(mode); 71 | test_all.dependOn(&test_cases.step); 72 | test_main.dependOn(&test_cases.step); 73 | test_main_nofork.dependOn(&test_cases.step); 74 | } 75 | 76 | { 77 | // Forked tests must be run 1 at a time, otherwise they interfere with other tests. 78 | var test_cases = b.addTest("src/main.zig"); 79 | test_cases.setFilter("fork/socket:"); 80 | test_cases.addPackagePath("zzz", "libs/zzz/src/main.zig"); 81 | test_cases.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); 82 | test_cases.addPackagePath("ziglyph", "libs/ziglyph/src/ziglyph.zig"); 83 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 84 | test_cases.setTarget(target); 85 | test_cases.setBuildMode(mode); 86 | test_all.dependOn(&test_cases.step); 87 | test_main.dependOn(&test_cases.step); 88 | } 89 | 90 | { 91 | const test_cases = b.addTest("src/state.zig"); 92 | test_cases.setFilter("state:"); 93 | test_cases.addPackagePath("zzz", "libs/zzz/src/main.zig"); 94 | test_cases.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); 95 | test_cases.addPackagePath("ziglyph", "libs/ziglyph/src/ziglyph.zig"); 96 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 97 | test_cases.setTarget(target); 98 | test_cases.setBuildMode(mode); 99 | test_all.dependOn(&test_cases.step); 100 | test_state.dependOn(&test_cases.step); 101 | } 102 | 103 | { 104 | const test_cases = b.addTest("src/buffer_api.zig"); 105 | test_cases.setFilter("buffer:"); 106 | test_cases.addPackagePath("ziglyph", "libs/ziglyph/src/ziglyph.zig"); 107 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 108 | test_cases.setTarget(target); 109 | test_cases.setBuildMode(mode); 110 | test_all.dependOn(&test_cases.step); 111 | test_buffer.dependOn(&test_cases.step); 112 | } 113 | 114 | { 115 | const test_cases = b.addTest("src/text_buffer_array.zig"); 116 | test_cases.setFilter("buffer:"); 117 | test_cases.addPackagePath("ziglyph", "libs/ziglyph/src/ziglyph.zig"); 118 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 119 | test_cases.setTarget(target); 120 | test_cases.setBuildMode(mode); 121 | test_all.dependOn(&test_cases.step); 122 | test_buffer.dependOn(&test_cases.step); 123 | } 124 | 125 | { 126 | const test_cases = b.addTest("src/config.zig"); 127 | test_cases.setFilter("config:"); 128 | test_cases.addPackagePath("zzz", "libs/zzz/src/main.zig"); 129 | test_cases.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); 130 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 131 | test_cases.setTarget(target); 132 | test_cases.setBuildMode(mode); 133 | test_all.dependOn(&test_cases.step); 134 | test_config.dependOn(&test_cases.step); 135 | } 136 | 137 | { 138 | const test_cases = b.addTest("src/jsonrpc.zig"); 139 | test_cases.setFilter("jsonrpc:"); 140 | test_cases.setTarget(target); 141 | test_cases.setBuildMode(mode); 142 | test_all.dependOn(&test_cases.step); 143 | test_jsonrpc.dependOn(&test_cases.step); 144 | } 145 | 146 | { 147 | const test_cases = b.addTest("src/transport.zig"); 148 | test_cases.setFilter("transport/fork1:"); 149 | test_cases.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); 150 | test_cases.setTarget(target); 151 | test_cases.setBuildMode(mode); 152 | test_all.dependOn(&test_cases.step); 153 | test_transport.dependOn(&test_cases.step); 154 | } 155 | 156 | { 157 | const test_cases = b.addTest("src/transport.zig"); 158 | test_cases.setFilter("transport/fork2:"); 159 | test_cases.addPackagePath("known-folders", "libs/known-folders/known-folders.zig"); 160 | test_cases.setTarget(target); 161 | test_cases.setBuildMode(mode); 162 | test_all.dependOn(&test_cases.step); 163 | test_transport.dependOn(&test_cases.step); 164 | } 165 | 166 | { 167 | const test_cases = b.addTest("src/rpc.zig"); 168 | test_cases.setFilter("myrpc:"); 169 | test_cases.setTarget(target); 170 | test_cases.setBuildMode(mode); 171 | test_all.dependOn(&test_cases.step); 172 | test_rpc.dependOn(&test_cases.step); 173 | } 174 | 175 | { 176 | const test_cases = b.addTest("src/keys.zig"); 177 | test_cases.setFilter("keys:"); 178 | test_cases.setTarget(target); 179 | test_cases.setBuildMode(mode); 180 | test_all.dependOn(&test_cases.step); 181 | test_keys.dependOn(&test_cases.step); 182 | } 183 | 184 | { 185 | const test_cases = b.addTest("src/terminal_ui.zig"); 186 | test_cases.setFilter("ui:"); 187 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 188 | test_cases.setTarget(target); 189 | test_cases.setBuildMode(mode); 190 | test_all.dependOn(&test_cases.step); 191 | test_ui.dependOn(&test_cases.step); 192 | } 193 | 194 | { 195 | const test_cases = b.addTest("src/ui_api.zig"); 196 | test_cases.setFilter("ui:"); 197 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 198 | test_cases.setTarget(target); 199 | test_cases.setBuildMode(mode); 200 | test_all.dependOn(&test_cases.step); 201 | test_ui.dependOn(&test_cases.step); 202 | } 203 | { 204 | const test_cases = b.addTest("src/highlight.zig"); 205 | test_cases.setFilter("highlight:"); 206 | test_cases.addPackagePath("kisa", "src/kisa.zig"); 207 | test_cases.setTarget(target); 208 | test_cases.setBuildMode(mode); 209 | test_all.dependOn(&test_cases.step); 210 | test_highlight.dependOn(&test_cases.step); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /not_good_enough/docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation for Kisa 2 | 3 | https://greenfork.github.io/kisa/ 4 | 5 | ## Develop 6 | 7 | 1. Install [Nim]. 8 | 2. Run `nimble install --depsOnly` install dependencies. 9 | 3. Run `karun -r -w index.nim` to develop, browser page should be automatically 10 | opened. Alternatively open `localhost:8080`. 11 | 4. When done, run `nimble release` and push to repository. 12 | 13 | [Nim]: https://nim-lang.org/ 14 | -------------------------------------------------------------------------------- /not_good_enough/docs/ROADMAP.org: -------------------------------------------------------------------------------- 1 | #+title: Kisa project roadmap 2 | #+author: greenfork 3 | #+STARTUP: logdone content 4 | 5 | * DONE Stage 1: Prototype [100%] 6 | 7 | ** DONE Handle keyboard keys 8 | 9 | ** DONE Draw on the terminal 10 | 11 | ** DONE Read file for displaying 12 | 13 | * TODO Stage 2: Basic working program [14%] 14 | :PROPERTIES: 15 | :COOKIE_DATA: todo recursive 16 | :END: 17 | 18 | This stage should result in a basic working editor with some experimental 19 | features. It should provide answers to fundamental design decisions which 20 | should be taken further. 21 | 22 | ** TODO Write design documents [8/10] 23 | - [X] Client architecture 24 | - [X] Server architecture 25 | - [X] Configuration 26 | - [X] Cursor 27 | - [ ] Extensions 28 | - [X] Highlighting 29 | - [X] Keybindings 30 | - [X] Search 31 | - [X] Windowing 32 | - [ ] User interface 33 | 34 | ** TODO Client-Server communication [3/5] 35 | 36 | *** DONE Write RPC implementation [2/2] 37 | CLOSED: [2021-09-02 Thu 23:39] 38 | - [X] Write JSON-RPC 2.0 implementation 39 | - [X] Wrap it into program-specific interface 40 | 41 | *** DONE Write polling with file descriptors 42 | CLOSED: [2021-09-02 Thu 23:39] 43 | 44 | *** TODO Server-side loop [2/3] 45 | - [X] Accept new clients 46 | - [X] Process client requests 47 | - [ ] Send push notifications to clients 48 | 49 | *** TODO Client-side loop [1/3] 50 | - [X] Send key presses 51 | - [ ] Send commands 52 | - [ ] Process push notifications from the server 53 | 54 | *** DONE Error handling 55 | CLOSED: [2021-09-02 Thu 23:45] 56 | 57 | ** TODO Terminal user interface [0/6] 58 | 59 | *** TODO Draw on the screen 60 | 61 | *** TODO Read keys from the keyboard 62 | 63 | *** TODO Accumulate number multiplier 64 | Pressing numbers should create a multiplier for the next command, normally 65 | the number of times this command should be repeated. 66 | 67 | *** TODO Draw beautiful UI boxes 68 | 69 | *** TODO Enter command mode 70 | Prompt should appear where the user can type commands 71 | 72 | *** TODO Draw cursor and selections 73 | 74 | ** TODO Windowing [0/4] 75 | - [ ] Evenly spaced horizontal layout 76 | - [ ] Evenly spaced vertical layout 77 | - [ ] Splits layout 78 | - [ ] Stack layout 79 | 80 | ** TODO State management [1/2] 81 | - [X] Managing resources 82 | - [ ] Layout of multiple splits 83 | 84 | ** TODO Text buffer [0/2] 85 | *** TODO Prototype [3/9] 86 | Implementation as a simple contiguous growing array. Mainly 87 | it is needed to understand the interface and get some idea about the kind 88 | of processing that is necessary for common operations on the text buffer. 89 | - [-] Move the cursor 90 | + [X] Left, right 91 | + [X] Up, down 92 | + [X] Start of the line, end of the line 93 | + [X] To the first non-empty character of the line 94 | + [X] Start, end of buffer 95 | + [X] Next, previous word 96 | - [ ] Select 97 | + [ ] Surrounding inner object 98 | + [ ] Surrounding object with borders 99 | + [ ] Line, word, letters 100 | - [X] Insert/remove characters 101 | - [ ] Multiple cursors proof-of-concept 102 | - [ ] Search exact 103 | - [ ] Search regex 104 | - [X] Open file 105 | - [X] Open scratch buffer 106 | - [ ] Not consecutive lines :: Some functionality like code folding or 107 | filtering the buffer to only contain certain lines will make lines not 108 | consecutive. 109 | *** TODO Main implementation 110 | Decide on main implementation and write it. This decision is not final but 111 | still seems necessary for a basic working program. 112 | - [ ] Array of bytes 113 | - [ ] Piece table 114 | - [ ] Array of lines 115 | - [ ] Gap buffer 116 | - [ ] Rope 117 | 118 | ** TODO Configuration processing [1/3] 119 | *** DONE Decide on file format - [[https://github.com/gruebite/zzz][zzz]] - still not the final decision 120 | CLOSED: [2021-09-03 Fri 00:43] 121 | *** TODO Correction on file format 122 | *** TODO Read configuration [1/3] 123 | - [X] Read key map 124 | - [ ] Read general settings 125 | - [ ] Read scopes :: A mechanism for keeping configuration belonging to 126 | separate domains, for example language-specific configuration. 127 | 128 | *** TODO Merge several configuration files 129 | Usually there are several places for configuration files which are loaded 130 | in the order of priority. 131 | 132 | ** TODO Syntax highlighting 133 | The simplest possible one, just syntax tokens. Most probably implemented 134 | with an extension language. 135 | 136 | ** TODO Add extension language [1/3] 137 | 138 | *** DONE Decide on extension language 139 | CLOSED: [2022-02-07 Mon 21:49] 140 | - [ ] [[https://www.lua.org/][Lua]] 141 | - [ ] [[https://fennel-lang.org/][Fennel]] 142 | - [ ] [[http://synthcode.com/scheme/chibi/][Chibi Scheme]] 143 | - [X] [[https://janet-lang.org/][Janet]] 144 | 145 | *** TODO Implement event system 146 | Events are signals emitted by the core to which the extension language can 147 | subscribe and do desired actions. Event system is /de facto/ the way the 148 | extension language can know about anything happening in the editor. 149 | 150 | *** TODO Provide proof-of-concept implementations 151 | - [ ] Integration with [[https://github.com/junegunn/fzf][fzf]] 152 | - [ ] Syntax highlighting 153 | - [ ] Complex commands :: Most probably the "core" of the editor will only 154 | provide most basic commands for manipulating the state, and the extension 155 | language is then responsible for combining these simple commands into more 156 | complex interactions. 157 | 158 | ** TODO Integration with external tools [0/4] 159 | This will probably intersect with other sections such as extension language. 160 | 161 | *** TODO fzf 162 | 163 | *** TODO ripgrep 164 | 165 | *** TODO Linting 166 | 167 | *** TODO Formatting 168 | 169 | * TODO Stage 3: Polished working program [0%] 170 | :PROPERTIES: 171 | :COOKIE_DATA: todo recursive 172 | :END: 173 | 174 | This stage should result in a full working editor with basic features which 175 | work well and as expected. But it does not necessarily include all the nice 176 | convenience features which are generally expected in a modern text editor. 177 | 178 | ** TODO Write documentation 179 | ** TODO Finalize text buffer implementation [0/2] 180 | - [ ] Implement several variants 181 | - [ ] Benchmark them 182 | 183 | ** TODO Implement tower of highlighting 184 | Experimentation with different modes for highlighting and implementation of 185 | the "tower of highlighting". Only the basic structure should be implemented, 186 | not necessarily all the different semantic modes. 187 | 188 | ** TODO Decide on configuration handling 189 | - Do we need extension language? 190 | - If we keep extension language, do we need zzz file format? 191 | 192 | ** TODO Search and replace 193 | *** TODO Decide which search variations to implement 194 | - [ ] Exact 195 | - [ ] Exact with word boundaries 196 | - [ ] Regex 197 | - [ ] Exact case-insensitive 198 | - [ ] Camel-Kebab-Pascal-Snake-case-insensitive 199 | - [ ] PEG 200 | 201 | *** TODO Implement searching 202 | *** TODO Implement replacing 203 | 204 | * TODO Stage 4: Experimental convenience functionality [0%] 205 | :PROPERTIES: 206 | :COOKIE_DATA: todo recursive 207 | :END: 208 | 209 | This stage includes all the nice features which help the programmer program 210 | quicker, faster and easier. A lot of experimental features are expected to be 211 | here with the idea that they can be further refined, removed or changed. 212 | 213 | ** TODO Autocompletion 214 | 215 | ** TODO Clipboard integration 216 | 217 | ** TODO Jump inside file 218 | 219 | ** TODO Mouse integration 220 | 221 | ** TODO Multiple cursors 222 | 223 | ** TODO Language server protocol 224 | 225 | * TODO Stage 5: Refined convenience functionality [0%] 226 | 227 | This stage should complete and decide on all the functionality that should be 228 | included in the "core" of the editor, moved to third-party library or removed 229 | completely and left as the exercise for the reader. 230 | 231 | ** TODO Decide on the features 232 | 233 | * TODO Stage 6: Final release 234 | 235 | This stage should be a release of version 1.0. After all the stages have been 236 | implemented and tested, this stage will stabilize the features, API and all 237 | the other important things so that users can expect a seamless upgrade process 238 | of future versions. The next breaking version will be 2.0. 239 | 240 | -------------------------------------------------------------------------------- /not_good_enough/docs/assets/architecture.drawio: -------------------------------------------------------------------------------- 1 | 7R1Zd6O2+tfktPchOWwG+zHLNLNPWmfuTPvSo4AMNCBRkGN7fn0lFrNYtokNSM5kHiZGCCHpW/VtnOnX4fI2BpH3CTswONMUZ3mm35xpmmqoY/qHtayylrGqZA1u7Dt5p7Jh6v+AeWPRbe47MKl1JBgHxI/qjTZGCNqk1gbiGC/q3WY4qL81Ai7caJjaINhs/eY7xMtbVXNS3ngLfdfLXz3WrOxGCIrO+UoSDzh4UWnS35zp1zHGJPsVLq9hwDav2Jfsud+23F1PLIaItHng8+8TBS8nC0cx3k8/BE9f3n+JzvNVPIFgni84nyxZFTsAkXPJNpJeIYxo45VHwoBeqfQnfXe8+k4vlItRcfln9d4NwwRlfbXKrxyQeNDJO26upIA0iF1Idkx/lPWDTg2I+fpvIQ4hfSntsChBN8rB4VWAVrTFMADEf6qDHuQY5K6HW7/hDvt0xpqSY7sxycfJcd0aK/UhEjyPbZg/VQXVnoFUXasPlG3MxkD0R2XZZVOKCc/AioL4qlhAt3iaX+KYeNjFCARvytarGM+Rk8KUAbjs8xHjKAf0P5CQVU7jYE5wA5eWPqmgEr36s3KnRCR2sapiFQf/lD34l9AdJAVa2wFIEt8umn/zg2AfZmaQ3LWFLTFYb4nBrVGzhgvPBbxuiQS8WgF7iQT7AF8De4kFsgO+LesaBvAqRw6YAZ3/1QynnKnECPPfOS5unCcpTC9pB3UcLcub9JfL/l4HPkyf//Uv3/1fMSadYjZs1mkD5+oYtfB8AqcRSLd9QdWMOvZsBdUTjAlc7tzc4m6D32pKzm8rckPVOILDVPqCh7qxKa8c+HmEqLckREMqQtSPJ0STR4hf350xBM6GeoiL5l9diGAM0mkhAuMZpTJ5ydSok6muiSZTnQeu4em2d1oy2mozmlTEZBxPTAaPmJA9jxN6KtXotEwQMkxHD0mU9UjHTyKAit639x/Snsr05uO2/nR51Ud4w0BiX1xc7HmgQtl+GAUwpKCmG43RyRC1IZ6ox69K8JH8YtSSX5hSsYtRT0rwfQxQElGckJUGDV2g/vvvd1MNHpWl6yjvV3eKd6dOLs+Fqr/SkWCDPg6jSbMtTSp8XOmcBrmAF6I/leceq3rwUVpC/kJ7lu2zX5j+NdJc4+v3aPbZ+Ph1NAnntvPjXBuL5Kvm0XyV7g6Hr05hTHmcrEzVHMtmVCi0884F3GUUBb6d6pmyQqOpZo6Ka3HQMMWyukM4XV3GWcL1TL2toimXtdV4te4VhosYP8JrHOA43QZdSf8NgxMFB5AEKfS+jh+lDf4SraS1A+iabMY946SMe32QUkEh+0lJMrufspWUMqPZoVb0T8Clm7/NBCcZBYm3pBVo8TMqnE1oiFc4Det4aHCp4v30y+fzP+6u24KC7iBp8rgq88rDjGaUuTWaQOC7iLFByJxVtIHBgyJCcJnfCH3HSdkuD8B1FOgBxtwjnsKBsd4bjMevMO4WxiNLOhjzgvYyqHhqAZCP0IXIqUCrvFU0RodjQzc4dYWXzImmUPCcpRGcNoxI5lVTQhBFFIws2pP+F1BAZ15qikVzm70TgTB9eOHB9PEIU8XogU5jG4KumyPpNuIyC1TVFAcmduw/sPUQj/0/C6japyl4xpACzXAc7hZ6vEV2RplU27F95NKGUXl1n6qx59o2qYopqaWr0G88SrgQ9cKI1TqRqpuydh1TWaVRddQBkXLt2CMRR4XyhK8ddMTXnnXG56r8B/oL2x40LKGG7NH2c0VrBjDiMYA3T3TLbnxKUsT25DVpT8ayndFH2/Wd1ic9bogHC5fCKGCLJ8x9G/rkoNiNQ9//CNmroxgmSSYja8J8W+jHoW/rZs5ODFJhhdbii0ozCNGgG7fwACPEzYCYwlukEIpXbEtDmKsX7Y7ze8gr8UDE7oRLl2XCXDyAxLcvMEXMv6nuGuA52eDwjh9DOxXn+g0ESbrEwI/edngIrdPreFMsagZPLGp9iUVDrFjs3/DdoVA02wrFkVCPvehkkcNUHau/YOXegM/d/olIhcjsQCHinoiucRhSkZeciiYk3rlvCXFOHOdOfl7OYI0IiyNphQKzJ5xNMqWNPTtKimzQUwuQNrebko470HyAqwcMYicLkW5oYiGeJ9vtNIIpuxkoYlqbStPAlC1RtMBPR9lWWyVMMheo1Zep4nQpu+mtkYCyeQkqjV2qHzGzCgLp4bHIoGcX7X0mzWNqViNBy/6yYVOavDDZT7ZvRvOgihgvYWd5TGwvf333TpeGcrUJKIsDp1F/gOLF5kgOKMra+geU2VSDhUPq+GBjvl3pFrKHHUBAYY1TcnsXswxmbqrM6PU1Sc1MB8QoyOE0jXGWMkYvGfA6wZO16byITSkU4aopiuehMXpDlA6iIXYgik27xjjLq43mZG3CpduYJBykqceylxL1FXXOVXU/7ug8D3wXuMO1uQiO3D4sScUSlaRijVuq0GLtmJrYY1YVpG1zztQqRGXMOeOBnm9EtgYC/c5pdm7GfIfOP8EQsyk2GftdMHd9dDjDF23xNMbCLZ4dGK9eY91qJ+RmHrDFgXFfsW7cxEVeXaLG7icLPwxAaTPKWW3t6NWTO5UT0rvO+qspIs1CcJxacn/QgxtALgtgW5+C9fo52OKENfHK2TX1HhBQ5EKAwCuGMMkGUJ5XMY4LJ4n8ugOYKJ+TyrYLrfcXE2ubtK0Ksz5y1ye0jNyBCnKPulSv+MAdUK6ygmoHRhmupGYhcwFYffMRs7cNozntlFOtRUnDxKlzWHtv2hR3BTIFrPZ0phVEs22LZ0nGw4vpvMr0/vHhFHh4B8XUXhgPV03ZmLhgw+TLZeJjqy0TF1Zpgr8+eYya7dChT5Nmr9hwAix83FcC8OmycF2RjYVLFU79oni4yikMcwJUq/ZWlhIuydV8Nhss36yjk3MjQFMdj4cjWT5f1Y6H0L4kNDtLkgYPLJFow2mUJz2tK/0kELYOnR80r0nJf///bJvToiO+3kCS9Vd4qkjCi0wqkKl7JOmgsvtxSJK6qRjXYlmFMK8LfQyW9IsaySMsA9dqEXUBeIDBHU78fKAN79nHRoe1F629vy19B5WkDowbPrshtBK1rpWoCofFFTGJVey1OsBe7hImYpWSIcqyClJKtNNUSjoQeS9JKRkJ1El2oUvn8PmG48ck3d5TAo9q1BnqyBIMHrVPbeARpUVjtqgBkG7biniskMur7JdP9msjvSH7N1HV5KCq1ReqamINElXB37oW/yn4gczWJmVhX8ng48PPlY4oIT4I+2IZHx80kfhw2Lc6XhQ+FGZ9SY4GZl9ehukKEbDcVGjeUiEYMEF4YmeGpvvYGNL3cHnlf/n9Lfju/fhbd+fqP1fx03TXqa4sHzklgFS1/0MLS3LV13xwxU/SMWLfJpllC6OZj9KCkD5KfCftk2ZV5YeRPLsKwazGE0ge2c0ZTi9Qoe8qsxiHZ0XOFVVS084LHAfOBf3xJTOjlcVHiqKUxYyYZrsoy1LaHkBuNpX1DNw0/atWq7HQxGk39pZ7VrHyFzYcYoNg4mWzAOzdxQbYaQ0sm85kjlgWIUzxgqIF58twZ436WsV62cvepVmLGCboF5LtC29bMtWcLf4622ECgZNxjFXWI11HZWOyIWo7WJsvXfi+b9WdepXKbriAwSvKxYux17qoVckle01s7N8hEly2MAH+vratHSFOgu+cd+cS/I6laZySiB4pdbveoOEBfNjwRPTwxCuIoNRNgtqJwbIQFC+NqQuCugcPp01PE3M4euImnupCRV/dsSVVneYd3xGU6SOSO6fZuffjA1yFIJI1Rdea1KlrMqCw4mcF9lVebjp/KArln3bBjA6griqNL1QYhYFwTxGeLspj8FOJ+qo99hkTf7b66SA8aVRZMrRNAPPygHur4txX4n3NtDRISYQNIHBAtRUu40Y++KDl8PllNIR+ul6WUpGHKTz8DdXaKjwD6Ts7Z9lDGWY0893TocjmN5cloEjBkXOSeUu7IUqOZ0xobXR+PamfLZFjAG7cFvAZQxTGjvvygL5ZEoiSzKd0effudPhyU1Pqs3gUvYwx27b1vVsWofYJO5D1+A8= -------------------------------------------------------------------------------- /not_good_enough/docs/assets/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenfork/kisa/68f344bd2fc28ccdd850dec14859a1995f8cb97f/not_good_enough/docs/assets/architecture.png -------------------------------------------------------------------------------- /not_good_enough/docs/assets/styles.css: -------------------------------------------------------------------------------- 1 | .tok-string { color: green } 2 | .tok-object-key { color: sienna } 3 | .tok-value { color: crimson } 4 | .tok-comma { color: sienna } 5 | .tok-boundary-0 { color: #ef476f } 6 | .tok-boundary-1 { color: #ffd166 } 7 | .tok-boundary-2 { color: #06d6a0 } 8 | .tok-boundary-3 { color: #118ab2 } 9 | .tok-boundary-4 { color: #073b4c } 10 | .tok-boundary-5 { color: #ef476f } 11 | .tok-boundary-6 { color: #ffd166 } 12 | .tok-boundary-7 { color: #06d6a0 } 13 | .tok-boundary-8 { color: #118ab2 } 14 | -------------------------------------------------------------------------------- /not_good_enough/docs/colorize.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import strutils 3 | import jsconsole 4 | from strutils import join 5 | 6 | type 7 | ParserState = enum 8 | Value 9 | String 10 | Number 11 | Array 12 | Object 13 | ObjectKey 14 | TokenKind = enum 15 | tkBoundary = "boundary" 16 | tkString = "string" 17 | tkValue = "value" 18 | tkComma = "comma" 19 | tkObjectKey = "object-key" 20 | 21 | func spanStart(class: TokenKind): string = fmt"""""" 22 | func spanEnd(): string = "" 23 | func span(str: string, class: TokenKind): string = spanStart(class) & str & spanEnd() 24 | func span(ch: char, class: TokenKind): string = ($ch).span(class) 25 | func spanBoundary(ch: char, stackLevel: int): string = 26 | fmt"""{ch}""" 27 | 28 | func logError(args: varargs[string, `$`]) = 29 | when not defined(release): 30 | {.cast(noSideEffect).}: 31 | console.error args.join(" ") 32 | 33 | func colorizeJson*(str: string): string = 34 | var states: seq[ParserState] 35 | states.add Value 36 | var cnt = 0 37 | var stackLevel = 0 38 | 39 | proc skipSpaces() = 40 | while cnt < str.len and str[cnt].isSpaceAscii(): 41 | result.add str[cnt] 42 | cnt.inc 43 | proc expectNext(ch: char) = 44 | cnt.inc 45 | skipSpaces() 46 | if str[cnt] != ch: logError "unexpected: ", str[cnt], " != ", ch 47 | proc expectNext(s: string) = 48 | for ch in s: 49 | cnt.inc 50 | if str[cnt] != ch: logError "unexpected: ", str[cnt], " != ", ch 51 | 52 | while cnt < str.len: 53 | defer: cnt.inc 54 | skipSpaces() 55 | 56 | case states[^1]: 57 | of Value: 58 | if str[cnt] == ',': 59 | result &= span(',', tkComma) 60 | discard states.pop() 61 | elif str[cnt] in [']', '}']: 62 | cnt.dec 63 | discard states.pop() 64 | elif str[cnt] == '{': 65 | result &= spanBoundary('{', stackLevel) 66 | stackLevel.inc 67 | states.add Object 68 | elif str[cnt] == '[': 69 | result &= spanBoundary('[', stackLevel) 70 | stackLevel.inc 71 | states.add Array 72 | elif str[cnt] == '"': 73 | result &= spanStart(tkString) 74 | result.add '"' 75 | states.add String 76 | elif str[cnt] == 't': 77 | expectNext("rue") 78 | result &= span("true", tkValue) 79 | elif str[cnt] == 'f': 80 | expectNext("alse") 81 | result &= span("false", tkValue) 82 | elif str[cnt] == 'n': 83 | expectNext("ull") 84 | result &= span("null", tkValue) 85 | elif str[cnt].isDigit(): 86 | cnt.dec 87 | result &= spanStart(tkValue) 88 | states.add Number 89 | else: 90 | logError "unexpected Value character: ", str[cnt].repr 91 | discard 92 | of String: 93 | if str[cnt] == '"': 94 | result.add '"' 95 | result &= spanEnd() 96 | discard states.pop() 97 | else: 98 | result.add str[cnt] 99 | of Number: 100 | if str[cnt].isDigit() or str[cnt] == '.': 101 | result.add str[cnt] 102 | else: 103 | cnt.dec 104 | result &= spanEnd() 105 | discard states.pop() 106 | of Array: 107 | if str[cnt] == ']': 108 | stackLevel.dec 109 | result &= spanBoundary(']', stackLevel) 110 | discard states.pop() 111 | else: 112 | cnt.dec 113 | states.add Value 114 | of Object: 115 | if str[cnt] == '}': 116 | result &= spanEnd() 117 | stackLevel.dec 118 | result &= spanBoundary('}', stackLevel) 119 | discard states.pop() 120 | elif str[cnt] == '"': 121 | result &= spanStart(tkObjectKey) 122 | result.add '"' 123 | states.add ObjectKey 124 | else: 125 | logError "unexpected Object character: ", str[cnt].repr 126 | discard 127 | of ObjectKey: 128 | if str[cnt] == '"': 129 | result.add '"' 130 | result &= spanEnd() 131 | expectNext(':') 132 | result &= span(':', tkBoundary) 133 | discard states.pop() 134 | states.add Value 135 | else: 136 | result.add str[cnt] 137 | if states.len > 1 or states[0] != Value: 138 | logError "unexpected end: ", states.repr 139 | -------------------------------------------------------------------------------- /not_good_enough/docs/docs.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "Dmitry Matveyev" 5 | description = "A new awesome nimble package" 6 | license = "MIT" 7 | srcDir = "." 8 | bin = @["index.js"] 9 | backend = "js" 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.4.8" 14 | requires "karax#head" 15 | requires "regex" 16 | 17 | task release, "build site in release mode for most space efficiency": 18 | exec "nim r increase_assets_version.nim" 19 | exec "nim js -d:release -d:danger index.nim" 20 | # yarn global install uglify-js 21 | exec "uglifyjs index.js -m -c -o index.js" 22 | -------------------------------------------------------------------------------- /not_good_enough/docs/increase_assets_version.nim: -------------------------------------------------------------------------------- 1 | from strutils import parseInt, find 2 | 3 | const filesData = [ 4 | ("index.html", "index.js?v=", '"'), 5 | ("index.html", "styles.css?v=", '"'), 6 | ] 7 | 8 | for (filename, startText, endText) in filesData: 9 | let text = readFile(filename) 10 | let startIndex = text.find(startText) 11 | var numberStr: string 12 | 13 | for ch in text[startIndex + startText.len..^1]: 14 | if ch == endText: break 15 | else: numberStr.add ch 16 | 17 | let number = parseInt(numberStr) 18 | 19 | let newText = 20 | text[0..startIndex + startText.len - 1] & 21 | $(number + 1) & 22 | text[startIndex + startText.len + numberStr.len..^1] 23 | 24 | writeFile(filename, newText) 25 | -------------------------------------------------------------------------------- /not_good_enough/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Kisa API docs 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /not_good_enough/docs/index.nim: -------------------------------------------------------------------------------- 1 | include karax/prelude 2 | import ./jsonrpc_schema 3 | from ./colorize import colorizeJson 4 | from ./utils import sanitizeHtml, parameterize 5 | from strutils import replace 6 | 7 | proc renderStepCode(step: Step): string = 8 | case step.kind 9 | of skOther: 10 | result = step.other.sanitizeHtml 11 | of skRequest: 12 | var toTarget = 13 | case step.to 14 | of tkServer: " To Server --> " 15 | of tkClient: " To Client --> " 16 | result = toTarget & 17 | step.request.toCode(step.pretty, 1).sanitizeHtml.colorizeJson 18 | of skResponse: 19 | var fromTarget = 20 | case step.`from` 21 | of tkServer: "From Server <-- " 22 | of tkClient: "From Client <-- " 23 | result = fromTarget & 24 | step.success.toCode(step.pretty, 1).sanitizeHtml.colorizeJson 25 | for error in step.errors: 26 | result &= 27 | "\n" & 28 | fromTarget & 29 | error.toCode(step.pretty, 1).sanitizeHtml.colorizeJson 30 | 31 | func replaceLinks(str: string, dataReferences: seq[DataReference]): string = 32 | ## Replaces formatted string "%name%" with an anchor with href="#name". 33 | result = str 34 | for dataReference in dataReferences: 35 | result = result.replace(dataReference.title.linkFormat, dataReference.anchor) 36 | 37 | proc createDom(): VNode = 38 | buildHtml(tdiv): 39 | h1: 40 | text "Kisa API documentation" 41 | for interaction in interactions: 42 | h2: 43 | text interaction.title 44 | p: 45 | verbatim interaction.description 46 | ol: 47 | for step in interaction.steps: 48 | li: 49 | p: 50 | verbatim step.description 51 | pre: 52 | code: 53 | verbatim renderStepCode(step).replaceLinks(dataReferences) 54 | h2: 55 | text "Data references" 56 | for dataReference in dataReferences: 57 | h3(id = dataReference.title.parameterize): 58 | text dataReference.title 59 | p: 60 | verbatim dataReference.description 61 | pre: 62 | code: 63 | verbatim dataReference.data.toCode(true, 1).sanitizeHtml.colorizeJson 64 | 65 | setRenderer createDom 66 | -------------------------------------------------------------------------------- /not_good_enough/docs/jsonrpc_schema.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import tables 3 | from strutils import repeat 4 | from sequtils import mapIt 5 | from ./poormanmarkdown import markdown 6 | from ./utils import parameterize 7 | 8 | # Partial JSON-RPC 2.0 specification in types. 9 | type 10 | ParamKind* = enum 11 | pkVoid, pkNull, pkBool, pkInteger, pkFloat, pkString, pkArray, pkObject, 12 | pkReference 13 | Parameter* = object 14 | case kind*: ParamKind 15 | of pkVoid: vVal*: bool 16 | of pkNull: nVal*: bool 17 | of pkBool: bVal*: bool 18 | of pkInteger: iVal*: int 19 | of pkFloat: fVal*: float 20 | of pkString: sVal*: string 21 | of pkArray: aVal*: seq[Parameter] 22 | of pkObject: oVal*: seq[(string, Parameter)] 23 | of pkReference: rVal*: string 24 | Request* = object 25 | id: int 26 | `method`*: string 27 | params*: Parameter 28 | notification*: bool ## notifications in json-rpc don't have an `id` element 29 | Error* = object 30 | code*: int 31 | message*: string 32 | data*: Parameter 33 | ResponseKind* = enum 34 | rkResult, rkError 35 | Response* = object 36 | id: int 37 | notification: bool ## notifications in json-rpc don't have an `id` element 38 | case kind*: ResponseKind 39 | of rkResult: 40 | result*: Parameter 41 | of rkError: 42 | error*: Error 43 | StepKind* = enum 44 | skRequest, skResponse, skOther 45 | TargetKind* = enum 46 | tkClient, tkServer 47 | Step* = object 48 | description*: string 49 | pretty*: bool 50 | case kind*: StepKind 51 | of skRequest: 52 | request*: Request 53 | to*: TargetKind 54 | of skResponse: 55 | success*: Response 56 | errors*: seq[Response] 57 | `from`*: TargetKind 58 | of skOther: 59 | other*: string 60 | Interaction* = object 61 | title*: string 62 | description*: string 63 | steps*: seq[Step] 64 | FaceAttribute* = enum 65 | underline, reverse, bold, blink, dim, italic 66 | DataReference* = object 67 | title*: string 68 | description*: string 69 | data*: Parameter 70 | 71 | func toParam(val: string): Parameter = Parameter(kind: pkString, sVal: val) 72 | func toParam(val: bool): Parameter = Parameter(kind: pkBool, bVal: val) 73 | func toParam(val: int): Parameter = Parameter(kind: pkInteger, iVal: val) 74 | func toParam(val: float): Parameter = Parameter(kind: pkFloat, fVal: val) 75 | func toParam(val: FaceAttribute): Parameter = ($val).toParam 76 | func toParam[T](val: openArray[T]): Parameter = 77 | Parameter(kind: pkArray, aVal: val.mapIt(it.toParam)) 78 | func toParam(val: Parameter): Parameter = val 79 | 80 | func quoteString(str: string): string = 81 | for ch in str: 82 | if ch == '"': 83 | result.add '\\' 84 | result.add '"' 85 | else: 86 | result.add ch 87 | 88 | const spacesPerIndentationLevel = 4 89 | 90 | func linkFormat*(str: string): string = fmt""""%{str.parameterize}%"""" 91 | func anchor*(dr: DataReference): string = 92 | let name = dr.title.parameterize 93 | result = fmt"""{name}""" 94 | 95 | func toCode*(p: Parameter, pretty = false, 96 | indentationLevel: Natural = 0): string = 97 | case p.kind 98 | of pkVoid: assert false 99 | of pkNull: result = "null" 100 | of pkBool: result = $p.bVal 101 | of pkInteger: result = $p.iVal 102 | of pkFloat: result = $p.fVal 103 | of pkString: result = fmt""""{quoteString(p.sVal)}"""" 104 | of pkReference: result = linkFormat(p.rVal) 105 | of pkArray: 106 | result &= "[" 107 | if p.aVal.len > 0: 108 | if pretty: result &= "\n" 109 | for idx, parameter in p.aVal: 110 | if idx != 0: result &= (if (pretty): ",\n" else: ", ") 111 | if pretty: result &= repeat(' ', indentationLevel * spacesPerIndentationLevel) 112 | result &= parameter.toCode(pretty, indentationLevel + 1) 113 | if pretty: result &= "\n" 114 | if pretty: result &= repeat(' ', (indentationLevel - 1) * spacesPerIndentationLevel) 115 | result &= "]" 116 | of pkObject: 117 | result &= "{" 118 | if p.oVal.len > 0: 119 | if pretty: result &= "\n" 120 | for idx, (key, parameter) in p.oVal: 121 | if idx != 0: result &= (if (pretty): ",\n" else: ", ") 122 | if pretty: result &= repeat(' ', indentationLevel * spacesPerIndentationLevel) 123 | result &= fmt""""{key}": {parameter.toCode(pretty, indentationLevel + 1)}""" 124 | if pretty: result &= "\n" 125 | if pretty: result &= repeat(' ', (indentationLevel - 1) * spacesPerIndentationLevel) 126 | result &= "}" 127 | 128 | func toCode*(r: Request, pretty = false, 129 | indentationLevel: Natural = 0): string = 130 | assert r.params.kind in [pkArray, pkObject, pkVoid] # as per specification 131 | 132 | var rs = Parameter( 133 | kind: pkObject, 134 | oVal: @[("jsonrpc", "2.0".toParam)] 135 | ) 136 | if not r.notification: 137 | rs.oVal.add ("id", r.id.toParam) 138 | rs.oVal.add ("method", r.`method`.toParam) 139 | if r.params.kind != pkVoid: 140 | rs.oVal.add ("params", r.params) 141 | result = rs.toCode(pretty, indentationLevel) 142 | 143 | func toCode*(r: Response, pretty = false, 144 | indentationLevel: Natural = 0): string = 145 | var rs = Parameter( 146 | kind: pkObject, 147 | oVal: @[("jsonrpc", "2.0".toParam)] 148 | ) 149 | if not r.notification: 150 | rs.oVal.add ("id", r.id.toParam) 151 | case r.kind 152 | of rkResult: 153 | rs.oVal.add ("result", r.result) 154 | of rkError: 155 | rs.oVal.add ("code", r.error.code.toParam) 156 | rs.oVal.add ("message", r.error.message.toParam) 157 | if r.error.data.kind != pkVoid: 158 | rs.oVal.add ("data", r.error.data) 159 | result = rs.toCode(pretty, indentationLevel) 160 | 161 | func faceParam(fg: string, bg: string, attributes: openArray[FaceAttribute] = []): Parameter = 162 | result = Parameter(kind: pkObject) 163 | result.oVal.add ("fg", fg.toParam) 164 | result.oVal.add ("bg", bg.toParam) 165 | result.oVal.add ("attributes", attributes.toParam) 166 | 167 | func refParam(anchor: string): Parameter = 168 | Parameter( 169 | kind: pkReference, 170 | rVal: anchor.parameterize, 171 | ) 172 | 173 | func req[T](met: string, param: T = Parameter(kind: pkVoid), id: int = 1): Request = 174 | Request( 175 | `method`: met, 176 | params: param.toParam, 177 | notification: false, 178 | id: id, 179 | ) 180 | 181 | func notif[T](met: string, param: T = Parameter(kind: pkVoid)): Request = 182 | Request( 183 | `method`: met, 184 | params: param.toParam, 185 | notification: true, 186 | ) 187 | 188 | func res[T](rs: T, id: int = 1): Response = 189 | Response( 190 | kind: rkResult, 191 | result: rs.toParam, 192 | id: id, 193 | ) 194 | 195 | func err(code: int, message: string, id: int = 1): Response = 196 | Response( 197 | kind: rkError, 198 | error: Error( 199 | code: code, 200 | message: message, 201 | ), 202 | id: id, 203 | ) 204 | 205 | # Construction of all the structures must happen at compile time so that we get 206 | # faster run time and less JavaScript bundle size generated from this file. 207 | const interactions* = block: 208 | var interactions: seq[Interaction] 209 | 210 | interactions.add( 211 | Interaction( 212 | title: "Initialize a client", 213 | description: """ 214 | This is the very first thing the client should do. 215 | Client connects to the server and receives a confirmation. 216 | """, 217 | steps: @[ 218 | Step( 219 | kind: skOther, 220 | description: """ 221 | The first thing to do is to send a connection request to the unix domain socket 222 | of type seqpacket which is located at the user runtime directory inside `kisa` 223 | directory with an of a currently running session (by convention it is the 224 | process ID of the running server). Below is an example for a Zig language, see 225 | the documentation of [socket(2)] and [connect(2)] for more information. 226 | 227 | [socket(2)]: https://linux.die.net/man/2/socket 228 | [connect(2)]: https://linux.die.net/man/2/connect 229 | """, 230 | other: """ 231 | const std = @import("std"); 232 | const os = std.os; 233 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 234 | var allocator = &gpa.allocator; 235 | const address = try allocator.create(std.net.Address); 236 | address.* = try std.net.Address.initUnix("/var/run/user/1000/kisa/"); 237 | const socket = try os.socket( 238 | os.AF_UNIX, 239 | os.SOCK_SEQPACKET | os.SOCK_CLOEXEC, 240 | os.PF_UNIX, 241 | ); 242 | os.connect(socket, &address.any, address.getOsSockLen()); 243 | """ 244 | ), 245 | Step( 246 | kind: skRequest, 247 | description: """ 248 | After that the server notifies the client that the connection was accepted. 249 | """, 250 | to: tkClient, 251 | request: notif("connected"), 252 | ), 253 | ] 254 | ) 255 | ) 256 | 257 | interactions.add( 258 | Interaction( 259 | title: "Deinitialize a client", 260 | description: """ 261 | This is the very last thing the client should do. 262 | Client notifies the server that it is going to be deinitialized. If this is 263 | the last client of the server, the server quits. 264 | """, 265 | steps: @[ 266 | Step( 267 | kind: skRequest, 268 | description: "Client sends a notification that the client quits.", 269 | to: tkServer, 270 | request: notif("quitted") 271 | ), 272 | ] 273 | ) 274 | ) 275 | 276 | interactions.add( 277 | Interaction( 278 | title: "Open a file", 279 | description: """ 280 | Client asks the server to open a file and get data to display it on the screen. 281 | """, 282 | steps: @[ 283 | Step( 284 | kind: skRequest, 285 | description: "Client sends an absolute path to file.", 286 | to: tkServer, 287 | request: req("openFile", ["/home/grfork/reps/kisa/kisarc.zzz"]), 288 | ), 289 | Step( 290 | kind: skResponse, 291 | description: """ 292 | Server responds with `true` on success or with error description. 293 | """, 294 | `from`: tkServer, 295 | success: res(true), 296 | errors: @[ 297 | err(1, "Operation not permitted"), 298 | err(2, "No such file or directory"), 299 | err(12, "Cannot allocate memory"), 300 | err(13, "Permission denied"), 301 | err(16, "Device or resource busy"), 302 | err(17, "File exists"), 303 | err(19, "No such device"), 304 | err(20, "Not a directory"), 305 | err(21, "Is a directory"), 306 | err(23, "Too many open files in system"), 307 | err(24, "Too many open files"), 308 | err(27, "File too large"), 309 | err(28, "No space left on device"), 310 | err(36, "File name too long"), 311 | err(40, "Too many levels of symbolic links"), 312 | err(75, "Value too large for defined data type"), 313 | ], 314 | ), 315 | Step( 316 | kind: skRequest, 317 | description: """ 318 | Client asks to receive the data to draw re-sending same file path. 319 | """, 320 | to: tkServer, 321 | request: req("sendDrawData", ["/home/grfork/reps/kisa/kisarc.zzz"], 2), 322 | ), 323 | Step( 324 | kind: skResponse, 325 | description: """ 326 | Server responds with `true` on success or with error description. 327 | """, 328 | `from`: tkServer, 329 | success: res(refParam("data-to-draw"), 2), 330 | ), 331 | ] 332 | ) 333 | ) 334 | 335 | var links: Table[string, string] 336 | for interaction in interactions.mitems: 337 | interaction.description = markdown(interaction.description, links) 338 | for step in interaction.steps.mitems: 339 | step.description = markdown(step.description, links) 340 | 341 | interactions 342 | 343 | const dataReferences* = block: 344 | var dataReferences: seq[DataReference] 345 | 346 | dataReferences.add( 347 | DataReference( 348 | title: "Data to draw", 349 | description: """ 350 | On many occasions the client will receive the data that should be drawn on the 351 | screen. For now it copies what Kakoune does and will be changed in future. 352 | """, 353 | data: Parameter( 354 | kind: pkArray, 355 | aVal: @[ 356 | # Lines 357 | Parameter( 358 | kind: pkArray, 359 | aVal: @[ 360 | # Line 1 361 | Parameter( 362 | kind: pkArray, 363 | aVal: @[ 364 | Parameter( 365 | kind: pkObject, 366 | oVal: @[ 367 | ("contents", " 1 ".toParam), 368 | ("face", faceParam("#fcfcfc", "#fedcdc", [reverse, bold])) 369 | ] 370 | ), 371 | Parameter( 372 | kind: pkObject, 373 | oVal: @[ 374 | ("contents", "my first string".toParam), 375 | ("face", faceParam("default", "default")) 376 | ] 377 | ), 378 | Parameter( 379 | kind: pkObject, 380 | oVal: @[ 381 | ("contents", " and more".toParam), 382 | ("face", faceParam("red", "black", [italic])) 383 | ] 384 | ), 385 | ] 386 | ), 387 | # Line 2 388 | Parameter( 389 | kind: pkArray, 390 | aVal: @[ 391 | Parameter( 392 | kind: pkObject, 393 | oVal: @[ 394 | ("contents", " 2 ".toParam), 395 | ("face", faceParam("#fcfcfc", "#fedcdc", [reverse, bold])) 396 | ] 397 | ), 398 | Parameter( 399 | kind: pkObject, 400 | oVal: @[ 401 | ("contents", "next line".toParam), 402 | ("face", faceParam("red", "black", [italic])) 403 | ] 404 | ), 405 | ] 406 | ), 407 | ] 408 | ), 409 | # Cursors 410 | Parameter( 411 | kind: pkArray, 412 | aVal: @[ 413 | faceParam("default", "default", []), 414 | faceParam("blue", "black", []) 415 | ] 416 | ) 417 | ] 418 | ) 419 | ) 420 | ) 421 | 422 | dataReferences 423 | -------------------------------------------------------------------------------- /not_good_enough/docs/nim.cfg: -------------------------------------------------------------------------------- 1 | --gc:orc 2 | -------------------------------------------------------------------------------- /not_good_enough/docs/poormanmarkdown.nim: -------------------------------------------------------------------------------- 1 | ## Poor man's markdown parser. 2 | 3 | import tables 4 | from regex import re, findAll, groupFirstCapture, replace 5 | from strutils import multiReplace 6 | import strformat 7 | from ./utils import sanitizeHtml 8 | 9 | const linkDefinitionRe = re"(?m)^(\[.+?\]): (.+)$" 10 | 11 | func quoteString(str: string): string = 12 | for ch in str: 13 | if ch != '"': 14 | result.add ch 15 | 16 | func htmlLink(text: string, url: string): string = 17 | fmt"""{text}""" 18 | 19 | func toReplacementGroups(table: Table[string, string]): seq[(string, string)] = 20 | for key, url in table: 21 | result.add (key, htmlLink(key[1..^2], url)) 22 | 23 | func inlineCodeReplacementGroups(str: string): seq[(string, string)] = 24 | const inlineCodeRe = re"(?s)`(.+?)`" 25 | for m in findAll(str, inlineCodeRe): 26 | result.add (str[m.boundaries], "" & m.groupFirstCapture(0, str) & "") 27 | 28 | ## `links` must be initialized and passed on every parse call. It is modified 29 | ## inside a proc and same `links` can be passed to further `markdown` calls. 30 | func markdown*(str: string, links: var Table[string, string]): string = 31 | for m in findAll(str, linkDefinitionRe): 32 | links[m.groupFirstCapture(0, str)] = m.groupFirstCapture(1, str) 33 | result = str.replace(linkDefinitionRe, "") 34 | result = result.sanitizeHtml() 35 | result = result.multiReplace(inlineCodeReplacementGroups(result)) 36 | result = result.multiReplace(links.toReplacementGroups) 37 | -------------------------------------------------------------------------------- /not_good_enough/docs/utils.nim: -------------------------------------------------------------------------------- 1 | from strutils import multiReplace, toLowerAscii 2 | 3 | func sanitizeHtml*(str: string): string = 4 | str.multiReplace( 5 | ("<", "<"), 6 | (">", ">"), 7 | ) 8 | 9 | func parameterize*(str: string): string = 10 | for ch in str: 11 | if ch == ' ': 12 | result.add '-' 13 | elif ch in {'a'..'z', 'A'..'Z', '-', '_'}: 14 | result.add ch.toLowerAscii 15 | else: 16 | assert false 17 | -------------------------------------------------------------------------------- /not_good_enough/kisarc.zzz: -------------------------------------------------------------------------------- 1 | keymap: 2 | normal: 3 | h: cursor_move_left 4 | j: cursor_move_down 5 | k: cursor_move_up 6 | l: cursor_move_right 7 | n: 8 | cursor_move_down 9 | cursor_move_right 10 | default: nop 11 | insert: 12 | default: insert_character 13 | ctrl-alt-c: quit 14 | ctrl-s: save 15 | shift-d: delete_word 16 | arrow_up: cursor_move_up 17 | super-arrow_up: cursor_move_up 18 | -------------------------------------------------------------------------------- /not_good_enough/src/highlight.zig: -------------------------------------------------------------------------------- 1 | //! Here we always assume that data provided to functions is correct, for example kisa.Selection 2 | //! has offsets less than buffer length. 3 | const std = @import("std"); 4 | const testing = std.testing; 5 | const kisa = @import("kisa"); 6 | const rb = @import("rb.zig"); 7 | const assert = std.debug.assert; 8 | 9 | const Highlight = @This(); 10 | const cursor_style = kisa.Style{ 11 | .foreground = .{ .base16 = .black }, 12 | .background = .{ .base16 = .white }, 13 | }; 14 | const selection_style = kisa.Style{ 15 | .foreground = .{ .base16 = .white }, 16 | .background = .{ .base16 = .blue }, 17 | }; 18 | 19 | ally: std.mem.Allocator, 20 | 21 | /// A red-black tree of non-intersecting segments which are basically ranges with start and end. 22 | segments: rb.Tree, 23 | 24 | // TODO: include here some Options from kisa.DrawData such as active_line_number. 25 | active_line_number: u32 = 0, 26 | 27 | pub const Segment = struct { 28 | /// Part of a red-black tree. 29 | node: rb.Node = undefined, 30 | /// Inclusive. 31 | start: usize, 32 | /// Exclusive. 33 | end: usize, 34 | /// Color and style for the specified range in the text buffer. 35 | style: kisa.Style, 36 | }; 37 | 38 | fn segmentsCompare(l: *rb.Node, r: *rb.Node, _: *rb.Tree) std.math.Order { 39 | const left = @fieldParentPtr(Segment, "node", l); 40 | const right = @fieldParentPtr(Segment, "node", r); 41 | assert(left.start < left.end); 42 | assert(right.start < right.end); 43 | 44 | // Intersecting segments are considered "equal". Current implementation of a red-black tree 45 | // does not allow duplicates, this insures that our tree will never have any intersecting 46 | // segments. 47 | if (left.end <= right.start) { 48 | return .lt; 49 | } else if (left.start >= right.end) { 50 | return .gt; 51 | } else { 52 | return .eq; 53 | } 54 | } 55 | 56 | pub fn init(ally: std.mem.Allocator) Highlight { 57 | return .{ .segments = rb.Tree.init(segmentsCompare), .ally = ally }; 58 | } 59 | 60 | pub fn deinit(self: Highlight) void { 61 | var node = self.segments.first(); 62 | while (node) |n| { 63 | node = n.next(); 64 | self.ally.destroy(n); 65 | } 66 | } 67 | 68 | // Examples of common cases of segment overlapping: 69 | // Let A be a segment: start=3, end=7 - base segment. 70 | // Let B be a segment: start=8, end=10 - not overlapping. 71 | // Let C be a segment: start=1, end=4 - overlapping from the start. 72 | // Let D be a segment: start=6, end=10 - overlapping from the end. 73 | // Let E be a segment: start=3, end=7 - complete overlapping. 74 | // Let F be a segment: start=4, end=6 - overlapping in the middle. 75 | pub fn addSegment(self: *Highlight, s: Segment) !void { 76 | if (s.start >= s.end) return error.StartMustBeLessThanEnd; 77 | 78 | var segment = try self.ally.create(Segment); 79 | errdefer self.ally.destroy(segment); 80 | segment.* = s; 81 | 82 | // insert returns a duplicated node if there's one, inserts the value otherwise. 83 | while (self.segments.insert(&segment.node)) |duplicated_node| { 84 | var duplicated_segment = @fieldParentPtr(Segment, "node", duplicated_node); 85 | 86 | if (segment.start <= duplicated_segment.start and segment.end >= duplicated_segment.end) { 87 | std.debug.print("A-E\n", .{}); 88 | std.debug.print("segment: {d} - {d}, dup: {d} - {d}\n", .{ segment.start, segment.end, duplicated_segment.start, duplicated_segment.end }); 89 | // A and E - complete overlapping. 90 | self.segments.remove(&duplicated_segment.node); 91 | self.ally.destroy(duplicated_segment); 92 | } else if (segment.start <= duplicated_segment.start and segment.end > duplicated_segment.start) { 93 | std.debug.print("A-C\n", .{}); 94 | std.debug.print("segment: {d} - {d}, dup: {d} - {d}\n", .{ segment.start, segment.end, duplicated_segment.start, duplicated_segment.end }); 95 | // A and C - overlapping from the start. 96 | duplicated_segment.start = segment.end; 97 | } else if (segment.start < duplicated_segment.end and segment.end >= duplicated_segment.end) { 98 | std.debug.print("A-D\n", .{}); 99 | // A and D - overlapping from the end. 100 | duplicated_segment.end = segment.start; 101 | } else if (segment.start > duplicated_segment.start and segment.end < duplicated_segment.end) { 102 | std.debug.print("A-F\n", .{}); 103 | // A and F - overlapping in the middle. 104 | 105 | // First half is the modified duplicated segment. 106 | duplicated_segment.end = segment.start; 107 | 108 | var second_half_segment = try self.ally.create(Segment); 109 | errdefer self.ally.destroy(second_half_segment); 110 | second_half_segment.* = .{ 111 | .start = segment.end, 112 | .end = duplicated_segment.end, 113 | .style = duplicated_segment.style, 114 | }; 115 | assert(self.segments.insert(&second_half_segment.node) != null); 116 | } 117 | } 118 | // At this point we should have resolved all possible overlapping scenarios. 119 | assert(self.segments.insert(&segment.node) != null); 120 | } 121 | 122 | pub fn addSelection(highlight: *Highlight, s: kisa.Selection) !void { 123 | if (s.primary) highlight.active_line_number = s.cursor.line; 124 | if (s.cursor.offset > s.anchor.offset) { 125 | try highlight.addSegment(.{ 126 | .start = s.anchor.offset, 127 | .end = s.cursor.offset, 128 | .style = selection_style, 129 | }); 130 | } 131 | try highlight.addSegment(.{ 132 | .start = s.cursor.offset, 133 | .end = s.cursor.offset + 1, 134 | .style = cursor_style, 135 | }); 136 | if (s.cursor.offset < s.anchor.offset) { 137 | try highlight.addSegment(.{ 138 | .start = s.cursor.offset + 1, 139 | .end = s.anchor.offset + 1, 140 | .style = selection_style, 141 | }); 142 | } 143 | } 144 | 145 | pub fn addPattern( 146 | highlight: *Highlight, 147 | slice: []const u8, 148 | pattern: []const u8, 149 | style: kisa.Style, 150 | ) !void { 151 | var start_index: usize = 0; 152 | while (std.mem.indexOfPos(u8, slice, start_index, pattern)) |idx| { 153 | try highlight.addSegment(.{ 154 | .start = idx, 155 | .end = idx + pattern.len, 156 | .style = style, 157 | }); 158 | start_index = idx + pattern.len; 159 | if (start_index >= slice.len) break; 160 | } 161 | } 162 | 163 | pub fn decorateLine( 164 | highlight: Highlight, 165 | ally: std.mem.Allocator, 166 | slice: []const u8, 167 | line_start: usize, 168 | line_end: usize, 169 | ) ![]const kisa.DrawData.Line.Segment { 170 | var segments = std.ArrayList(kisa.DrawData.Line.Segment).init(ally); 171 | var processed_index = line_start; 172 | var last_highlight_segment: ?*Highlight.Segment = null; 173 | 174 | var node = highlight.segments.first(); 175 | while (node) |n| : (node = n.next()) { 176 | const highlight_segment = @fieldParentPtr(Segment, "node", n); 177 | std.debug.print("hs: {d} - {d}\n", .{ highlight_segment.start, highlight_segment.end }); 178 | } 179 | while (node) |n| : (node = n.next()) { 180 | const highlight_segment = @fieldParentPtr(Segment, "node", n); 181 | if (highlight_segment.start > line_end) continue; 182 | if (highlight_segment.end < line_start) break; 183 | 184 | const start = std.math.max(highlight_segment.start, line_start); 185 | const end = std.math.min(highlight_segment.end, line_end); 186 | std.debug.print("start: {d}, end: {d}\n", .{ start, end }); 187 | if (processed_index < start) { 188 | try segments.append(kisa.DrawData.Line.Segment{ 189 | .contents = slice[processed_index..start], 190 | }); 191 | } 192 | try segments.append(kisa.DrawData.Line.Segment{ 193 | .contents = slice[start..end], 194 | .style = highlight_segment.style.toData(), 195 | }); 196 | processed_index = end; 197 | last_highlight_segment = highlight_segment; 198 | } 199 | if (processed_index < line_end) { 200 | try segments.append(kisa.DrawData.Line.Segment{ 201 | .contents = slice[processed_index..line_end], 202 | }); 203 | } else if (last_highlight_segment != null and last_highlight_segment.?.end > line_end) { 204 | // Hihglight the newline at the end of line in case higlight segment spans several lines. 205 | try segments.append(kisa.DrawData.Line.Segment{ 206 | .contents = " ", 207 | .style = last_highlight_segment.?.style.toData(), 208 | }); 209 | } 210 | return segments.items; 211 | } 212 | 213 | /// `ally` should be an arena allocator which is freed after resulting `DrawData` is used. 214 | pub fn synthesize( 215 | ally: std.mem.Allocator, 216 | highlight: Highlight, 217 | slice: []const u8, 218 | newline: []const u8, 219 | ) !kisa.DrawData { 220 | var lines = std.ArrayList(kisa.DrawData.Line).init(ally); 221 | var line_it = std.mem.split(u8, slice, newline); 222 | var line_number: u32 = 0; 223 | while (line_it.next()) |line| { 224 | const line_offset = @ptrToInt(line.ptr) - @ptrToInt(slice.ptr); 225 | if (line_offset == slice.len) break; // if this is a line past the final newline 226 | line_number += 1; 227 | const segments = try highlight.decorateLine( 228 | ally, 229 | slice, 230 | line_offset, 231 | line_offset + line.len, 232 | ); 233 | try lines.append(kisa.DrawData.Line{ 234 | .number = line_number, 235 | .segments = segments, 236 | }); 237 | } 238 | 239 | var max_line_number_length: u8 = 0; 240 | // +1 in case the file doesn't have a final newline. 241 | var max_line_number = std.mem.count(u8, slice, newline) + 1; 242 | while (max_line_number != 0) : (max_line_number = max_line_number / 10) { 243 | max_line_number_length += 1; 244 | } 245 | 246 | return kisa.DrawData{ 247 | .max_line_number_length = max_line_number_length, 248 | .active_line_number = highlight.active_line_number, 249 | .lines = lines.items, 250 | }; 251 | } 252 | 253 | // Run from project root: zig build run-highlight 254 | pub fn main() !void { 255 | const text = 256 | \\My first line 257 | \\def max(x, y) 258 | \\ if x > y 259 | \\ x 260 | \\ else 261 | \\ y 262 | \\ end 263 | \\end 264 | \\ 265 | \\ 266 | ; 267 | var hl = Highlight.init(testing.allocator); 268 | defer hl.deinit(); 269 | try hl.addPattern(text, "end", kisa.Style{ .foreground = .{ .base16 = .blue } }); 270 | try hl.addPattern(text, "e", kisa.Style{ .foreground = .{ .base16 = .red } }); 271 | try hl.addSelection(kisa.Selection{ 272 | .cursor = .{ .offset = 2, .line = 1, .column = 3 }, 273 | .anchor = .{ .offset = 0, .line = 1, .column = 1 }, 274 | .primary = false, 275 | }); 276 | try hl.addSelection(kisa.Selection{ 277 | .cursor = .{ .offset = 4, .line = 1, .column = 5 }, 278 | .anchor = .{ .offset = 6, .line = 1, .column = 7 }, 279 | .primary = false, 280 | }); 281 | try hl.addSelection(kisa.Selection{ 282 | .cursor = .{ .offset = 8, .line = 1, .column = 9 }, 283 | .anchor = .{ .offset = 8, .line = 1, .column = 9 }, 284 | .primary = false, 285 | }); 286 | try hl.addSelection(kisa.Selection{ 287 | .cursor = .{ .offset = 16, .line = 2, .column = 3 }, 288 | .anchor = .{ .offset = 11, .line = 1, .column = 12 }, 289 | }); 290 | var synthesize_arena = std.heap.ArenaAllocator.init(testing.allocator); 291 | defer synthesize_arena.deinit(); 292 | const draw_data = try synthesize(synthesize_arena.allocator(), hl, text, "\n"); 293 | 294 | const ui_api = @import("ui_api.zig"); 295 | var ui = try ui_api.init(std.io.getStdIn(), std.io.getStdOut()); 296 | // defer ui.deinit(); 297 | const default_style = kisa.Style{}; 298 | try ui_api.draw(&ui, draw_data, .{ 299 | .default_text_style = default_style, 300 | .line_number_separator = "| ", 301 | .line_number_style = .{ .font_style = .{ .underline = true } }, 302 | .line_number_separator_style = .{ .foreground = .{ .base16 = .magenta } }, 303 | .active_line_number_style = .{ .font_style = .{ .reverse = true } }, 304 | }); 305 | ui.deinit(); 306 | 307 | // for (hl.segments.items) |s| { 308 | // std.debug.print("start: {d}, end: {d}\n", .{ s.start, s.end }); 309 | // } 310 | for (draw_data.lines) |line| { 311 | std.debug.print("{d}: ", .{line.number}); 312 | for (line.segments) |s| { 313 | std.debug.print("{s}, ", .{s.contents}); 314 | } 315 | std.debug.print("\n", .{}); 316 | } 317 | } 318 | 319 | test "highlight: reference all" { 320 | testing.refAllDecls(@This()); 321 | } 322 | -------------------------------------------------------------------------------- /not_good_enough/src/keys.zig: -------------------------------------------------------------------------------- 1 | //! Representation of a frontend-agnostic "key" which is supposed to encode any possible key 2 | //! unambiguously. All UI frontends are supposed to provide a `Key` struct out of their `nextKey` 3 | //! function for consumption by the backend. 4 | 5 | const std = @import("std"); 6 | 7 | pub const KeySym = enum { 8 | arrow_up, 9 | arrow_down, 10 | arrow_left, 11 | arrow_right, 12 | 13 | pub fn jsonStringify( 14 | value: KeySym, 15 | options: std.json.StringifyOptions, 16 | out_stream: anytype, 17 | ) @TypeOf(out_stream).Error!void { 18 | _ = options; 19 | try out_stream.writeAll(std.meta.tagName(value)); 20 | } 21 | }; 22 | 23 | pub const MouseButton = enum { 24 | left, 25 | middle, 26 | right, 27 | scroll_up, 28 | scroll_down, 29 | 30 | pub fn jsonStringify( 31 | value: MouseButton, 32 | options: std.json.StringifyOptions, 33 | out_stream: anytype, 34 | ) @TypeOf(out_stream).Error!void { 35 | _ = options; 36 | try out_stream.writeAll(std.meta.tagName(value)); 37 | } 38 | }; 39 | 40 | pub const KeyCode = union(enum) { 41 | unicode_codepoint: u21, 42 | function: u8, 43 | keysym: KeySym, 44 | mouse_button: MouseButton, 45 | mouse_position: struct { x: u32, y: u32 }, 46 | }; 47 | 48 | pub const Key = struct { 49 | code: KeyCode, 50 | modifiers: u8 = 0, 51 | // Any Unicode character can be UTF-8 encoded in no more than 6 bytes, plus terminating null 52 | utf8: [6:0]u8 = undefined, 53 | 54 | // zig fmt: off 55 | const shift_bit = @as(u8, 1 << 0); 56 | const alt_bit = @as(u8, 1 << 1); 57 | const ctrl_bit = @as(u8, 1 << 2); 58 | const super_bit = @as(u8, 1 << 3); 59 | const hyper_bit = @as(u8, 1 << 4); 60 | const meta_bit = @as(u8, 1 << 5); 61 | const caps_lock_bit = @as(u8, 1 << 6); 62 | const num_lock_bit = @as(u8, 1 << 7); 63 | // zig fmt: on 64 | 65 | pub fn hasShift(self: Key) bool { 66 | return (self.modifiers & shift_bit) != 0; 67 | } 68 | pub fn hasAlt(self: Key) bool { 69 | return (self.modifiers & alt_bit) != 0; 70 | } 71 | pub fn hasCtrl(self: Key) bool { 72 | return (self.modifiers & ctrl_bit) != 0; 73 | } 74 | pub fn hasSuper(self: Key) bool { 75 | return (self.modifiers & super_bit) != 0; 76 | } 77 | pub fn hasHyper(self: Key) bool { 78 | return (self.modifiers & hyper_bit) != 0; 79 | } 80 | pub fn hasMeta(self: Key) bool { 81 | return (self.modifiers & meta_bit) != 0; 82 | } 83 | pub fn hasCapsLock(self: Key) bool { 84 | return (self.modifiers & caps_lock_bit) != 0; 85 | } 86 | pub fn hasNumLock(self: Key) bool { 87 | return (self.modifiers & num_lock_bit) != 0; 88 | } 89 | 90 | pub fn addShift(self: *Key) void { 91 | self.modifiers = self.modifiers | shift_bit; 92 | } 93 | pub fn addAlt(self: *Key) void { 94 | self.modifiers = self.modifiers | alt_bit; 95 | } 96 | pub fn addCtrl(self: *Key) void { 97 | self.modifiers = self.modifiers | ctrl_bit; 98 | } 99 | pub fn addSuper(self: *Key) void { 100 | self.modifiers = self.modifiers | super_bit; 101 | } 102 | pub fn addHyper(self: *Key) void { 103 | self.modifiers = self.modifiers | hyper_bit; 104 | } 105 | pub fn addMeta(self: *Key) void { 106 | self.modifiers = self.modifiers | meta_bit; 107 | } 108 | pub fn addCapsLock(self: *Key) void { 109 | self.modifiers = self.modifiers | caps_lock_bit; 110 | } 111 | pub fn addNumLock(self: *Key) void { 112 | self.modifiers = self.modifiers | num_lock_bit; 113 | } 114 | 115 | fn utf8len(self: Key) usize { 116 | var length: usize = 0; 117 | for (self.utf8) |byte| { 118 | if (byte == 0) break; 119 | length += 1; 120 | } else { 121 | unreachable; // we are responsible for making sure this never happens 122 | } 123 | return length; 124 | } 125 | pub fn isAscii(self: Key) bool { 126 | return self.code == .unicode_codepoint and self.utf8len() == 1; 127 | } 128 | pub fn isCtrl(self: Key, character: u8) bool { 129 | return self.isAscii() and self.utf8[0] == character and self.modifiers == ctrl_bit; 130 | } 131 | 132 | pub fn ascii(character: u8) Key { 133 | var key = Key{ .code = .{ .unicode_codepoint = character } }; 134 | key.utf8[0] = character; 135 | key.utf8[1] = 0; 136 | return key; 137 | } 138 | pub fn ctrl(character: u8) Key { 139 | var key = ascii(character); 140 | key.addCtrl(); 141 | return key; 142 | } 143 | pub fn alt(character: u8) Key { 144 | var key = ascii(character); 145 | key.addAlt(); 146 | return key; 147 | } 148 | pub fn shift(character: u8) Key { 149 | var key = ascii(character); 150 | key.addShift(); 151 | return key; 152 | } 153 | 154 | // We don't use `utf8` field for equality because it only contains necessary information 155 | // to represent other values and must not be considered to be always present. 156 | pub fn eql(a: Key, b: Key) bool { 157 | return std.meta.eql(a.code, b.code) and std.meta.eql(a.modifiers, b.modifiers); 158 | } 159 | pub fn hash(key: Key) u64 { 160 | var hasher = std.hash.Wyhash.init(0); 161 | std.hash.autoHash(&hasher, key.code); 162 | std.hash.autoHash(&hasher, key.modifiers); 163 | return hasher.final(); 164 | } 165 | 166 | pub const HashMapContext = struct { 167 | pub fn hash(self: @This(), s: Key) u64 { 168 | _ = self; 169 | return Key.hash(s); 170 | } 171 | pub fn eql(self: @This(), a: Key, b: Key) bool { 172 | _ = self; 173 | return Key.eql(a, b); 174 | } 175 | }; 176 | 177 | pub fn format( 178 | value: Key, 179 | comptime fmt: []const u8, 180 | options: std.fmt.FormatOptions, 181 | writer: anytype, 182 | ) !void { 183 | _ = options; 184 | if (fmt.len == 1 and fmt[0] == 's') { 185 | try writer.writeAll("Key("); 186 | if (value.hasNumLock()) try writer.writeAll("num_lock-"); 187 | if (value.hasCapsLock()) try writer.writeAll("caps_lock-"); 188 | if (value.hasMeta()) try writer.writeAll("meta-"); 189 | if (value.hasHyper()) try writer.writeAll("hyper-"); 190 | if (value.hasSuper()) try writer.writeAll("super-"); 191 | if (value.hasCtrl()) try writer.writeAll("ctrl-"); 192 | if (value.hasAlt()) try writer.writeAll("alt-"); 193 | if (value.hasShift()) try writer.writeAll("shift-"); 194 | switch (value.code) { 195 | .unicode_codepoint => |val| { 196 | try std.fmt.format(writer, "{c}", .{@intCast(u8, val)}); 197 | }, 198 | .function => |val| try std.fmt.format(writer, "f{d}", .{val}), 199 | .keysym => |val| try std.fmt.format(writer, "{s}", .{std.meta.tagName(val)}), 200 | .mouse_button => |val| try std.fmt.format(writer, "{s}", .{std.meta.tagName(val)}), 201 | .mouse_position => |val| { 202 | try std.fmt.format(writer, "MousePosition({d},{d})", .{ val.x, val.y }); 203 | }, 204 | } 205 | try writer.writeAll(")"); 206 | } else if (fmt.len == 0) { 207 | try std.fmt.format( 208 | writer, 209 | "{s}{{ .code = {}, .modifiers = {b}, .utf8 = {any} }}", 210 | .{ @typeName(@TypeOf(value)), value.code, value.modifiers, value.utf8 }, 211 | ); 212 | } else { 213 | @compileError("Unknown format character for Key: '" ++ fmt ++ "'"); 214 | } 215 | } 216 | }; 217 | 218 | test "keys: construction" { 219 | try std.testing.expect(!Key.ascii('c').hasCtrl()); 220 | try std.testing.expect(Key.ctrl('c').hasCtrl()); 221 | try std.testing.expect(Key.ascii('c').isAscii()); 222 | try std.testing.expect(Key.ctrl('c').isAscii()); 223 | try std.testing.expect(Key.ctrl('c').isCtrl('c')); 224 | } 225 | -------------------------------------------------------------------------------- /not_good_enough/src/kisa.zig: -------------------------------------------------------------------------------- 1 | //! Common data structures and functionality used by various components of this application. 2 | const std = @import("std"); 3 | const rpc = @import("rpc.zig"); 4 | const Server = @import("main.zig").Server; 5 | 6 | pub const keys = @import("keys.zig"); 7 | pub const Key = keys.Key; 8 | 9 | /// Data sent to Client which represents the data to draw on the screen. 10 | pub const DrawData = struct { 11 | /// Can be used to reserve space and beautifully align displayed line numbers. 12 | max_line_number_length: u8, 13 | /// Where the primary selection is. Can be used to accent the style of the active line. 14 | active_line_number: u32, 15 | /// Main data to draw on the screen. 16 | lines: []const Line, 17 | 18 | pub const Line = struct { 19 | number: u32, 20 | segments: []const Segment, 21 | 22 | pub const Segment = struct { 23 | contents: []const u8, 24 | style: Style.Data = .{}, 25 | }; 26 | }; 27 | }; 28 | 29 | pub const Color = union(enum) { 30 | special: Special, 31 | base16: Base16, 32 | rgb: RGB, 33 | 34 | pub const Special = enum { 35 | default, 36 | 37 | pub fn jsonStringify( 38 | value: Special, 39 | options: std.json.StringifyOptions, 40 | out_stream: anytype, 41 | ) @TypeOf(out_stream).Error!void { 42 | try std.json.stringify(std.meta.tagName(value), options, out_stream); 43 | } 44 | }; 45 | 46 | pub const Base16 = enum { 47 | black, 48 | red, 49 | green, 50 | yellow, 51 | blue, 52 | magenta, 53 | cyan, 54 | white, 55 | black_bright, 56 | red_bright, 57 | green_bright, 58 | yellow_bright, 59 | blue_bright, 60 | magenta_bright, 61 | cyan_bright, 62 | white_bright, 63 | 64 | pub fn jsonStringify( 65 | value: Base16, 66 | options: std.json.StringifyOptions, 67 | out_stream: anytype, 68 | ) @TypeOf(out_stream).Error!void { 69 | try std.json.stringify(std.meta.tagName(value), options, out_stream); 70 | } 71 | }; 72 | 73 | pub const RGB = struct { 74 | r: u8, 75 | g: u8, 76 | b: u8, 77 | }; 78 | }; 79 | 80 | pub const FontStyle = struct { 81 | bold: bool = false, 82 | dim: bool = false, 83 | italic: bool = false, 84 | underline: bool = false, 85 | reverse: bool = false, 86 | strikethrough: bool = false, 87 | 88 | pub fn toData(self: FontStyle) u8 { 89 | var result: u8 = 0; 90 | if (self.bold) result += 1; 91 | if (self.dim) result += 2; 92 | if (self.italic) result += 4; 93 | if (self.underline) result += 8; 94 | if (self.reverse) result += 16; 95 | if (self.strikethrough) result += 32; 96 | return result; 97 | } 98 | 99 | pub fn fromData(data: u8) FontStyle { 100 | var result = FontStyle{}; 101 | if (data & 1 != 0) result.bold = true; 102 | if (data & 2 != 0) result.dim = true; 103 | return result; 104 | } 105 | }; 106 | 107 | pub const Style = struct { 108 | foreground: Color = .{ .base16 = .white }, 109 | background: Color = .{ .base16 = .black }, 110 | font_style: FontStyle = .{}, 111 | 112 | pub const Data = struct { 113 | fg: Color = .{ .special = .default }, 114 | bg: Color = .{ .special = .default }, 115 | attrs: u8 = 0, 116 | }; 117 | 118 | pub fn toData(self: Style) Data { 119 | return .{ 120 | .fg = self.foreground, 121 | .bg = self.background, 122 | .attrs = self.font_style.toData(), 123 | }; 124 | } 125 | }; 126 | 127 | pub const CommandKind = enum { 128 | nop, 129 | /// Params has information about the key. 130 | keypress, 131 | /// Sent by client when it quits. 132 | quitted, 133 | /// First value in params is the command kind, others are arguments to this command. 134 | initialize, 135 | quit, 136 | save, 137 | redraw, 138 | insert_character, 139 | cursor_move_down, 140 | cursor_move_left, 141 | cursor_move_up, 142 | cursor_move_right, 143 | delete_word, 144 | delete_line, 145 | open_file, 146 | }; 147 | 148 | /// Command is a an action issued by client to be executed on the server. 149 | pub const Command = union(CommandKind) { 150 | nop, 151 | /// Sent by client when it quits. 152 | quitted, 153 | quit, 154 | save, 155 | redraw, 156 | /// Provide initial parameters to initialize a client. 157 | initialize: ClientInitParams, 158 | /// Value is inserted character. TODO: should not be u8. 159 | insert_character: u8, 160 | keypress: Keypress, 161 | cursor_move_down, 162 | cursor_move_left, 163 | cursor_move_up, 164 | cursor_move_right, 165 | delete_word, 166 | delete_line, 167 | /// Value is absolute file path. 168 | open_file: struct { path: []const u8 }, 169 | 170 | /// Parameters necessary to create new state in workspace and get `active_display_state`. 171 | pub const ClientInitParams = struct { 172 | path: []const u8, 173 | readonly: bool, 174 | text_area_rows: u32, 175 | text_area_cols: u32, 176 | line_ending: LineEnding, 177 | }; 178 | 179 | // pub const Multiplier = struct { multiplier: u32 = 1 }; 180 | 181 | /// Different editing operations accept a numeric multiplier which specifies the number of 182 | /// times the operation should be executed. 183 | pub const Keypress = struct { key: Key, multiplier: u32 }; 184 | }; 185 | 186 | pub const TextBufferMetrics = struct { 187 | max_line_number: u32 = 0, 188 | }; 189 | 190 | pub const LineEnding = enum { 191 | unix, 192 | dos, 193 | old_mac, 194 | 195 | pub fn str(self: @This()) []const u8 { 196 | return switch (self) { 197 | .unix => "\n", 198 | .dos => "\r\n", 199 | .old_mac => "\r", 200 | }; 201 | } 202 | 203 | pub fn jsonStringify( 204 | value: @This(), 205 | options: std.json.StringifyOptions, 206 | out_stream: anytype, 207 | ) @TypeOf(out_stream).Error!void { 208 | _ = options; 209 | switch (value) { 210 | .unix => try std.json.stringify("unix", options, out_stream), 211 | .dos => try std.json.stringify("dox", options, out_stream), 212 | .old_mac => try std.json.stringify("old_mac", options, out_stream), 213 | } 214 | } 215 | }; 216 | 217 | /// Pair of two positions in the buffer `cursor` and `anchor` that creates a selection of 218 | /// characters. In the simple case when `cursor` and `anchor` positions are same, the selection 219 | /// is of width 1. 220 | pub const Selection = struct { 221 | /// Main caret position. 222 | cursor: Position, 223 | /// Caret position which creates selection if it is not same as `cursor`. 224 | anchor: Position, 225 | /// Whether to move `anchor` together with `cursor`. 226 | anchored: bool = false, 227 | /// For multiple cursors, primary cursor never leaves the display window. 228 | primary: bool = true, 229 | /// Used for next/previous line movements. Saves the column value and always tries to reach 230 | /// it even when the line does not have enough columns. 231 | transient_column: Dimension = 0, 232 | /// Used for next/previous line movements. Saves whether the cursor was at newline and always 233 | /// tries to reach a newline on consecutive lines. Takes precedence over `transient_column`. 234 | transient_newline: bool = false, 235 | 236 | const Self = @This(); 237 | pub const Offset = u32; 238 | pub const Dimension = u32; 239 | pub const Position = struct { offset: Offset, line: Dimension, column: Dimension }; 240 | 241 | pub fn moveTo(self: Self, position: Position) Self { 242 | var result = self; 243 | result.cursor = position; 244 | if (!self.anchored) result.anchor = position; 245 | return result; 246 | } 247 | 248 | pub fn resetTransients(self: Self) Self { 249 | var result = self; 250 | result.transient_column = 0; 251 | result.transient_newline = false; 252 | return result; 253 | } 254 | }; 255 | 256 | pub const Selections = std.ArrayList(Selection); 257 | -------------------------------------------------------------------------------- /not_good_enough/src/terminal_ui.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const kisa = @import("kisa"); 4 | 5 | const TerminalUI = @This(); 6 | const writer_buffer_size = 4096; 7 | const WriterCtx = std.io.BufferedWriter( 8 | writer_buffer_size, 9 | std.io.Writer(std.fs.File, std.fs.File.WriteError, std.fs.File.write), 10 | ); 11 | pub const Dimensions = struct { 12 | width: u16, 13 | height: u16, 14 | }; 15 | const Support = struct { 16 | ansi_escape_codes: bool, 17 | }; 18 | const esc = "\x1B"; 19 | const csi = esc ++ "["; 20 | const style_reset = csi ++ "0m"; 21 | const style_bold = csi ++ "1m"; 22 | const style_dim = csi ++ "2m"; 23 | const style_italic = csi ++ "3m"; 24 | const style_underline = csi ++ "4m"; 25 | const style_reverse = csi ++ "7m"; 26 | const style_strikethrough = csi ++ "9m"; 27 | const clear_all = csi ++ "2J"; 28 | const cursor_hide = csi ++ "?25l"; 29 | const cursor_show = csi ++ "?25h"; 30 | 31 | original_termios: std.os.termios, 32 | dimensions: Dimensions, 33 | in: std.fs.File, 34 | out: std.fs.File, 35 | writer_ctx: WriterCtx, 36 | support: Support, 37 | 38 | pub fn init(in: std.fs.File, out: std.fs.File) !TerminalUI { 39 | if (!in.isTty()) return error.NotTTY; 40 | 41 | var original_termios = try std.os.tcgetattr(in.handle); 42 | var termios = original_termios; 43 | 44 | // Black magic, see https://github.com/antirez/kilo 45 | termios.iflag &= ~(std.os.linux.BRKINT | std.os.linux.ICRNL | std.os.linux.INPCK | 46 | std.os.linux.ISTRIP | std.os.linux.IXON); 47 | termios.oflag &= ~(std.os.linux.OPOST); 48 | termios.cflag |= (std.os.linux.CS8); 49 | termios.lflag &= ~(std.os.linux.ECHO | std.os.linux.ICANON | std.os.linux.IEXTEN | 50 | std.os.linux.ISIG); 51 | // Polling read, doesn't block. 52 | termios.cc[std.os.linux.V.MIN] = 0; 53 | // VTIME tenths of a second elapses between bytes. Can be important for slow terminals, 54 | // libtermkey (neovim) uses 50ms, which matters for example when pressing Alt key which 55 | // sends several bytes but the very first one is Esc, so it is necessary to wait enough 56 | // time for disambiguation. This setting alone can be not enough. 57 | termios.cc[std.os.linux.V.TIME] = 1; 58 | 59 | try std.os.tcsetattr(in.handle, .FLUSH, termios); 60 | return TerminalUI{ 61 | .original_termios = original_termios, 62 | .dimensions = try getWindowSize(in.handle), 63 | .in = in, 64 | .out = out, 65 | .writer_ctx = .{ .unbuffered_writer = out.writer() }, 66 | .support = .{ .ansi_escape_codes = out.supportsAnsiEscapeCodes() }, 67 | }; 68 | } 69 | 70 | fn getWindowSize(handle: std.os.fd_t) !Dimensions { 71 | var window_size: std.os.linux.winsize = undefined; 72 | const err = std.os.linux.ioctl(handle, std.os.linux.T.IOCGWINSZ, @ptrToInt(&window_size)); 73 | if (std.os.errno(err) != .SUCCESS) { 74 | return error.IoctlError; 75 | } 76 | return Dimensions{ 77 | .width = window_size.ws_col, 78 | .height = window_size.ws_row, 79 | }; 80 | } 81 | 82 | pub fn prepare(self: *TerminalUI) !void { 83 | if (self.support.ansi_escape_codes) { 84 | try self.writer().writeAll(cursor_hide); 85 | } 86 | } 87 | 88 | pub fn deinit(self: *TerminalUI) void { 89 | if (self.support.ansi_escape_codes) { 90 | self.writer().writeAll(cursor_show) catch {}; 91 | } 92 | self.flush() catch {}; 93 | std.os.tcsetattr(self.in.handle, .FLUSH, self.original_termios) catch {}; 94 | } 95 | 96 | pub fn writer(self: *TerminalUI) WriterCtx.Writer { 97 | return self.writer_ctx.writer(); 98 | } 99 | 100 | pub fn flush(self: *TerminalUI) !void { 101 | try self.writer_ctx.flush(); 102 | } 103 | 104 | pub fn clearScreen(self: *TerminalUI) !void { 105 | if (self.support.ansi_escape_codes) { 106 | try self.writer().writeAll(clear_all); 107 | try goTo(self.writer(), 1, 1); 108 | } 109 | } 110 | 111 | pub fn writeNewline(self: *TerminalUI) !void { 112 | if (self.support.ansi_escape_codes) { 113 | try self.writer().writeAll("\n\r"); 114 | } else { 115 | try self.writer().writeAll("\n"); 116 | } 117 | } 118 | 119 | fn goTo(w: anytype, x: u16, y: u16) !void { 120 | try std.fmt.format(w, csi ++ "{d};{d}H", .{ y, x }); 121 | } 122 | 123 | fn base16LinuxConsoleNumber(base16_color: kisa.Color.Base16) u8 { 124 | return switch (base16_color) { 125 | .black => 0, 126 | .red => 1, 127 | .green => 2, 128 | .yellow => 3, 129 | .blue => 4, 130 | .magenta => 5, 131 | .cyan => 6, 132 | .white => 7, 133 | .black_bright => 8, 134 | .red_bright => 9, 135 | .green_bright => 10, 136 | .yellow_bright => 11, 137 | .blue_bright => 12, 138 | .magenta_bright => 13, 139 | .cyan_bright => 14, 140 | .white_bright => 15, 141 | }; 142 | } 143 | 144 | fn writeFg(w: anytype, color: kisa.Color) !void { 145 | switch (color) { 146 | .rgb => |c| try std.fmt.format(w, csi ++ "38;2;{d};{d};{d}m", .{ c.r, c.g, c.b }), 147 | .base16 => |c| try std.fmt.format(w, csi ++ "38;5;{d}m", .{base16LinuxConsoleNumber(c)}), 148 | .special => return error.SpecialIsNotAColor, 149 | } 150 | } 151 | 152 | fn writeBg(w: anytype, color: kisa.Color) !void { 153 | switch (color) { 154 | .rgb => |c| try std.fmt.format(w, csi ++ "48;2;{d};{d};{d}m", .{ c.r, c.g, c.b }), 155 | .base16 => |c| try std.fmt.format(w, csi ++ "48;5;{d}m", .{base16LinuxConsoleNumber(c)}), 156 | .special => return error.SpecialIsNotAColor, 157 | } 158 | } 159 | 160 | fn writeFontStyle(w: anytype, font_style: kisa.FontStyle) !void { 161 | if (font_style.bold) try w.writeAll(style_bold); 162 | if (font_style.dim) try w.writeAll(style_dim); 163 | if (font_style.italic) try w.writeAll(style_italic); 164 | if (font_style.underline) try w.writeAll(style_underline); 165 | if (font_style.reverse) try w.writeAll(style_reverse); 166 | if (font_style.strikethrough) try w.writeAll(style_strikethrough); 167 | } 168 | 169 | fn writeStyleStart(self: *TerminalUI, style: kisa.Style) !void { 170 | var w = self.writer(); 171 | if (self.support.ansi_escape_codes) { 172 | try writeFg(w, style.foreground); 173 | try writeBg(w, style.background); 174 | try writeFontStyle(w, style.font_style); 175 | } 176 | } 177 | 178 | fn writeStyleEnd(self: *TerminalUI) !void { 179 | if (self.support.ansi_escape_codes) { 180 | try self.writer().writeAll(style_reset); 181 | } 182 | } 183 | 184 | pub fn writeAllFormatted(self: *TerminalUI, style: kisa.Style, string: []const u8) !void { 185 | try self.writeStyleStart(style); 186 | try self.writer().writeAll(string); 187 | try self.writeStyleEnd(); 188 | } 189 | 190 | pub fn writeByteNTimesFormatted(self: *TerminalUI, style: kisa.Style, byte: u8, n: usize) !void { 191 | try self.writeStyleStart(style); 192 | try self.writer().writeByteNTimes(byte, n); 193 | try self.writeStyleEnd(); 194 | } 195 | 196 | // Run from project root: zig build run-terminal-ui 197 | pub fn main() !void { 198 | var file = try std.fs.cwd().openFile("src/terminal_ui.zig", .{}); 199 | const text = try file.readToEndAlloc(testing.allocator, std.math.maxInt(usize)); 200 | defer testing.allocator.free(text); 201 | var ui = try TerminalUI.init(std.io.getStdIn(), std.io.getStdOut()); 202 | defer ui.deinit(); 203 | try ui.prepare(); 204 | try ui.clearScreen(); 205 | var text_it = std.mem.split(u8, text, "\n"); 206 | const style = kisa.Style{ 207 | .foreground = .{ .base16 = .yellow }, 208 | .background = .{ .rgb = .{ .r = 33, .g = 33, .b = 33 } }, 209 | .font_style = .{ .italic = true }, 210 | }; 211 | const style2 = kisa.Style{ 212 | .foreground = .{ .base16 = .red }, 213 | .background = .{ .rgb = .{ .r = 33, .g = 33, .b = 33 } }, 214 | }; 215 | var i: u32 = 0; 216 | while (text_it.next()) |line| { 217 | i += 1; 218 | if (i % 5 == 0) { 219 | try ui.writer().writeAll(line); 220 | } else if (i % 2 == 0) { 221 | try ui.writeAllFormatted(style, line); 222 | } else { 223 | try ui.writeAllFormatted(style2, line); 224 | } 225 | try ui.writeNewline(); 226 | } 227 | } 228 | 229 | test "ui: reference all" { 230 | testing.refAllDecls(TerminalUI); 231 | } 232 | -------------------------------------------------------------------------------- /not_good_enough/src/ui_api.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const kisa = @import("kisa"); 4 | pub const UI = @import("terminal_ui.zig"); 5 | 6 | comptime { 7 | const interface_functions = [_][]const u8{ 8 | "init", 9 | "prepare", 10 | "deinit", 11 | "writer", 12 | "flush", 13 | "clearScreen", 14 | "writeNewline", 15 | "writeAllFormatted", 16 | "writeByteNTimesFormatted", 17 | }; 18 | for (interface_functions) |f| { 19 | if (!std.meta.trait.hasFn(f)(UI)) { 20 | @compileError("'UI' interface does not implement function '" ++ f ++ "'"); 21 | } 22 | } 23 | } 24 | 25 | pub const Config = struct { 26 | default_text_style: kisa.Style = .{}, 27 | line_number_style: kisa.Style = .{}, 28 | active_line_number_style: kisa.Style = .{}, 29 | line_number_separator: []const u8 = " ", 30 | line_number_separator_style: kisa.Style = .{}, 31 | }; 32 | 33 | pub fn init(in: std.fs.File, out: std.fs.File) !UI { 34 | var ui = try UI.init(in, out); 35 | try ui.prepare(); 36 | try ui.clearScreen(); 37 | return ui; 38 | } 39 | 40 | pub fn deinit(self: *UI) void { 41 | self.deinit(); 42 | } 43 | 44 | fn parseColor(color: kisa.Color, default_color: kisa.Color) kisa.Color { 45 | switch (color) { 46 | .special => |special| return switch (special) { 47 | .default => default_color, 48 | }, 49 | else => return color, 50 | } 51 | } 52 | 53 | fn parseFontStyle( 54 | font_style_attributes: u8, 55 | default_font_style: kisa.FontStyle, 56 | ) kisa.FontStyle { 57 | if (font_style_attributes == 0) return default_font_style; 58 | return kisa.FontStyle.fromData(font_style_attributes); 59 | } 60 | 61 | fn parseSegmentStyle(style_data: kisa.Style.Data, default_style: kisa.Style) !kisa.Style { 62 | return kisa.Style{ 63 | .foreground = parseColor(style_data.fg, default_style.foreground), 64 | .background = parseColor(style_data.bg, default_style.background), 65 | .font_style = parseFontStyle(style_data.attrs, default_style.font_style), 66 | }; 67 | } 68 | 69 | pub fn draw(ui: *UI, draw_data: kisa.DrawData, config: Config) !void { 70 | switch (config.default_text_style.foreground) { 71 | .special => return error.DefaultStyleMustHaveConcreteColor, 72 | else => {}, 73 | } 74 | switch (config.default_text_style.background) { 75 | .special => return error.DefaultStyleMustHaveConcreteColor, 76 | else => {}, 77 | } 78 | var line_buf: [10]u8 = undefined; 79 | for (draw_data.lines) |line| { 80 | var current_line_length: usize = 0; 81 | const line_str = try std.fmt.bufPrint(&line_buf, "{d}", .{line.number}); 82 | const line_number_style = if (draw_data.active_line_number == line.number) 83 | config.active_line_number_style 84 | else 85 | config.line_number_style; 86 | if (line_str.len < draw_data.max_line_number_length) 87 | try ui.writeByteNTimesFormatted( 88 | line_number_style, 89 | ' ', 90 | draw_data.max_line_number_length - line_str.len, 91 | ); 92 | try ui.writeAllFormatted(line_number_style, line_str); 93 | try ui.writeAllFormatted(config.line_number_separator_style, config.line_number_separator); 94 | current_line_length += draw_data.max_line_number_length + config.line_number_separator.len; 95 | for (line.segments) |segment| { 96 | const segment_style = try parseSegmentStyle(segment.style, config.default_text_style); 97 | try ui.writeAllFormatted(segment_style, segment.contents); 98 | current_line_length += segment.contents.len; 99 | } 100 | try ui.writeByteNTimesFormatted( 101 | config.default_text_style, 102 | ' ', 103 | ui.dimensions.width - current_line_length, 104 | ); 105 | try ui.writeNewline(); 106 | } 107 | } 108 | 109 | const draw_data_sample = kisa.DrawData{ 110 | .max_line_number_length = 3, 111 | .active_line_number = 7, 112 | .lines = &[_]kisa.DrawData.Line{ 113 | .{ 114 | .number = 1, 115 | .segments = &[_]kisa.DrawData.Line.Segment{.{ .contents = "My first line" }}, 116 | }, 117 | .{ 118 | .number = 2, 119 | .segments = &[_]kisa.DrawData.Line.Segment{ 120 | .{ 121 | .contents = "def ", 122 | .style = .{ .fg = .{ .base16 = .red } }, 123 | }, 124 | .{ 125 | .contents = "max", 126 | .style = .{ .attrs = (kisa.FontStyle{ .underline = true }).toData() }, 127 | }, 128 | .{ 129 | .contents = "(", 130 | }, 131 | .{ 132 | .contents = "x", 133 | .style = .{ 134 | .fg = .{ .base16 = .green }, 135 | .attrs = (kisa.FontStyle{ .bold = true }).toData(), 136 | }, 137 | }, 138 | .{ 139 | .contents = ", ", 140 | }, 141 | .{ 142 | .contents = "y", 143 | .style = .{ 144 | .fg = .{ .base16 = .green }, 145 | .attrs = (kisa.FontStyle{ .bold = true }).toData(), 146 | }, 147 | }, 148 | .{ 149 | .contents = ")", 150 | }, 151 | }, 152 | }, 153 | .{ 154 | .number = 3, 155 | .segments = &[_]kisa.DrawData.Line.Segment{ 156 | .{ 157 | .contents = " ", 158 | }, 159 | .{ 160 | .contents = "if", 161 | .style = .{ .fg = .{ .base16 = .blue } }, 162 | }, 163 | .{ 164 | .contents = " ", 165 | }, 166 | .{ 167 | .contents = "x", 168 | .style = .{ 169 | .fg = .{ .base16 = .green }, 170 | .attrs = (kisa.FontStyle{ .bold = true, .underline = true }).toData(), 171 | }, 172 | }, 173 | .{ 174 | .contents = " ", 175 | }, 176 | .{ 177 | .contents = ">", 178 | .style = .{ .fg = .{ .base16 = .yellow } }, 179 | }, 180 | .{ 181 | .contents = " ", 182 | }, 183 | .{ 184 | .contents = "y", 185 | .style = .{ 186 | .fg = .{ .base16 = .green }, 187 | .attrs = (kisa.FontStyle{ .bold = true, .underline = true }).toData(), 188 | }, 189 | }, 190 | }, 191 | }, 192 | .{ 193 | .number = 4, 194 | .segments = &[_]kisa.DrawData.Line.Segment{ 195 | .{ 196 | .contents = " ", 197 | }, 198 | .{ 199 | .contents = "x", 200 | .style = .{ 201 | .bg = .{ .rgb = .{ .r = 63, .g = 63, .b = 63 } }, 202 | .attrs = (kisa.FontStyle{ .underline = true }).toData(), 203 | }, 204 | }, 205 | }, 206 | }, 207 | .{ 208 | .number = 5, 209 | .segments = &[_]kisa.DrawData.Line.Segment{ 210 | .{ 211 | .contents = " ", 212 | }, 213 | .{ 214 | .contents = "else", 215 | .style = .{ .fg = .{ .base16 = .blue } }, 216 | }, 217 | }, 218 | }, 219 | .{ 220 | .number = 6, 221 | .segments = &[_]kisa.DrawData.Line.Segment{ 222 | .{ 223 | .contents = " ", 224 | }, 225 | .{ 226 | .contents = "y", 227 | .style = .{ 228 | .bg = .{ .rgb = .{ .r = 63, .g = 63, .b = 63 } }, 229 | .attrs = (kisa.FontStyle{ .underline = true }).toData(), 230 | }, 231 | }, 232 | }, 233 | }, 234 | .{ 235 | .number = 7, 236 | .segments = &[_]kisa.DrawData.Line.Segment{ 237 | .{ 238 | .contents = " ", 239 | }, 240 | .{ 241 | .contents = "end", 242 | .style = .{ .fg = .{ .base16 = .blue } }, 243 | }, 244 | }, 245 | }, 246 | .{ 247 | .number = 10, 248 | .segments = &[_]kisa.DrawData.Line.Segment{ 249 | .{ 250 | .contents = "end", 251 | .style = .{ .fg = .{ .base16 = .red } }, 252 | }, 253 | }, 254 | }, 255 | }, 256 | }; 257 | 258 | // Run from project root: zig build run-ui 259 | pub fn main() !void { 260 | var ui = try init(std.io.getStdIn(), std.io.getStdOut()); 261 | defer ui.deinit(); 262 | { 263 | const default_style = kisa.Style{ 264 | .foreground = .{ .base16 = .white }, 265 | .background = .{ .base16 = .black }, 266 | }; 267 | try draw(&ui, draw_data_sample, .{ .default_text_style = default_style }); 268 | } 269 | { 270 | const default_style = kisa.Style{ 271 | .foreground = .{ .base16 = .magenta_bright }, 272 | .background = .{ .rgb = .{ .r = 55, .g = 55, .b = 55 } }, 273 | .font_style = kisa.FontStyle{ .italic = true }, 274 | }; 275 | try draw(&ui, draw_data_sample, .{ 276 | .default_text_style = default_style, 277 | .line_number_separator = "| ", 278 | .line_number_style = .{ .font_style = .{ .underline = true } }, 279 | .line_number_separator_style = .{ .foreground = .{ .base16 = .magenta } }, 280 | .active_line_number_style = .{ .font_style = .{ .reverse = true } }, 281 | }); 282 | } 283 | } 284 | 285 | test "ui: reference all" { 286 | testing.refAllDecls(@This()); 287 | } 288 | 289 | test "ui: generate/parse DrawData" { 290 | var generated = std.ArrayList(u8).init(testing.allocator); 291 | defer generated.deinit(); 292 | try std.json.stringify(draw_data_sample, .{}, generated.writer()); 293 | var token_stream = std.json.TokenStream.init(generated.items); 294 | const parsed = try std.json.parse(kisa.DrawData, &token_stream, .{ .allocator = testing.allocator }); 295 | defer std.json.parseFree(kisa.DrawData, parsed, .{ .allocator = testing.allocator }); 296 | try testing.expectEqual(draw_data_sample.max_line_number_length, parsed.max_line_number_length); 297 | } 298 | -------------------------------------------------------------------------------- /poc/ipc_dgram_socket.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | const mem = std.mem; 4 | const testing = std.testing; 5 | const assert = std.debug.assert; 6 | 7 | test "communication via dgram connection-less unix domain socket" { 8 | const socket = try os.socket( 9 | os.AF_UNIX, 10 | os.SOCK_DGRAM | os.SOCK_CLOEXEC, 11 | os.PF_UNIX, 12 | ); 13 | defer os.closeSocket(socket); 14 | 15 | const runtime_dir = try std.fmt.allocPrint(testing.allocator, "/var/run/user/1000", .{}); 16 | const subpath = "/kisa"; 17 | var path_builder = std.ArrayList(u8).fromOwnedSlice(testing.allocator, runtime_dir); 18 | defer path_builder.deinit(); 19 | try path_builder.appendSlice(subpath); 20 | std.fs.makeDirAbsolute(path_builder.items) catch |err| switch (err) { 21 | error.PathAlreadyExists => {}, 22 | else => return err, 23 | }; 24 | const filename = try std.fmt.allocPrint(testing.allocator, "{d}", .{os.linux.getpid()}); 25 | defer testing.allocator.free(filename); 26 | try path_builder.append('/'); 27 | try path_builder.appendSlice(filename); 28 | std.fs.deleteFileAbsolute(path_builder.items) catch |err| switch (err) { 29 | error.FileNotFound => {}, 30 | else => return err, 31 | }; 32 | 33 | const addr = try testing.allocator.create(os.sockaddr_un); 34 | defer testing.allocator.destroy(addr); 35 | addr.* = os.sockaddr_un{ .path = undefined }; 36 | mem.copy(u8, &addr.path, path_builder.items); 37 | addr.path[path_builder.items.len] = 0; // null-terminated string 38 | 39 | const sockaddr = @ptrCast(*os.sockaddr, addr); 40 | var addrlen: os.socklen_t = @sizeOf(@TypeOf(addr.*)); 41 | try os.bind(socket, sockaddr, addrlen); 42 | 43 | const pid = try os.fork(); 44 | if (pid == 0) { 45 | // Client 46 | const message = try std.fmt.allocPrint(testing.allocator, "hello from client!\n", .{}); 47 | defer testing.allocator.free(message); 48 | const bytes_sent = try os.sendto(socket, message, 0, sockaddr, addrlen); 49 | assert(message.len == bytes_sent); 50 | } else { 51 | // Server 52 | var buf: [256]u8 = undefined; 53 | const bytes_read = try os.recvfrom(socket, &buf, 0, null, null); 54 | std.debug.print("\nreceived on server: {s}\n", .{buf[0..bytes_read]}); 55 | } 56 | std.time.sleep(std.time.ns_per_ms * 200); 57 | } 58 | -------------------------------------------------------------------------------- /poc/ipc_pipes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | 4 | // Communication from Client to Server. 5 | pub fn main() !void { 6 | var fds = try os.pipe(); 7 | var read_end = fds[0]; 8 | var write_end = fds[1]; 9 | const pid = try os.fork(); 10 | if (pid == 0) { 11 | // Client writes 12 | os.close(read_end); 13 | var write_stream = std.fs.File{ 14 | .handle = write_end, 15 | .capable_io_mode = .blocking, 16 | .intended_io_mode = .blocking, 17 | }; 18 | try write_stream.writer().writeAll("Hello!"); 19 | } else { 20 | // Server reads 21 | os.close(write_end); 22 | var read_stream = std.fs.File{ 23 | .handle = read_end, 24 | .capable_io_mode = .blocking, 25 | .intended_io_mode = .blocking, 26 | }; 27 | var buf = [_]u8{0} ** 6; 28 | const read_bytes = try read_stream.reader().read(buf[0..]); 29 | std.debug.print("read from buf: {s}\n", .{buf[0..read_bytes]}); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /poc/ipc_seqpacket_socket.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | const mem = std.mem; 4 | const testing = std.testing; 5 | const assert = std.debug.assert; 6 | 7 | test "communication via seqpacket unix domain socket" { 8 | const socket = try os.socket( 9 | os.AF_UNIX, 10 | os.SOCK_SEQPACKET | os.SOCK_CLOEXEC, 11 | os.PF_UNIX, 12 | ); 13 | defer os.closeSocket(socket); 14 | 15 | const runtime_dir = try std.fmt.allocPrint(testing.allocator, "/var/run/user/1000", .{}); 16 | const subpath = "/kisa"; 17 | var path_builder = std.ArrayList(u8).fromOwnedSlice(testing.allocator, runtime_dir); 18 | defer path_builder.deinit(); 19 | try path_builder.appendSlice(subpath); 20 | std.fs.makeDirAbsolute(path_builder.items) catch |err| switch (err) { 21 | error.PathAlreadyExists => {}, 22 | else => return err, 23 | }; 24 | const filename = try std.fmt.allocPrint(testing.allocator, "{d}", .{os.linux.getpid()}); 25 | defer testing.allocator.free(filename); 26 | try path_builder.append('/'); 27 | try path_builder.appendSlice(filename); 28 | std.fs.deleteFileAbsolute(path_builder.items) catch |err| switch (err) { 29 | error.FileNotFound => {}, 30 | else => return err, 31 | }; 32 | 33 | const addr = try testing.allocator.create(os.sockaddr_un); 34 | defer testing.allocator.destroy(addr); 35 | addr.* = os.sockaddr_un{ .path = undefined }; 36 | mem.copy(u8, &addr.path, path_builder.items); 37 | addr.path[path_builder.items.len] = 0; // null-terminated string 38 | 39 | const sockaddr = @ptrCast(*os.sockaddr, addr); 40 | var addrlen: os.socklen_t = @sizeOf(@TypeOf(addr.*)); 41 | try os.bind(socket, sockaddr, addrlen); 42 | 43 | const pid = try os.fork(); 44 | if (pid == 0) { 45 | // Client 46 | const client_socket = try os.socket( 47 | os.AF_UNIX, 48 | os.SOCK_SEQPACKET | os.SOCK_CLOEXEC, 49 | os.PF_UNIX, 50 | ); 51 | defer os.closeSocket(client_socket); 52 | const message = try std.fmt.allocPrint(testing.allocator, "hello from client!", .{}); 53 | defer testing.allocator.free(message); 54 | var client_connected = false; 55 | var connect_attempts: u8 = 25; 56 | while (!client_connected) { 57 | os.connect(client_socket, sockaddr, addrlen) catch |err| switch (err) { 58 | error.ConnectionRefused => { 59 | // If server is not yet listening, wait a bit. 60 | if (connect_attempts == 0) return err; 61 | std.time.sleep(std.time.ns_per_ms * 10); 62 | connect_attempts -= 1; 63 | continue; 64 | }, 65 | else => return err, 66 | }; 67 | client_connected = true; 68 | } 69 | var bytes_sent = try os.send(client_socket, message, os.MSG_EOR); 70 | assert(message.len == bytes_sent); 71 | bytes_sent = try os.send(client_socket, message, os.MSG_EOR); 72 | assert(message.len == bytes_sent); 73 | var buf: [256]u8 = undefined; 74 | var bytes_read = try os.recv(client_socket, &buf, 0); 75 | std.debug.print("\nreceived on client: {s}\n", .{buf[0..bytes_read]}); 76 | } else { 77 | // Server 78 | std.time.sleep(std.time.ns_per_ms * 200); 79 | try os.listen(socket, 10); 80 | const accepted_socket = try os.accept(socket, null, null, os.SOCK_CLOEXEC); 81 | defer os.closeSocket(accepted_socket); 82 | 83 | var buf: [256]u8 = undefined; 84 | var counter: u8 = 0; 85 | while (counter < 2) : (counter += 1) { 86 | const bytes_read = try os.recv(accepted_socket, &buf, 0); 87 | std.debug.print("\n{d}: received on server: {s}\n", .{ counter, buf[0..bytes_read] }); 88 | } 89 | const message = try std.fmt.allocPrint(testing.allocator, "hello from server!", .{}); 90 | defer testing.allocator.free(message); 91 | var bytes_sent = try os.send(accepted_socket, message, os.MSG_EOR); 92 | assert(message.len == bytes_sent); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /poc/nonblocking_io.zig: -------------------------------------------------------------------------------- 1 | // As long as socket buffer is large enough, the operation is non-blocking. On my 2 | // linux machine unix socket's defalt write buffer size is 208 KB which is more than enough. 3 | 4 | const std = @import("std"); 5 | const os = std.os; 6 | const mem = std.mem; 7 | const net = std.net; 8 | const testing = std.testing; 9 | const assert = std.debug.assert; 10 | 11 | // For testing slow and fast clients. 12 | const delay_time = std.time.ns_per_ms * 0; 13 | var bytes_sent: usize = 0; 14 | var bytes_read: usize = 0; 15 | 16 | test "socket sends small data amounts without blocking" { 17 | const socket_path = try std.fmt.allocPrint(testing.allocator, "/tmp/poll.socket", .{}); 18 | defer testing.allocator.free(socket_path); 19 | std.fs.deleteFileAbsolute(socket_path) catch |err| switch (err) { 20 | error.FileNotFound => {}, 21 | else => return err, 22 | }; 23 | const address = try testing.allocator.create(net.Address); 24 | defer testing.allocator.destroy(address); 25 | address.* = try net.Address.initUnix(socket_path); 26 | 27 | const pid = try os.fork(); 28 | if (pid == 0) { 29 | // Client 30 | const message = try std.fmt.allocPrint(testing.allocator, "hello from client!", .{}); 31 | defer testing.allocator.free(message); 32 | var buf: [6000]u8 = undefined; 33 | 34 | std.time.sleep(delay_time); 35 | const client_socket = try os.socket( 36 | os.AF_UNIX, 37 | os.SOCK_SEQPACKET | os.SOCK_CLOEXEC, 38 | os.PF_UNIX, 39 | ); 40 | defer os.closeSocket(client_socket); 41 | try os.connect( 42 | client_socket, 43 | @ptrCast(*os.sockaddr, &address.un), 44 | @sizeOf(@TypeOf(address.un)), 45 | ); 46 | 47 | bytes_read = try os.recv(client_socket, &buf, 0); 48 | std.debug.print("received on client: {d} bytes\n", .{bytes_read}); 49 | std.time.sleep(delay_time); 50 | 51 | bytes_read = try os.recv(client_socket, &buf, 0); 52 | std.debug.print("received on client: {d} bytes\n", .{bytes_read}); 53 | std.time.sleep(delay_time); 54 | 55 | bytes_read = try os.recv(client_socket, &buf, 0); 56 | std.debug.print("received on client: {d} bytes\n", .{bytes_read}); 57 | std.time.sleep(delay_time); 58 | } else { 59 | // Server 60 | const a5000_const = [_]u8{'a'} ** 5000; 61 | const a5000 = try std.fmt.allocPrint(testing.allocator, &a5000_const, .{}); 62 | defer testing.allocator.free(a5000); 63 | 64 | const socket = try os.socket( 65 | os.AF_UNIX, 66 | os.SOCK_SEQPACKET | os.SOCK_CLOEXEC, 67 | os.PF_UNIX, 68 | ); 69 | try os.bind(socket, @ptrCast(*os.sockaddr, &address.un), @sizeOf(@TypeOf(address.un))); 70 | try os.listen(socket, 10); 71 | const client_socket = try os.accept(socket, null, null, os.SOCK_CLOEXEC); 72 | defer os.closeSocket(client_socket); 73 | 74 | const socket_abstr = std.x.os.Socket.from(client_socket); 75 | std.debug.print( 76 | "\nInitial socket write buffer size: {d}\n", 77 | .{try socket_abstr.getWriteBufferSize()}, 78 | ); 79 | 80 | bytes_sent = try os.send(client_socket, a5000, os.MSG_EOR | os.MSG_DONTWAIT); 81 | assert(a5000.len == bytes_sent); 82 | 83 | try socket_abstr.setWriteBufferSize(5000); 84 | 85 | bytes_sent = try os.send(client_socket, a5000, os.MSG_EOR | os.MSG_DONTWAIT); 86 | assert(a5000.len == bytes_sent); 87 | 88 | try socket_abstr.setWriteBufferSize(4000); 89 | 90 | try std.testing.expectError( 91 | error.WouldBlock, 92 | os.send(client_socket, a5000, os.MSG_EOR | os.MSG_DONTWAIT), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /poc/poll_sockets.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | const mem = std.mem; 4 | const net = std.net; 5 | const testing = std.testing; 6 | const assert = std.debug.assert; 7 | 8 | // For testing slow and fast clients. 9 | const delay_time = std.time.ns_per_ms * 100; 10 | const clients_count = 40; 11 | // 1. Connect 12 | // 2. Write 13 | // 3. Disconnect 14 | const how_many_events_expected = clients_count * 3; 15 | 16 | fn startClient(address: *net.Address) !void { 17 | const message = try std.fmt.allocPrint(testing.allocator, "hello from client!", .{}); 18 | defer testing.allocator.free(message); 19 | var buf: [256]u8 = undefined; 20 | 21 | std.time.sleep(delay_time); 22 | const client_socket = try os.socket( 23 | os.AF_UNIX, 24 | os.SOCK_SEQPACKET | os.SOCK_CLOEXEC, 25 | os.PF_UNIX, 26 | ); 27 | defer os.closeSocket(client_socket); 28 | try os.connect( 29 | client_socket, 30 | @ptrCast(*os.sockaddr, &address.un), 31 | @sizeOf(@TypeOf(address.un)), 32 | ); 33 | 34 | std.time.sleep(delay_time); 35 | const bytes_sent = try os.send(client_socket, message, os.MSG_EOR); 36 | std.debug.print("client bytes sent: {d}\n", .{bytes_sent}); 37 | assert(message.len == bytes_sent); 38 | const bytes_read = try os.recv(client_socket, &buf, 0); 39 | std.debug.print( 40 | "received on client: {s}, {d} bytes\n", 41 | .{ buf[0..bytes_read], bytes_read }, 42 | ); 43 | } 44 | 45 | test "poll socket for listening and for reading" { 46 | const socket_path = try std.fmt.allocPrint(testing.allocator, "/tmp/poll.socket", .{}); 47 | defer testing.allocator.free(socket_path); 48 | std.fs.deleteFileAbsolute(socket_path) catch |err| switch (err) { 49 | error.FileNotFound => {}, 50 | else => return err, 51 | }; 52 | const address = try testing.allocator.create(net.Address); 53 | defer testing.allocator.destroy(address); 54 | address.* = try net.Address.initUnix(socket_path); 55 | 56 | const pid = try os.fork(); 57 | if (pid == 0) { 58 | // Client 59 | var threads: [clients_count]std.Thread = undefined; 60 | for (threads) |*thr| { 61 | thr.* = try std.Thread.spawn(.{}, startClient, .{address}); 62 | } 63 | for (threads) |thr| { 64 | std.Thread.join(thr); 65 | } 66 | } else { 67 | // Server 68 | var buf: [256]u8 = undefined; 69 | const message = try std.fmt.allocPrint(testing.allocator, "hello from server!", .{}); 70 | defer testing.allocator.free(message); 71 | 72 | const socket = try os.socket( 73 | os.AF_UNIX, 74 | os.SOCK_SEQPACKET | os.SOCK_CLOEXEC, 75 | os.PF_UNIX, 76 | ); 77 | try os.bind(socket, @ptrCast(*os.sockaddr, &address.un), @sizeOf(@TypeOf(address.un))); 78 | try os.listen(socket, 10); 79 | 80 | const FdType = enum { listen, read_write }; 81 | 82 | var fds = std.ArrayList(os.pollfd).init(testing.allocator); 83 | defer { 84 | for (fds.items) |fd| os.closeSocket(fd.fd); 85 | fds.deinit(); 86 | } 87 | var fd_types = std.ArrayList(FdType).init(testing.allocator); 88 | defer fd_types.deinit(); 89 | 90 | try fds.append(os.pollfd{ 91 | .fd = socket, 92 | .events = os.POLLIN, 93 | .revents = 0, 94 | }); 95 | try fd_types.append(.listen); 96 | 97 | std.debug.print("\n", .{}); 98 | var loop_counter: usize = 0; 99 | var event_counter: u8 = 0; 100 | while (true) : (loop_counter += 1) { 101 | std.debug.print("loop counter: {d}\n", .{loop_counter}); 102 | 103 | const polled_events_count = try os.poll(fds.items, -1); 104 | if (polled_events_count > 0) { 105 | var current_event: u8 = 0; 106 | var processed_events_count: u8 = 0; 107 | while (current_event < fds.items.len) : (current_event += 1) { 108 | if (fds.items[current_event].revents > 0) { 109 | processed_events_count += 1; 110 | event_counter += 1; 111 | if (fds.items[current_event].revents & os.POLLHUP != 0) { 112 | _ = fds.swapRemove(current_event); 113 | _ = fd_types.swapRemove(current_event); 114 | continue; 115 | } 116 | fds.items[current_event].revents = 0; 117 | 118 | switch (fd_types.items[current_event]) { 119 | .listen => { 120 | const accepted_socket = try os.accept( 121 | socket, 122 | null, 123 | null, 124 | os.SOCK_CLOEXEC, 125 | ); 126 | try fds.append( 127 | .{ .fd = accepted_socket, .events = os.POLLIN, .revents = 0 }, 128 | ); 129 | try fd_types.append(.read_write); 130 | }, 131 | .read_write => { 132 | const bytes_read = try os.recv(fds.items[current_event].fd, &buf, 0); 133 | std.debug.print( 134 | "received on server: {s}, {d} bytes\n", 135 | .{ buf[0..bytes_read], bytes_read }, 136 | ); 137 | const bytes_sent = try os.send( 138 | fds.items[current_event].fd, 139 | message, 140 | os.MSG_EOR, 141 | ); 142 | std.debug.print("server bytes sent: {d}\n", .{bytes_sent}); 143 | assert(message.len == bytes_sent); 144 | }, 145 | } 146 | } 147 | if (processed_events_count == polled_events_count) break; 148 | } 149 | } 150 | if (event_counter == how_many_events_expected) break; 151 | } 152 | std.debug.print("\n", .{}); 153 | std.debug.print("total events: {d}\n", .{event_counter}); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/client.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const kisa = @import("kisa"); 4 | const TerminalUI = @import("terminal_ui.zig"); 5 | const sqlite = @import("sqlite"); 6 | 7 | var sqlite_diags = sqlite.Diagnostics{}; 8 | 9 | pub fn main() anyerror!void { 10 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 11 | const ally = gpa.allocator(); 12 | 13 | var arg_it = try std.process.argsWithAllocator(ally); 14 | defer arg_it.deinit(); 15 | _ = arg_it.next() orelse unreachable; // binary name 16 | const filename = arg_it.next(); 17 | if (filename) |fname| { 18 | std.debug.print("Supplied filename: {s}\n", .{fname}); 19 | } else { 20 | std.debug.print("No filename Supplied\n", .{}); 21 | } 22 | 23 | var db = try setupDb(); 24 | 25 | const text_data = try db.oneAlloc([]const u8, ally, "SELECT data FROM kisa_text_data", .{}, .{}); 26 | const typed_text = try std.json.parse(kisa.Text, &std.json.TokenStream.init(text_data.?), .{ .allocator = ally }); 27 | defer std.json.parseFree(kisa.Text, typed_text, .{ .allocator = ally }); 28 | std.debug.print("{}\n", .{typed_text}); 29 | 30 | var ui = try TerminalUI.init(std.io.getStdIn(), std.io.getStdOut()); 31 | defer ui.deinit(); 32 | try ui.prepare(); 33 | } 34 | 35 | fn setupDb() !sqlite.Db { 36 | var db = sqlite.Db.init(.{ 37 | .mode = .{ .File = "kisa.db" }, 38 | .open_flags = .{ 39 | .write = true, 40 | .create = true, 41 | }, 42 | .diags = &sqlite_diags, 43 | }) catch |err| { 44 | std.log.err("Unable to open a database, got error {}. Diagnostics: {s}", .{ err, sqlite_diags }); 45 | return err; 46 | }; 47 | 48 | const query = @embedFile("db_setup.sql"); 49 | db.execMulti(query, .{ .diags = &sqlite_diags }) catch |err| { 50 | std.log.err("Unable to execute the statement, got error {}. Diagnostics: {s}", .{ err, sqlite_diags }); 51 | return err; 52 | }; 53 | return db; 54 | } 55 | -------------------------------------------------------------------------------- /src/db_setup.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS kisa_text_data; 2 | 3 | CREATE TABLE kisa_text_data( 4 | id INTEGER PRIMARY KEY, 5 | data TEXT 6 | ); 7 | 8 | INSERT INTO kisa_text_data (data) VALUES ( 9 | json(' 10 | { 11 | "lines": [ 12 | { 13 | "number": 1, 14 | "segments": [ 15 | { 16 | "c": "Hello", 17 | "style": { 18 | "fg": "default", 19 | "bg": "default", 20 | "fs": 0 21 | } 22 | } 23 | ] 24 | }, 25 | { 26 | "number": 2, 27 | "segments": [ 28 | { 29 | "c": "w", 30 | "style": { 31 | "fg": "red", 32 | "bg": "default", 33 | "fs": 1 34 | } 35 | }, 36 | { 37 | "c": "o", 38 | "style": { 39 | "fg": "green", 40 | "bg": "default", 41 | "fs": 3 42 | } 43 | }, 44 | { 45 | "c": "rld!", 46 | "style": { 47 | "fg": { "r": 255, "g": 0, "b": 0 }, 48 | "bg": "default", 49 | "fs": 20 50 | } 51 | } 52 | ] 53 | }, 54 | { 55 | "number": 3, 56 | "segments": [ 57 | { 58 | "c": "Hello" 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | ') 65 | ); 66 | -------------------------------------------------------------------------------- /src/kisa.zig: -------------------------------------------------------------------------------- 1 | //! Common data structures and functionality used by various components of this application. 2 | const std = @import("std"); 3 | 4 | /// Data sent to Client which represents the data to draw on the screen. 5 | pub const Text = struct { 6 | /// Main data to draw on the screen. 7 | lines: []const Line, 8 | 9 | pub const Line = struct { 10 | number: u32, 11 | segments: []const Segment, 12 | 13 | pub const Segment = struct { 14 | /// Contents 15 | c: []const u8, 16 | style: Style = .{}, 17 | }; 18 | }; 19 | 20 | pub fn format( 21 | value: Text, 22 | comptime fmt: []const u8, 23 | options: std.fmt.FormatOptions, 24 | writer: anytype, 25 | ) !void { 26 | _ = fmt; 27 | _ = options; 28 | for (value.lines) |line| { 29 | try writer.print("{d}:\n", .{line.number}); 30 | for (line.segments) |segment| { 31 | try writer.print(" {s} :: {}\n", .{ segment.c, segment.style }); 32 | } 33 | } 34 | } 35 | }; 36 | 37 | pub const Color = union(enum) { 38 | special: Special, 39 | base16: Base16, 40 | rgb: RGB, 41 | 42 | pub const Special = enum { 43 | default, 44 | 45 | pub fn jsonStringify( 46 | value: Special, 47 | options: std.json.StringifyOptions, 48 | out_stream: anytype, 49 | ) @TypeOf(out_stream).Error!void { 50 | try std.json.stringify(std.meta.tagName(value), options, out_stream); 51 | } 52 | }; 53 | 54 | pub const Base16 = enum { 55 | black, 56 | red, 57 | green, 58 | yellow, 59 | blue, 60 | magenta, 61 | cyan, 62 | white, 63 | black_bright, 64 | red_bright, 65 | green_bright, 66 | yellow_bright, 67 | blue_bright, 68 | magenta_bright, 69 | cyan_bright, 70 | white_bright, 71 | 72 | pub fn jsonStringify( 73 | value: Base16, 74 | options: std.json.StringifyOptions, 75 | out_stream: anytype, 76 | ) @TypeOf(out_stream).Error!void { 77 | try std.json.stringify(std.meta.tagName(value), options, out_stream); 78 | } 79 | }; 80 | 81 | pub const RGB = struct { 82 | r: u8, 83 | g: u8, 84 | b: u8, 85 | }; 86 | 87 | pub fn format( 88 | value: Color, 89 | comptime fmt: []const u8, 90 | options: std.fmt.FormatOptions, 91 | writer: anytype, 92 | ) !void { 93 | _ = fmt; 94 | _ = options; 95 | switch (value) { 96 | .special => |s| try writer.writeAll(std.meta.tagName(s)), 97 | .base16 => |b16| try writer.writeAll(std.meta.tagName(b16)), 98 | .rgb => |rgb| try writer.print("rgb({d},{d},{d})", .{ rgb.r, rgb.g, rgb.b }), 99 | } 100 | } 101 | }; 102 | 103 | pub const FontStyle = struct { 104 | bold: bool = false, 105 | dim: bool = false, 106 | italic: bool = false, 107 | underline: bool = false, 108 | reverse: bool = false, 109 | strikethrough: bool = false, 110 | 111 | pub fn toData(self: FontStyle) u8 { 112 | var result: u8 = 0; 113 | if (self.bold) result += 1; 114 | if (self.dim) result += 2; 115 | if (self.italic) result += 4; 116 | if (self.underline) result += 8; 117 | if (self.reverse) result += 16; 118 | if (self.strikethrough) result += 32; 119 | return result; 120 | } 121 | 122 | pub fn fromData(data: u8) FontStyle { 123 | var result = FontStyle{}; 124 | if (data & 1 != 0) result.bold = true; 125 | if (data & 2 != 0) result.dim = true; 126 | if (data & 4 != 0) result.italic = true; 127 | if (data & 8 != 0) result.underline = true; 128 | if (data & 16 != 0) result.reverse = true; 129 | if (data & 32 != 0) result.strikethrough = true; 130 | return result; 131 | } 132 | 133 | pub fn format( 134 | value: FontStyle, 135 | comptime fmt: []const u8, 136 | options: std.fmt.FormatOptions, 137 | writer: anytype, 138 | ) !void { 139 | _ = fmt; 140 | _ = options; 141 | var written = false; 142 | if (value.bold) { 143 | if (written) try writer.writeAll(","); 144 | try writer.writeAll("bold"); 145 | written = true; 146 | } 147 | if (value.dim) { 148 | if (written) try writer.writeAll(","); 149 | try writer.writeAll("dim"); 150 | written = true; 151 | } 152 | if (value.italic) { 153 | if (written) try writer.writeAll(","); 154 | try writer.writeAll("italic"); 155 | written = true; 156 | } 157 | if (value.underline) { 158 | if (written) try writer.writeAll(","); 159 | try writer.writeAll("underline"); 160 | written = true; 161 | } 162 | if (value.reverse) { 163 | if (written) try writer.writeAll(","); 164 | try writer.writeAll("reverse"); 165 | written = true; 166 | } 167 | if (value.strikethrough) { 168 | if (written) try writer.writeAll(","); 169 | try writer.writeAll("strikethrough"); 170 | written = true; 171 | } 172 | if (!written) { 173 | try writer.writeAll("none"); 174 | } 175 | } 176 | }; 177 | 178 | pub const Style = struct { 179 | fg: Color = .{ .special = .default }, 180 | bg: Color = .{ .special = .default }, 181 | /// FontStyle 182 | fs: u8 = 0, 183 | 184 | pub fn format( 185 | value: Style, 186 | comptime fmt: []const u8, 187 | options: std.fmt.FormatOptions, 188 | writer: anytype, 189 | ) !void { 190 | _ = fmt; 191 | _ = options; 192 | try writer.print("{}:{}:{}", .{ value.fg, value.bg, FontStyle.fromData(value.fs) }); 193 | } 194 | }; 195 | -------------------------------------------------------------------------------- /src/terminal_ui.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const kisa = @import("kisa"); 4 | 5 | const TerminalUI = @This(); 6 | const writer_buffer_size = 4096; 7 | const WriterCtx = std.io.BufferedWriter( 8 | writer_buffer_size, 9 | std.io.Writer(std.fs.File, std.fs.File.WriteError, std.fs.File.write), 10 | ); 11 | pub const Dimensions = struct { 12 | width: u16, 13 | height: u16, 14 | }; 15 | const Support = struct { 16 | ansi_escape_codes: bool, 17 | }; 18 | const esc = "\x1B"; 19 | const csi = esc ++ "["; 20 | const style_reset = csi ++ "0m"; 21 | const style_bold = csi ++ "1m"; 22 | const style_dim = csi ++ "2m"; 23 | const style_italic = csi ++ "3m"; 24 | const style_underline = csi ++ "4m"; 25 | const style_reverse = csi ++ "7m"; 26 | const style_strikethrough = csi ++ "9m"; 27 | const clear_all = csi ++ "2J"; 28 | const cursor_hide = csi ++ "?25l"; 29 | const cursor_show = csi ++ "?25h"; 30 | 31 | original_termios: std.os.termios, 32 | dimensions: Dimensions, 33 | in: std.fs.File, 34 | out: std.fs.File, 35 | writer_ctx: WriterCtx, 36 | support: Support, 37 | 38 | pub fn init(in: std.fs.File, out: std.fs.File) !TerminalUI { 39 | if (!in.isTty()) return error.NotTTY; 40 | 41 | var original_termios = try std.os.tcgetattr(in.handle); 42 | var termios = original_termios; 43 | 44 | // Black magic, see https://github.com/antirez/kilo 45 | termios.iflag &= ~(std.os.linux.BRKINT | std.os.linux.ICRNL | std.os.linux.INPCK | 46 | std.os.linux.ISTRIP | std.os.linux.IXON); 47 | termios.oflag &= ~(std.os.linux.OPOST); 48 | termios.cflag |= (std.os.linux.CS8); 49 | termios.lflag &= ~(std.os.linux.ECHO | std.os.linux.ICANON | std.os.linux.IEXTEN | 50 | std.os.linux.ISIG); 51 | // Polling read, doesn't block. 52 | termios.cc[std.os.linux.V.MIN] = 0; 53 | // VTIME tenths of a second elapses between bytes. Can be important for slow terminals, 54 | // libtermkey (neovim) uses 50ms, which matters for example when pressing Alt key which 55 | // sends several bytes but the very first one is Esc, so it is necessary to wait enough 56 | // time for disambiguation. This setting alone can be not enough. 57 | termios.cc[std.os.linux.V.TIME] = 1; 58 | 59 | try std.os.tcsetattr(in.handle, .FLUSH, termios); 60 | return TerminalUI{ 61 | .original_termios = original_termios, 62 | .dimensions = try getWindowSize(in.handle), 63 | .in = in, 64 | .out = out, 65 | .writer_ctx = .{ .unbuffered_writer = out.writer() }, 66 | .support = .{ .ansi_escape_codes = out.supportsAnsiEscapeCodes() }, 67 | }; 68 | } 69 | 70 | fn getWindowSize(handle: std.os.fd_t) !Dimensions { 71 | var window_size: std.os.linux.winsize = undefined; 72 | const err = std.os.linux.ioctl(handle, std.os.linux.T.IOCGWINSZ, @ptrToInt(&window_size)); 73 | if (std.os.errno(err) != .SUCCESS) { 74 | return error.IoctlError; 75 | } 76 | return Dimensions{ 77 | .width = window_size.ws_col, 78 | .height = window_size.ws_row, 79 | }; 80 | } 81 | 82 | pub fn prepare(self: *TerminalUI) !void { 83 | if (self.support.ansi_escape_codes) { 84 | try self.writer().writeAll(cursor_hide); 85 | } 86 | } 87 | 88 | pub fn deinit(self: *TerminalUI) void { 89 | if (self.support.ansi_escape_codes) { 90 | self.writer().writeAll(cursor_show) catch {}; 91 | } 92 | self.flush() catch {}; 93 | std.os.tcsetattr(self.in.handle, .FLUSH, self.original_termios) catch {}; 94 | } 95 | 96 | pub fn writer(self: *TerminalUI) WriterCtx.Writer { 97 | return self.writer_ctx.writer(); 98 | } 99 | 100 | pub fn flush(self: *TerminalUI) !void { 101 | try self.writer_ctx.flush(); 102 | } 103 | 104 | pub fn clearScreen(self: *TerminalUI) !void { 105 | if (self.support.ansi_escape_codes) { 106 | try self.writer().writeAll(clear_all); 107 | try goTo(self.writer(), 1, 1); 108 | } 109 | } 110 | 111 | pub fn writeNewline(self: *TerminalUI) !void { 112 | if (self.support.ansi_escape_codes) { 113 | try self.writer().writeAll("\n\r"); 114 | } else { 115 | try self.writer().writeAll("\n"); 116 | } 117 | } 118 | 119 | fn goTo(w: anytype, x: u16, y: u16) !void { 120 | try std.fmt.format(w, csi ++ "{d};{d}H", .{ y, x }); 121 | } 122 | 123 | fn base16LinuxConsoleNumber(base16_color: kisa.Color.Base16) u8 { 124 | return switch (base16_color) { 125 | .black => 0, 126 | .red => 1, 127 | .green => 2, 128 | .yellow => 3, 129 | .blue => 4, 130 | .magenta => 5, 131 | .cyan => 6, 132 | .white => 7, 133 | .black_bright => 8, 134 | .red_bright => 9, 135 | .green_bright => 10, 136 | .yellow_bright => 11, 137 | .blue_bright => 12, 138 | .magenta_bright => 13, 139 | .cyan_bright => 14, 140 | .white_bright => 15, 141 | }; 142 | } 143 | 144 | fn writeFg(w: anytype, color: kisa.Color) !void { 145 | switch (color) { 146 | .rgb => |c| try std.fmt.format(w, csi ++ "38;2;{d};{d};{d}m", .{ c.r, c.g, c.b }), 147 | .base16 => |c| try std.fmt.format(w, csi ++ "38;5;{d}m", .{base16LinuxConsoleNumber(c)}), 148 | .special => return error.SpecialIsNotAColor, 149 | } 150 | } 151 | 152 | fn writeBg(w: anytype, color: kisa.Color) !void { 153 | switch (color) { 154 | .rgb => |c| try std.fmt.format(w, csi ++ "48;2;{d};{d};{d}m", .{ c.r, c.g, c.b }), 155 | .base16 => |c| try std.fmt.format(w, csi ++ "48;5;{d}m", .{base16LinuxConsoleNumber(c)}), 156 | .special => return error.SpecialIsNotAColor, 157 | } 158 | } 159 | 160 | fn writeFontStyle(w: anytype, font_style: kisa.FontStyle) !void { 161 | if (font_style.bold) try w.writeAll(style_bold); 162 | if (font_style.dim) try w.writeAll(style_dim); 163 | if (font_style.italic) try w.writeAll(style_italic); 164 | if (font_style.underline) try w.writeAll(style_underline); 165 | if (font_style.reverse) try w.writeAll(style_reverse); 166 | if (font_style.strikethrough) try w.writeAll(style_strikethrough); 167 | } 168 | 169 | fn writeStyleStart(self: *TerminalUI, style: kisa.Style) !void { 170 | var w = self.writer(); 171 | if (self.support.ansi_escape_codes) { 172 | try writeFg(w, style.foreground); 173 | try writeBg(w, style.background); 174 | try writeFontStyle(w, style.font_style); 175 | } 176 | } 177 | 178 | fn writeStyleEnd(self: *TerminalUI) !void { 179 | if (self.support.ansi_escape_codes) { 180 | try self.writer().writeAll(style_reset); 181 | } 182 | } 183 | 184 | pub fn writeAllFormatted(self: *TerminalUI, style: kisa.Style, string: []const u8) !void { 185 | try self.writeStyleStart(style); 186 | try self.writer().writeAll(string); 187 | try self.writeStyleEnd(); 188 | } 189 | 190 | pub fn writeByteNTimesFormatted(self: *TerminalUI, style: kisa.Style, byte: u8, n: usize) !void { 191 | try self.writeStyleStart(style); 192 | try self.writer().writeByteNTimes(byte, n); 193 | try self.writeStyleEnd(); 194 | } 195 | 196 | // Run from project root: zig build run-terminal-ui 197 | pub fn main() !void { 198 | var file = try std.fs.cwd().openFile("src/terminal_ui.zig", .{}); 199 | const text = try file.readToEndAlloc(testing.allocator, std.math.maxInt(usize)); 200 | defer testing.allocator.free(text); 201 | var ui = try TerminalUI.init(std.io.getStdIn(), std.io.getStdOut()); 202 | defer ui.deinit(); 203 | try ui.prepare(); 204 | try ui.clearScreen(); 205 | var text_it = std.mem.split(u8, text, "\n"); 206 | const style = kisa.Style{ 207 | .foreground = .{ .base16 = .yellow }, 208 | .background = .{ .rgb = .{ .r = 33, .g = 33, .b = 33 } }, 209 | .font_style = .{ .italic = true }, 210 | }; 211 | const style2 = kisa.Style{ 212 | .foreground = .{ .base16 = .red }, 213 | .background = .{ .rgb = .{ .r = 33, .g = 33, .b = 33 } }, 214 | }; 215 | var i: u32 = 0; 216 | while (text_it.next()) |line| { 217 | i += 1; 218 | if (i % 5 == 0) { 219 | try ui.writer().writeAll(line); 220 | } else if (i % 2 == 0) { 221 | try ui.writeAllFormatted(style, line); 222 | } else { 223 | try ui.writeAllFormatted(style2, line); 224 | } 225 | try ui.writeNewline(); 226 | } 227 | } 228 | 229 | test "ui: reference all" { 230 | testing.refAllDecls(TerminalUI); 231 | } 232 | -------------------------------------------------------------------------------- /tests/keypress.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const io = std.io; 3 | const os = std.os; 4 | 5 | pub fn main() !void { 6 | // Get stdin and stdout 7 | const in_stream = io.getStdIn(); 8 | // const out_stream = io.getStdOut(); 9 | 10 | // Save current termios 11 | const original_termios = try os.tcgetattr(in_stream.handle); 12 | 13 | // Set new termios 14 | var raw_termios = original_termios; 15 | raw_termios.iflag &= 16 | ~(@as(os.tcflag_t, os.BRKINT) | os.ICRNL | os.INPCK | os.ISTRIP | os.IXON); 17 | raw_termios.oflag &= ~(@as(os.tcflag_t, os.OPOST)); 18 | raw_termios.cflag |= os.CS8; 19 | raw_termios.lflag &= ~(@as(os.tcflag_t, os.ECHO) | os.ICANON | os.IEXTEN | os.ISIG); 20 | raw_termios.cc[os.VMIN] = 0; 21 | raw_termios.cc[os.VTIME] = 1; 22 | try os.tcsetattr(in_stream.handle, os.TCSA.FLUSH, raw_termios); 23 | 24 | // Enter extended mode TMUX style 25 | // try out_stream.writer().writeAll("\x1b[>4;1m"); 26 | // Enter extended mode KITTY style 27 | // try out_stream.writer().writeAll("\x1b[>1u"); 28 | 29 | // Read characters, press q to quit 30 | var buf = [_]u8{0} ** 8; 31 | var number_read = try in_stream.reader().read(buf[0..]); 32 | while (true) : (number_read = try in_stream.reader().read(buf[0..])) { 33 | if (number_read > 0) { 34 | switch (buf[0]) { 35 | 'q' => break, 36 | else => { 37 | std.debug.print("buf[0]: {x}\r\n", .{buf[0]}); 38 | std.debug.print("buf[1]: {x}\r\n", .{buf[1]}); 39 | std.debug.print("buf[2]: {x}\r\n", .{buf[2]}); 40 | std.debug.print("buf[3]: {x}\r\n", .{buf[3]}); 41 | std.debug.print("buf[4]: {x}\r\n", .{buf[4]}); 42 | std.debug.print("buf[5]: {x}\r\n", .{buf[5]}); 43 | std.debug.print("buf[6]: {x}\r\n", .{buf[6]}); 44 | std.debug.print("buf[7]: {x}\r\n", .{buf[7]}); 45 | std.debug.print("\r\n", .{}); 46 | buf[0] = 0; 47 | buf[1] = 0; 48 | buf[2] = 0; 49 | buf[3] = 0; 50 | buf[4] = 0; 51 | buf[5] = 0; 52 | buf[6] = 0; 53 | buf[7] = 0; 54 | }, 55 | } 56 | } 57 | std.time.sleep(10 * std.time.ns_per_ms); 58 | } 59 | 60 | // Exit extended mode TMUX style 61 | // try out_stream.writer().writeAll("\x1b[>4;0m"); 62 | // Enter extended mode KITTY style 63 | // try out_stream.writer().writeAll("\x1b[