use Cnile Timespec use Array # Array Iter instance use Bits u32-and, u32-ors, u32-or, u32-neg use Error unreachable, panic', todo, assert, or-fail, panic use Mem cast, cast-ptr, undefined, i32-size, i32-u32, u32-size, u32-i32, i64-i32, size-i32, size-i64, size-u32 use Misc (fst, snd) use Term ColorPalette, Color8, Color16, RGB, Color use Alloc (Allocator) use Slice (Slice) use StrView (StrView) use Scan use Iter next, to, for-each, cartesian, take, zip, from, count, map, sum use Str (Str(chars)) use Time # TODO: write how the coordinates work. They should be coordinates of cells on screen, but I forgot... # (20.06.26): Open question: should Tui store Chars or Scalars? # the problem i encountered is that Chars depend on some external state (kd text editor in this case) # and that they might either change (and the rendering breaks, since no changes are detected) # or have undefined behavior (piece table is "immutable", but adding to the `append` might cause reallocation and invalide the pointers in Char) # ideally, it should store chars (grapheme clusters) # but in practice terminals don't properly handle them anyway # my answer will depend on how terminals handle it (check that mitchell hashimoto blog post and how the new standard thing works and its adoption) # a few ways we can solve these (in no particular order) # 1. use scalars (Char.Scalar). no surprises and nice for GUIs. worse for text editors and wherever there is data entry/display # (but it won't matter if terminals cant render it properly) # 2. leave it as is, let the use care about it. must add an "official" mechanism to invalidate the "previous" buffer. # fastest put-char(), lightest library, but error prone and rerender more often # (in case of the kd text editor, forces us to rerender everything every time we add to the append string in the piece table # or everytime with current TextBuf) # 3. (worse than 4 imo) copy the underlying string representations every time # (to some shared buffer? - problem when a new char is longer than the older one.) # 4. try scalarize. if scalar, then just put it in. if cluster, copy it (Allocator.allocate with the tui.al) # then, every time we overwrite the cell, we first check if it's a cluster. if it is, we must free it. # fast for general case (though we have to scalarize every time, but it should be pretty fast); no need to invalidate buffers. # put-char() can now allocate and fail # # 5. i keep thinking about this. add a RelPtr (relative pointer) type and make it the third case (Scalar, Ref, RelRef). otherwise, same as 2. # this will allow us to use immutable data structures without worrying about their position in memory. # the weirdest option :))) # my current favourite is 4! (but it allocates too often? makes it less robust??? we still reallocate when resizing though...) # there should be a global termios for signal handlers. # orig-termios = @undefined # but right now, idc. # orig-termios-p = &orig-termios fn disable-raw-mode(orig-termios Ptr Cnile.Termios) if Cnile.tcsetattr(Cnile.stdin-fileno(), Cnile.tcsa-flush(), orig-termios cast-ptr()) == -1 panic('tcsetattr') fn enable-raw-mode () -> Cnile.Termios orig-termios =& undefined() if Cnile.tcgetattr(Cnile.stdin-fileno(), orig-termios cast-ptr()) == -1 panic('tcgetattr') raw = orig-termios& raw <.c-lflag= raw.c-lflag u32-and(u32-ors( [ Cnile.echo() , Cnile.icanon() , Cnile.isig() , Cnile.iexten() as U32 ]) u32-neg()) raw <.c-iflag= raw.c-iflag u32-and(u32-ors( [ Cnile.brkint() , Cnile.icrnl() , Cnile.inpck() , Cnile.istrip() , Cnile.ixon() as U32 ]) u32-neg()) raw <.c-oflag= raw.c-oflag u32-and(u32-ors( [ Cnile.opost() as U32 ]) u32-neg()) raw <.c-cflag= raw.c-cflag u32-or(Cnile.cs8()) Array.set(&raw.c-cc, Cnile.vmin(), 0) Array.set(&raw.c-cc, Cnile.vtime(), 0) if Cnile.tcsetattr(Cnile.stdin-fileno(), Cnile.tcsa-flush(), cast-ptr(&raw)) == -1 panic('tcsetattr') return orig-termios& Tui width U32 height U32 target-fps U32 actual-fps U32 palette ColorPalette last-sync Timespec fps-ts Timespec fps-count U32 # orig-termios Cnile.Termios should-redraw Bool # NOTE: put it either here or in Screen. NOTE2: actually, screen already had it bruh. - `requires-full-redraw` # I'll see how I approach non-screen clearing stuff like tqdm(), which would not use the Screen part and finally decide where it belongs. fn read-byte(timeout-ms I32) -> Maybe Cnile.AsciiChar pfd = Cnile.Pollfd { fd: 0, events: 1, revents: 0 } if Cnile.poll(Mem.cast-ptr(&pfd), 1, timeout-ms) <= 0 return None c = Mem.zeroed() as Cnile.AsciiChar if Cnile.read(Cnile.stdin-fileno(), (&c) cast-ptr(), 1) <= 0 return None return Just(c) fn flush-stdout() # Cnile.fflush((Cnile.stdout-fileno() as Size) cast()) # i don't have access to stdout, so we flush all Cnile.fflush(Mem.null-ptr()) fn enable-mouse() Str.print-str('\x1b[?1000h\x1b[?1006h') fn disable-mouse() Str.print-str('\x1b[?1000l\x1b[?1006l') # orig-termios =& undefined() tui-global-state =& { orig-termios: undefined() , should-resize: False } fn mk() -> Tui Term.enable-alternative-screen-buffer() tui-global-state <&.orig-termios= enable-raw-mode() Term.hide-cursor() Term.reset-colors() Term.clear-screen() enable-mouse() flush-stdout() # it dont want to compile for some reason... still the same problem with unions. # @register-signal(Cnile.sigabrt(), fn(_)) # disable-raw-mode(&tui-global-state&.orig-termios) # Term.disable-alternative-screen-buffer() palette = Term.query-palette() dims = Term.get-dimensions() fps = 60 last-sync = Time.now() @register-signal(Cnile.sigwinch(), fn (_)) tui-global-state <&.should-resize= True return Tui { width: dims fst() , height: dims snd() , target-fps: fps , actual-fps: 0 , last-sync: last-sync , fps-ts: last-sync , fps-count: 0 # , orig-termios: og-termios , palette: palette , should-redraw: True # True for first draw. } fn sync(tui Ptr Tui) if tui&.target-fps == 0 return frame-ns = 1000000000 / (tui&.target-fps u32-size() size-i64()) now =& Mem.undefined() Cnile.clock-gettime(Cnile.clock-monotonic(), now) elapsed-ns = (now&.tv-sec - tui&.last-sync.tv-sec) * 1000000000 + (now&.tv-nsec - tui&.last-sync.tv-nsec) sleep-ns = frame-ns - elapsed-ns if sleep-ns > 0 ts =& Timespec { tv-sec: 0, tv-nsec: sleep-ns } Cnile.nanosleep(ts, Mem.null-ptr()) last-sync =& Mem.undefined() Cnile.clock-gettime(Cnile.clock-monotonic(), last-sync) tui <&.last-sync= last-sync& tui <&.fps-count= tui&.fps-count + 1 fps-elapsed-ms = (tui&.last-sync.tv-sec - tui&.fps-ts.tv-sec) * 1000 + (tui&.last-sync.tv-nsec - tui&.fps-ts.tv-nsec) / 1000000 if fps-elapsed-ms >= 1000 tui <&.actual-fps= tui&.fps-count tui <&.fps-count= 0 tui <&.fps-ts= tui&.last-sync fn deinit(tui Ptr Tui) disable-mouse() disable-raw-mode(&tui-global-state&.orig-termios) Term.show-cursor() Term.reset-colors() Term.clear-screen() Term.reset-cursor-position() Term.disable-alternative-screen-buffer() flush-stdout() ### INPUT MouseButton MouseLeft MouseMiddle MouseRight ScrollUp ScrollDown inst Eq MouseButton # NOTE: maybe add an intrinsic to get tag value? eq (l, r): Mem.cast-on-zeroed(l) == (Mem.cast-on-zeroed(r) as U32) MouseEvent button MouseButton x I32 y I32 pressed Bool Key Escape Enter Tab Backspace Char Cnile.AsciiChar Ctrl Cnile.AsciiChar Up Down Left Right Home End PageUp PageDown Delete Insert F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 InputEvent Key Key Mouse MouseEvent fn btn-to-mouse-button (btn I64): (case i64-i32(btn)) 0: MouseLeft 1: MouseMiddle 2: MouseRight 64: ScrollUp 65: ScrollDown _: panic'('incorrect mouse button code: \(btn)') fn parse-ss3(c Cnile.AsciiChar) -> Maybe Key if c == 'A' return Just(Up) if c == 'B' return Just(Down) if c == 'C' return Just(Right) if c == 'D' return Just(Left) if c == 'H' return Just(Home) if c == 'F' return Just(End) if c == 'P' return Just(F1) if c == 'Q' return Just(F2) if c == 'R' return Just(F3) if c == 'S' return Just(F4) return None fn parse-csi(seq Slice Cnile.AsciiChar) -> Maybe InputEvent if seq.count == 0 return None last = seq[seq.count - 1] if seq[0] == '<' and (last == 'M' or last == 'm') # TODO: don't use the UTF-8 char scanner and just do Slice Cnile.AsciiChar thing. (make a separate scan-int'()?) sc =& Scan.mk-from-str(StrView.substr(StrView.from-ascii-slice(seq), 1, seq.count)) case Scan.scan-int(sc) None return None Just(btn) next(sc) # skip ';' case Scan.scan-int(sc) None return None Just(mx) next(sc) # skip ';' case Scan.scan-int(sc) None return None Just(my) return Just(Mouse(MouseEvent { button: btn-to-mouse-button(btn) , x: mx i64-i32() - 1 , y: my i64-i32() - 1 , pressed: last == 'M' })) if seq.count == 1 if last == 'A' return Just(Key(Up)) if last == 'B' return Just(Key(Down)) if last == 'C' return Just(Key(Right)) if last == 'D' return Just(Key(Left)) if last == 'H' return Just(Key(Home)) if last == 'F' return Just(Key(End)) return None if last == '~' sc =& Scan.mk(StrView.from-ascii-slice(seq)) case Scan.scan-int(sc) None return None Just(n) return (case i64-i32(n)) 1: Just(Key(Home)) 2: Just(Key(Insert)) 3: Just(Key(Delete)) 4: Just(Key(End)) 5: Just(Key(PageUp)) 6: Just(Key(PageDown)) 15: Just(Key(F5)) 17: Just(Key(F6)) 18: Just(Key(F7)) 19: Just(Key(F8)) 20: Just(Key(F9)) 21: Just(Key(F10)) 23: Just(Key(F11)) 24: Just(Key(F12)) _: None return None fn read-key() -> Maybe InputEvent ch =& Mem.undefined() case read-byte(0) None return None Just(x) ch <&= x # normalize (and just map?) if Char.ascii-u8(ch&) == 13 return Just(Key(Enter)) # actually, keep tabs as chars bruh. # if Char.ascii-u8(ch&) == 9 # return Just(Key(Tab)) if Char.ascii-u8(ch&) == 127 return Just(Key(Backspace)) if Char.ascii-u8(ch&) < 27 and Char.ascii-u8(ch&) /= 9 # Ctrl codes (Tab stays as Char) letter = ch& Char.ascii-u8() Mem.u8-u32() u32-or(Convert.from-hex('60')) Mem.u32-u8() Char.u8-ascii() return Just(Key(Ctrl(letter))) if Char.ascii-u8(ch&) /= 27 # ESCAPE if Char.ascii-u8(ch&) > 127 # IGNORE UTF-8 FOR NOW. return read-key() else return Just(Key(Char(ch&))) ch2 =& Mem.undefined() case read-byte(50) None return Just(Key(Escape)) Just(x) ch2 <&= x if ch2& == '[' seq =& Mem.zeroed() as Array 32 Cnile.AsciiChar slen = 0 while slen < 31 sc =& Mem.undefined() case read-byte(50) None break Just(x) sc <&= x Array.set(seq, Mem.i32-size(slen), sc&) slen <= slen + 1 if Char.ascii-u8(sc&) >= 64 and Char.ascii-u8(sc&) <= 126 break return parse-csi(Array.as-slice(seq) Slice.subslice(0, Mem.i32-size(slen))) if ch2& == 'O' sc =& Mem.undefined() case read-byte(50) None return Just(Key(Escape)) Just(x) sc <&= x case parse-ss3(sc&) None return None Just(k) return Just(Key(k)) return Just(Key(Escape)) fn update-dimensions (tui Ptr Tui) if not tui-global-state&.should-resize return False tui <&.should-redraw= True tui-global-state <&.should-resize= False dim = Term.get-dimensions() w = dim fst() h = dim snd() tui <&.width= w tui <&.height= h return True fn read-event (tui Ptr Tui) -> Maybe InputEvent tui update-dimensions() case read-key() None return None Just(ev) tui <&.should-redraw= True return Just(ev) # NOTE: always use as a first argument, because it clears the redraw flag! # NOTE2: that's actually stupid. mouse movements also trigger events. # The ideal way would be to compare current program state with the previous state, or make elif chains which end in else, which triggers "no-action" or something. fn should-redraw (tui Ptr Tui) -> Bool redraw = tui&.should-redraw tui <&.should-redraw = False return redraw # another idea! # `should-redraw()` always be the first argument to the drawing condition # because it clears the `tui.should-redraw` flag. (otherwise, stuff might get drawn twice when there are other reasons for redrawing.) # so, force it with a HOF: # WARNING: bad codegen, even with -O3: # CPU USAGE: # `should-redraw()`: 1% # `draw()`: 5% <- why is this so large? or is it a bug in my code? i guess the unoptimized function environment goes like this bruh. # no capping: 8% fn draw(tui Ptr Tui, cond, body () -> ()) if tui should-redraw() or cond body() ### SCREEN # TODO: handle wcwidth stuff. maybe keep track of width in the Cell and when displaying cut the display early. Cell c Char fg Color bg Color char-width I32 # set along the char yo. # todo: transparent? inst Eq Cell eq (l, r) if l.c /= r.c return False if l.fg /= r.fg return False if l.bg /= r.bg return False return True fn invert-cell-colors (cell Ptr Cell) fg = cell&.fg cell <&.fg = cell&.bg cell <&.bg = fg fn default-cell (): Cell { c: ' ' , fg: Term.ColorDefault , bg: Term.ColorDefault , char-width: 1 } Screen al Allocator tui Ptr Tui current Slice Cell previous Slice Cell requires-full-redraw Bool default-fg Color default-bg Color fn mk-screen(tui Ptr Tui, al Allocator) cur = al Alloc.allocate((tui&.width * tui&.height) u32-size()) cur Slice.map(fn _: default-cell()) prev = al Alloc.allocate((tui&.width * tui&.height) u32-size()) prev Slice.map(fn _: default-cell()) return Screen { current: cur , previous: prev , al: al , tui: tui , requires-full-redraw: True , default-fg: Term.ColorDefault , default-bg: Term.ColorDefault } fn free-screen(screen Ptr (Screen Allocator)) # TODO: the parameter is not defined in the declaration, so it should be HIDDEN al = screen&.al al Alloc.free(screen&.current) al Alloc.free(screen&.previous) fn clear-screen(screen Ptr (Screen Allocator)) screen&.current Slice.map(fn _: default-cell()) # fixed wcwidth as to how it's actually rendered fn rendered-wcwidth (c Char): Math.max(Term.wcwidth(c), 1) RenderState x U32 y U32 fg Color bg Color changes U32 fn emit-cell(rs Ptr RenderState, c Ptr Cell, x U32, y U32) if x /= rs&.x or y /= rs&.y Term.move-cursor-to(x, y) rs <&.x= x rs <&.y= y # preprocess some stuff - handle disallowed chars. char = c&.c bg = c&.bg if c&.char-width == -1 char <= ' ' bg <= Term.Color8(Term.Red8) if rs&.fg /= c&.fg Term.set-fg(c&.fg) rs <&.fg= c&.fg if rs&.bg /= bg Term.set-bg(bg) rs <&.bg= bg Str.print-str(char) char-width = Math.max(c&.char-width, 1) Mem.i32-u32() rs <&.x = rs&.x + char-width fn render-screen(screen Ptr (Screen Allocator)) w = screen&.tui&.width u32-i32() h = screen&.tui&.height u32-i32() rs =& RenderState { x: 0, y: 0, fg: Term.ColorDefault, bg: Term.ColorDefault, changes: 0 } Term.move-cursor-to(0, 0) for y in 0 to (h - 1) # (20.06.26): fyi, we changed the way we render characters. when we "packed" wcwidth > 1 characters, # then we couldn't really index into screen positions (we had to look up the width for all previous characters.) # i realized this was unacceptable in a Tui application (where screen position is for layouting n shii) # -- after i unpacked stuff, we had problems where some characters weren't updated. check the stuff below. x-v = 0 while x-v < w i = (y * w + x-v) i32-size() cur = Slice.get-ptr(screen&.current, i) char-width = Math.max(cur&.char-width, 1) curs = Slice.subslice(screen&.current, i, i + char-width Mem.i32-size()) prevs = Slice.subslice(screen&.previous, i, i + char-width Mem.i32-size()) # (20.06.26): it got complicated due to wcwidth>1/utf8 char handling # stuff wasn't getting updated when needed. i don't know why. # checking all cells and updating them fixed it. but i don't know why this works :) (sub 40 iq) if screen&.requires-full-redraw or curs zip (prevs) Iter.any(fn ((c, p)): c /= p) rs <&.changes= rs&.changes + 1 emit-cell(rs, cur, x-v i32-u32(), y i32-u32()) curs Slice.copy-to(prevs) x-v <= x-v + char-width screen <&.requires-full-redraw= False Term.reset-colors() flush-stdout() return rs&.changes # calls update-dimensions() anyway. fn resize-screen-if-needed(screen Ptr (Screen Allocator)) -> Bool tui = screen&.tui updated-dimensions = tui update-dimensions() if not updated-dimensions return False screen <&.requires-full-redraw = True w = screen&.tui&.width h = screen&.tui&.height nusz = u32-size(w * h) if nusz <= screen&.current.count return True nuscreen = tui mk-screen(screen&.al) screen <&.current= nuscreen.current screen <&.previous= nuscreen.previous # TODO: free? return True fn set-screen-fg (screen Ptr (Screen Allocator), c Color) screen <&.default-fg= c fn set-screen-bg (screen Ptr (Screen Allocator), c Color) screen <&.default-bg= c fn set-screen-colors (screen Ptr (Screen Allocator), fg Color, bg Color) screen <&.default-fg= fg screen <&.default-bg= bg fn invert-screen-colors (screen Ptr (Screen Allocator)) fg = screen&.default-fg screen <&.default-fg = screen&.default-bg screen <&.default-bg = fg fn get-cell-ptr (screen Ptr (Screen Allocator), x I32, y I32) -> Maybe (Ptr Cell) w = screen&.tui&.width u32-i32() if x < 0 or x >= w return None if y < 0 or y >= screen&.tui&.height u32-i32() return None i = (y * w + x) i32-size() return Just(Slice.get-ptr(screen&.current, i)) fn fill-default(screen Ptr (Screen Allocator)) # NOTE: a faster way would be to just memcpy! this is correct, but slower. for y in 0 to (screen&.tui&.height u32-i32() - 1) for x in 0 to (screen&.tui&.width u32-i32() - 1) cell = screen get-cell-ptr(x, y) or-fail('no cell??') cell <&.bg= screen&.default-bg # non-wrapping drawing thing. fn put-char (screen Ptr (Screen Allocator), c Char, x I32, y I32) w = screen&.tui&.width u32-i32() if x >= w or y >= screen&.tui&.height u32-i32() or x < 0 or y < 0 return i = (y * w + x) i32-size() fg = screen&.default-fg bg = screen&.default-bg c = c char-width = Term.wcwidth(c) # its so baaad. we need to keep track if we're drawing over wcwidth>1 chars and reset the cells there if needed. # (20.06.26): maybe the packed representation is better? we can just translate the coordinates! # but then field access is O(n) and i should think of an interface that avoids recalculating offsets. # ... no. if we pack it and then replace a wide char with a non-wide one, we would have to move everything a cell. it's crap too. # anyway. we're doing so much magic that option 4 would be nice. if x > 0 and screen&.current[i - 1].char-width > 1 # i checked that max wcwidth == 2 i guess. pc = screen&.current[i - 1] screen&.current Slice.set(i - 1, { c: ' ', fg: pc.fg, bg: pc.bg, char-width: 1 }) # it means it's a control character (or an invalid one) # keep char-width -1 to signal it's an invalid char (and render it differently) screen&.current Slice.set(i, Cell { c, fg, bg, char-width }) for xx in (x + 1) to (Math.min(x + char-width - 1, w)) i = (y * w + xx) i32-size() # screen&.current Slice.set(i, Cell { c: '\0', fg, bg, char-width: -1 }) screen&.current Slice.set(i, Cell { c: ' ', fg, bg, char-width: 1 }) # display space (for example, when a wcwidth>1 char gets cut off.) fn draw-str (screen Ptr (Screen Allocator), s Str, x I32, y I32) w = screen&.tui&.width u32-i32() if y < 0 or y >= screen&.tui&.height u32-i32() return i = y * w + x x = Math.min(x, w) # clamp the result to bounds max-len = (w - x) i32-size() xx = 0 for c in s chars() screen put-char(c, x + xx, y) xx <= xx + rendered-wcwidth(c) fn draw-str-right(screen Ptr (Screen Allocator), s Str, x I32, y I32) slen = s chars() map(rendered-wcwidth) sum() w = screen&.tui&.width u32-i32() x = w - x - slen screen draw-str(s, x, y) fn draw-str-center(screen Ptr (Screen Allocator), s Str, x I32, y I32) slen = s chars() count() size-i32() x = x - slen / 2 screen draw-str(s, x, y)