use Tui Screen use StrBuilder StrBuilder use Misc own use Cnile (AsciiChar) use TextBuf TextBuf use Pos Pos use Pane Pane set-mode use Theme Theme use Config Config # some structure: # Editor # | # [Pane] # | <- panes can have the same textbuf open. (and have different cursor positions on them and stuff); # | opening a new file should go through the map first. # [Filename -> TextBuf] MenuItem al key AsciiChar description StrView # cast to Editor, we don't yet have recursive types. action (Ptr (Pane al)) -> () Menu al name StrView items Slice (MenuItem al) EditorMode al Normal Cmd Pos (StrBuilder al) Search Pos (StrBuilder al) Menu (Menu al) inst Str EditorMode chars (self _) c = @undefined case self Normal c <= 'Normal' StrCase1() Cmd(pos, sb) c <= 'Cmd(\(pos)|\(sb))' StrCase2() Search(pos, sb) c <= 'Search(\(pos)|\(sb))' StrCase2() Menu(menu) c <= 'Menu' StrCase1() _ return todo() return c chars() Editor al al al running Bool pane Pane al clipboard Maybe StrView search-term Maybe StrView mode EditorMode al msg Maybe StrView # theme and stuff. cfg Ptr Config og-theme Ptr Theme # hackish. stores the theme we currently have set (but might not be "trying out" with the :theme command) # use this to get the current pane. fn pane (ed Ptr (Editor al)) -> Ptr (Pane al): &ed&.pane fn copy-selection-to-clipboard (ed Ptr (Editor al)) <= Allocator al let (from, to) = Pane.selection(ed pane()) cpd = ed&.pane.buf TextBuf.str-between(from, to) ed&.clipboard if-just(fn(cp)) cp StrView.free(ed&.al) ed <&.clipboard= Just(cpd) fn reset-msg (ed Ptr (Editor al)) <= Allocator al ed&.msg if-just(fn(msg)) StrView.free(msg, ed&.al) ed <&.msg= None fn set-msg (ed Ptr (Editor al), s Str) <= Allocator al ed reset-msg() ed <&.msg = s StrBuilder.mk-dyn-str(ed&.al) Just() fn live-cmd (ed Ptr (Editor al), cmd StrView) <= Allocator al case cmd parse-int() Just(line) line = line Mem.i64-i32() Math.clamp(1, ed pane()&.buf TextBuf.num-lines()) # 1..lines range line = line - 1 # lines start from 1 on-screen ed pane() Pane.set-cursors({ line, bi: 0 }, None, Pane.NoChanges) None sc =& Scan.mk-from-strview(cmd) cmd = sc Scan.take-str-while(is-alpha) if cmd == 'theme' sc Scan.drop-str-while(is-whitespace) theme-name = sc Scan.take-str-while(fn c: not c is-whitespace()) ed <&.cfg&.theme= Theme.match-theme(theme-name) or-else(ed&.og-theme) else pass fn run-cmd (ed Ptr (Editor al), s StrView) <= Allocator al sc =& Scan.mk-from-strview(s) sc Scan.drop-str-while(is-whitespace) cmd = sc Scan.take-str-while(is-not-whitespace) sc Scan.drop-str-while(is-whitespace) # use 'case' in the future :) if cmd == 'q' ed <&.running= False elif cmd == 'wq' pass elif cmd == 'w' filename = sc Scan.take-str-while(is-not-whitespace) filename = if filename null(): None else Just(filename) write-result = ed pane()&.buf TextBuf.write-to-file(filename) case write-result None if filename is-none() ed set-msg('could not save changes (filename not set!!)') else ed set-msg('could not write to file \(filename)') Just(bytes-written) ed set-msg('written \(bytes-written) bytes') elif cmd == 'o' todo() pass elif cmd == 'ln' what = sc Scan.take-str-while(is-not-whitespace) if what == 'on' ed <&.cfg&.display-line-numbers= True elif what == 'off' ed <&.cfg&.display-line-numbers= False elif what chars() count() >= 3 and 'absolute' Str.begins-with(what) ed <&.cfg&.relative-line-numbers= False elif what chars() count() >= 3 and 'relative' Str.begins-with(what) ed <&.cfg&.relative-line-numbers= True elif cmd == 'ft' type = sc Scan.take-str-while(is-not-whitespace) tb = ed pane()&.buf if type == 'text' tb TextBuf.set-filetype(TextBuf.Text) elif type == 'kc' tb TextBuf.set-filetype(TextBuf.KC) elif type == 'md' tb TextBuf.set-filetype(TextBuf.Markdown) else ed set-msg('unknown file type') elif cmd == 'testerror' panic('test error') # live-cmd() elif cmd == 'theme' pass elif cmd all(is-digit) pass else ed set-msg('unknown command') fn live-search (ed Ptr (Editor al), begin-pos Pos, query StrView) if not query null() case ed pane() Pane.search-from(begin-pos, query) Just((sel, cur)) ed pane() Pane.set-cursors(cur, Just(sel), Pane.NoChanges) None ed pane() Pane.set-cursors(begin-pos, None, Pane.NoChanges) else ed pane() Pane.set-cursors(begin-pos, None, Pane.NoChanges) fn next-match (ed Ptr (Editor al)) case ed&.search-term None return Just(st) case ed pane() Pane.search-from(ed pane() Pane.max-pos(), st) None pass Just((sel, cur)) ed pane() Pane.set-cursors(cur, Just(sel), Pane.UpdateVI) fn prev-match (ed Ptr (Editor al)) case ed&.search-term None return Just(st) case ed pane() Pane.search-back(ed pane() Pane.min-pos(), st) None pass Just((sel, cur)) ed pane() Pane.set-cursors(cur, Just(sel), Pane.UpdateVI) fn enmenue (ed Ptr (Editor al), menu) ed <&.mode= Menu(menu) EditorAllocator = Alloc.CAllocator goto-menu = Menu { name: 'goto' , items: [ MenuItem { key: 'l', description: 'move to end of line', action: fn pane: pane Pane.move-to-end() } , MenuItem { key: 'h', description: 'move to beginning of line', action: fn pane: pane Pane.move-to-beginning-indented() } , MenuItem { key: 'g', description: 'move up yah', action: fn pane: pane Pane.set-cursors({ line: 0, bi: 0 }, None, Pane.UpdateVI) } ] } as Menu EditorAllocator space-menu = Menu { name: 'space' , items: [ MenuItem { key: 'f', description: 'open dir bruh', action: fn pane: pane Pane.move-to-end() } , MenuItem { key: 'y', description: 'copy to system clipboard', action: fn pane: () } # , MenuItem { key: 'g', description: 'move up yah', action: fn pane: pane Pane.set-cursors({ line: 0, bi: 0}, None, Pane.NoChanges) } ] } as Menu EditorAllocator fn handle-normal-key (ed Ptr (Editor EditorAllocator), key Tui.Key) case key Tui.Char(c) if c == 'q' ed <&.running= False elif c == 'h' Pane.move-left(ed pane()) elif c == 'l' Pane.move-right(ed pane()) elif c == 'j' Pane.move-down(ed pane(), 1) elif c == 'k' Pane.move-up(ed pane(), 1) elif c == 'i' ed pane() set-mode(Pane.Insert) elif c == 'I' ed pane() Pane.move-to-beginning-indented() ed pane() set-mode(Pane.Insert) elif c == 'A' ed pane() Pane.move-to-end() ed pane() set-mode(Pane.Insert) elif c == 'o' # NOTE(24.06.26): slower compile times with this block on. bruh. what. pane = ed pane() indent = pane Pane.indent-at-line(pane&.cursor.line) pane Pane.move-to-end() pane Pane.add-str-at-char('\n') pane Pane.move-right() pane Pane.indent-at-current(indent) pane set-mode(Pane.Insert) elif c == 'O' # NOTE(24.06.26): also slower due to this. we're reached 300ms. wtffff is going on. pane = ed pane() indent = pane Pane.indent-at-line(pane&.cursor.line) pane Pane.move-to-beginning() pane Pane.add-str-at-char('\n') pane Pane.indent-at-current(indent) pane set-mode(Pane.Insert) elif c == 'w' (ed pane()) Pane.advance-word(Pane.MoveFwd, Pane.NextWordStart) elif c == 'e' (ed pane()) Pane.advance-word(Pane.MoveFwd, Pane.NextWordEnd) elif c == 'b' (ed pane()) Pane.advance-word(Pane.MoveBwd, Pane.NextWordEnd) elif c == 'U' Pane.redo(ed pane()) elif c == 'u' Pane.undo(ed pane()) elif c == 'd' ed copy-selection-to-clipboard() Pane.replace-selection(ed pane(), ed pane() Pane.selection(), '') ed pane() set-mode(Pane.Normal) elif c == 'c' ed copy-selection-to-clipboard() Pane.replace-selection(ed pane(), ed pane() Pane.selection(), '') ed pane() set-mode(Pane.Insert) elif c == 'y' ed copy-selection-to-clipboard() bytes-yanked = ed&.clipboard or-else('') StrView.num-bytes() ed set-msg('yanked \(bytes-yanked) bytes :)') elif c == 'p' ed&.clipboard if-just(fn(cp)) start = ed pane() Pane.max-pos() # ends in newline? paste after line. cp = cp own() if cp.contents Slice.last() == '\n' Char.ascii-u8() max-lines = ed pane()&.buf TextBuf.num-lines() start = { line: (start.line + 1) Math.clamp(0, max-lines), bi: 0 } # should this logic be here or part of TextBuf? it's a bit crappy obv. # also the selection is incorrect, since the selection should also contain the new line. if start.line == max-lines cp <= cp StrView.byte-substr(0, cp.contents.count - 1) ed pane() Pane.replace-selection((start, start), cp) else start = TextBuf.right-pos(ed&.pane.buf, start) ed pane() Pane.replace-selection((start, start), cp) elif c == 'P' ed&.clipboard if-just(fn(cp)) start = ed pane() Pane.min-pos() # ends in newline? paste before the line. if cp.contents Slice.last() == '\n' Char.ascii-u8() start <= start { bi: 0 } ed pane() Pane.replace-selection((start, start), cp) elif c == 'R' ed&.clipboard if-just(fn(cp)) start = ed pane() Pane.min-pos() end = ed pane() Pane.max-pos() ed pane() Pane.replace-selection((start, end { bi: end.bi + 1 }), cp) elif c == 'x' ed pane() Pane.expand() elif c == 'v' ed pane() set-mode(Pane.Select) if ed&.pane.sel is-none() ed <&.pane.sel= Just(ed&.pane.cursor) elif c == ':' ed <&.mode= Cmd(ed pane()&.cursor, StrBuilder.mk(ed&.al)) elif c == '/' ed <&.mode= Search(ed pane()&.cursor, StrBuilder.mk(ed&.al)) elif c == 'n' ed next-match() elif c == 'N' ed prev-match() elif c == '>' ed pane() Pane.indent-selection() elif c == '<' ed pane() Pane.dedent-selection() elif c == 'g' # ed <&.mode= Menu(goto-menu) # special movement menu # 'goto' ed enmenue(goto-menu) elif c == ' ' ed enmenue(space-menu) Tui.Escape ed pane() set-mode(Pane.Normal) Tui.Ctrl(c) if c == 'c' ed pane() Pane.toggle-comment() elif c == 'u' ed pane() Pane.move-up(15) elif c == 'd' ed pane() Pane.move-down(15) else pass _ pass fn handle-key (ed Ptr (Editor EditorAllocator), key Tui.Key) case &ed&.mode Ptr(Normal) case ed&.pane.mode Pane.Normal ed handle-normal-key(key) Pane.Select ed handle-normal-key(key) Pane.Insert case key Tui.Escape ed pane() set-mode( Pane.Normal) Tui.Enter indent = ed pane() Pane.indent-at-line(ed pane()&.cursor.line) ed pane() Pane.add-str-at-char('\n') ed pane() Pane.move-right() ed pane() Pane.indent-at-current(indent) Tui.Backspace ed pane() Pane.backspace() Tui.Char(c) s = Array.as-slice(&[c]) StrView.from-ascii-slice() ed pane() Pane.add-str-at-char(s) ed pane() Pane.move-right() Ptr(Cmd(prev-cur, sb)) case key Tui.Escape ed pane() Pane.set-cursors(prev-cur, None, Pane.UpdateVI) ed <&.mode= Normal ed <&.cfg&.theme= ed&.og-theme # reset theme in case it was changed. Tui.Enter ed run-cmd(StrBuilder.as-str(&sb)) StrBuilder.free(&sb) # assume we are already in the correct place. # we're really just setting the vi ed pane() Pane.set-cursors(ed pane()&.cursor, None, Pane.UpdateVI) ed <&.mode= Normal ed <&.og-theme= ed&.cfg&.theme # set the theme if it was modified. Tui.Char(c) StrBuilder.write-char(&sb, c Char.ascii-char()) ed live-cmd(StrBuilder.as-str(&sb)) Tui.Backspace if not sb null() StrBuilder.pop(&sb) ed live-cmd(StrBuilder.as-str(&sb)) _ pass Ptr(Search(prev-cur, sb)) case key Tui.Escape ed pane() Pane.set-cursors(prev-cur, None, Pane.UpdateVI) ed <&.mode= Normal Tui.Enter # assume we are already in the correct place. # we're really just setting the vi ed pane() Pane.set-cursors(ed pane()&.cursor, ed pane()&.sel, Pane.UpdateVI) ed&.search-term if-just(fn(st)) StrView.free(st, ed&.al) if not sb null() to = ed pane()&.cursor from = ed pane()&.sel or-else(to) ed <&.search-term= Just(StrBuilder.as-str(&sb)) else ed <&.search-term= None ed <&.mode= Normal Tui.Char(c) StrBuilder.write-char(&sb, c Char.ascii-char()) ed live-search(prev-cur, StrBuilder.as-str(&sb)) Tui.Backspace if not sb null() StrBuilder.pop(&sb) ed live-search(prev-cur, StrBuilder.as-str(&sb)) _ pass Ptr(Menu(menu)) case key Tui.Char(c) for menu-item in menu.items if menu-item.key == c menu-item.action(ed pane()) ed <&.mode= Normal Tui.Escape ed <&.mode= Normal _ pass _ todo() fn render-editor (screen Ptr (Screen al), ed Ptr (Editor al)) <= Allocator al screen-dims = { from-sx: 0 , to-sx: screen&.tui&.width Mem.u32-i32() -1 , from-sy: 0 , to-sy: screen&.tui&.height Mem.u32-i32() -1 } screen Pane.render(ed pane(), screen-dims, ed&.cfg) # overlay ed&.msg if-just(fn(msg)) num-chars = msg chars() count() Mem.size-i32() screen Tui.draw-str(' \(msg) ', (screen&.tui&.width Mem.u32-i32() - num-chars) / 2, screen&.tui&.height Mem.u32-i32() - 1) case ed&.mode Normal pass Cmd(_, sb) num-chars = sb chars() count() Mem.size-i32() pad = if num-chars Math.mod(2) == 0: '' else ' ' num-chars = num-chars + num-chars Math.mod(2) # todo: keyboard emoji has a surrogate pair, which I should theoretically handle, but I'm not doing that. full-str = ' ⌨️ \(sb)\(pad) ' # alternate char that looks nice is '#' full-chars = full-str chars() count() Mem.size-i32() screen Tui.draw-str(full-str, (screen&.tui&.width Mem.u32-i32() - full-chars) / 2, 1) Search(_, sb) num-chars = sb chars() count() Mem.size-i32() pad = if num-chars Math.mod(2) == 0: '' else ' ' num-chars = num-chars + num-chars Math.mod(2) full-str = ' 🔎 \(sb)\(pad) ' full-chars = full-str chars() count() Mem.size-i32() screen Tui.draw-str(full-str, (screen&.tui&.width Mem.u32-i32() - full-chars) / 2, 1) Menu(menu) screen Tui.draw-str(menu.name, 0, 0) for (menu-item, i) in menu.items zip(from(1)) screen Tui.draw-str('\(menu-item.key) \(menu-item.description)', 0, i) _ todo() # debug stuff. curline =& 0 fn ann(s) screen Tui.draw-str-right(s, 0, curline&) curline <&= curline& + 1 # ann('mode: \(ed&.pane.mode)') # cur-ty = ed pane() Pane.char-at(ed&.pane.cursor) CharType.char-type() # ann('cur: \(ed&.pane.cursor), \(cur-ty)') # ann('sel: \(ed&.pane.sel)') # (21.06.26): it breaks for some reason. # fn abbreviate-str (s Str) # len = 10 # if s chars() take(len + 1) count() > len # ss = s chars() take(len) # return '\(ss)...' StrCase1() # else # return s StrCase2() # cp = ed&.clipboard Misc.fmap-maybe(abbreviate-str) # cp = ed&.clipboard Misc.fmap-maybe(fn s: s take(10) StrConcat(if s drop(10) null(): '' else '...')) # ann('cp: \(cp)') # st = ed&.search-term Misc.fmap-maybe(fn s: s take(10) StrConcat(if s drop(10) null(): '' else '...')) # ann('st: \(st)') # ann('edmode: \(ed&.mode)') # ann('msg: \(ed&.msg)') # ann('sc-off: (\(ed&.pane.sc-off.screen-top), \(ed&.pane.sc-off.screen-left))') # ann('dims: { \(screen-dims.to-sx), \(screen-dims.to-sy) }') # ann('num actions: \(ed&.pane.buf&.actions.list.count)') # ann(Time.now().tv-sec) al = Alloc.idc() tb =& @undefined args = Args.get() initial-message = None if args.count > 0 fname = args[0] StrView.from-const-str() case TextBuf.mk-from-file(al, fname) None tb <&= TextBuf.mk(al) tb <&.filename= Just(fname StrView.clone-0(al)) Just(ftb) tb <&= ftb else tb <&= TextBuf.mk(al) cfg =& Config { theme: Theme.default , display-line-numbers: True , relative-line-numbers: False } ed =& Editor { running: True , al: al , pane: Pane.mk(al, tb) , clipboard: None , search-term: None , mode: Normal #, mode: Menu({ name: 'miauuu', items: [MenuItem { key: 'a', description: 'miau', action: fn _: () }] }) , msg: initial-message , cfg: cfg , og-theme: Theme.default } # screen shit tui =& Tui.mk() screen =& tui Tui.mk-screen(al) last-redraw-changes = 0 while ed&.running for ev in Iter.from-function(fn: Tui.read-event(tui)) case ev Tui.Key(k) ed reset-msg() ed handle-key(k) _ pass screen Tui.resize-screen-if-needed() if tui Tui.should-redraw() # we are modifying the underlying buffer, so the underlying repr of Chars stored in screen changes # This is real bad, but it will be changed when we use an immutable structure like a piece table. # (technically, it's not that easy. whenether the append string in a piece table reallocates, we'll need to discard the screen.previous...) # more thoughts in Tui. that's where I'll solve it. screen <&.requires-full-redraw= True screen Tui.clear-screen() theme = ed&.cfg&.theme screen Tui.set-screen-fg(theme&.default.fg) screen Tui.set-screen-bg(theme&.default.bg) screen Tui.fill-default() screen render-editor(ed) # screen Tui.draw-str('REDRAWS[\(last-redraw-changes)]', 0, screen&.tui&.height Mem.u32-i32() - 1) last-redraw-changes <= screen Tui.render-screen() tui Tui.sync() screen Tui.free-screen() tui Tui.deinit()