diff --git a/DESIGN.org b/DESIGN.org new file mode 100644 index 0000000..69dda20 --- /dev/null +++ b/DESIGN.org @@ -0,0 +1,24 @@ + +* Keymapping system +** Keymap Stacking +There shouldn't be modes, there would be keymaps and a stack. Insert mode would +be at the bottom of the stack. Adding a new mode means pushing onto the stack, +and when a keybinding event is raised, you iterate over the stack from top to +bottom to find the correct keybinding. The vim nomenclature wouldn't work in +this case as you don't "leave insert mode". Rather, you pop the "normal mode" +keymap ontop of the "insert mode" keymap. +** Passthrough/Blocking +The keymap object should have additional settings that dictate its behavior when +a key is found and not found. +*** Found +**** Pop +The default could be to pop from the stack. +**** Keep +Keep the keymap on the stack. This is useful for both normal mode and transient +like modes where you need to explicitly pop it from the stack. +*** Not found +**** Passthrough +Allow the loop to go to the next keymap +**** Block +Ignore missing key, don't pop the keymap, maybe report an error + diff --git a/config.py b/config.py index 07b6674..fa01d28 100644 --- a/config.py +++ b/config.py @@ -1,10 +1,19 @@ from editing import * +from keybindings import * -keymap = { - 'left': cursor_go_left, - 'right': cursor_go_right, - 'up': cursor_go_up, - 'down': cursor_go_down, - 'C-e': cursor_go_line_end, - 'C-a': cursor_go_line_beginning, -} +kb('insert', 'left' , cursor_go_left) +kb('insert', 'right', cursor_go_right) +kb('insert', 'up' , cursor_go_up) +kb('insert', 'down' , cursor_go_down) +kb('insert', 'C-e' , cursor_go_line_end) +kb('insert', 'C-a' , cursor_go_line_beginning) +kb('insert', 'escape', lambda: push(keymappings['normal'])) +kb('normal', 'i', keymap_pop) +kb('normal', 'H', cursor_go_buffer_beginning) +kb('normal', 'L', cursor_go_buffer_end) +kb('normal', 'h', cursor_go_left) +kb('normal', 'j', cursor_go_down) +kb('normal', 'k', cursor_go_up) +kb('normal', 'l', cursor_go_right) +kb('normal', '^', cursor_go_line_beginning) +kb('normal', '$', cursor_go_line_end) diff --git a/editing.py b/editing.py index d7d9470..4ccfa41 100644 --- a/editing.py +++ b/editing.py @@ -53,6 +53,14 @@ def cursor_go_line_end(): buf.cursor.col = buffer_line_len(buf) buf.cursor.want_col = buf.cursor.col +def cursor_go_buffer_beginning(): + buf = ctx.current_buffer + buf.cursor.line_num = 0 + +def cursor_go_buffer_end(): + buf = ctx.current_buffer + buf.cursor.line_num = len(buf.lines) - 1 + def buffer_insert_line_below(buf: Buffer, text): line_num = buffer_line_num(buf) buf.lines.insert(line_num + 1, line_create(text)) @@ -127,3 +135,9 @@ def buffer_insert_text_at_cursor(text): curr_line.length += text_len cursor.col += text_len cursor.want_col = text_len + +# def buffer_switch_normal_mode(): +# ctx.current_buffer.mode = 'normal' + +# def buffer_switch_insert_mode(): +# ctx.current_buffer.mode = 'insert' diff --git a/fide.py b/fide.py index 56e89bb..a1e1319 100755 --- a/fide.py +++ b/fide.py @@ -30,6 +30,7 @@ class Line: class Buffer: lines: List[Line] cursor: Cursor + mode: str = 'insert' def cursor_pos(cursor: Cursor): return (cursor.line_num, cursor.col) @@ -59,26 +60,6 @@ def buffer_total_lines(buf: Buffer) -> int: def buffer_line_len(buf: Buffer) -> int: return buffer_line_current(buf).length -def translate_keyname(keyname: str) -> str: - match keyname: - case 'left ctrl': - return 'C-' - case 'left alt': - return 'A-' - case 'left meta': - return 'M-' - case 'left shift': - return 'S-' - - case 'right ctrl': - return 'RC-' - case 'right alt': - return 'RA-' - case 'right meta': - return 'RM-' - case 'right shift': - return 'RS-' - # oswinx, oswiny = 1940,50 oswinx, oswiny = 20,50 @@ -109,6 +90,7 @@ global ctx ctx = GlobalContext() from config import * from editing import * +from keybindings import * def main(): running = True frame_count = 0 @@ -119,6 +101,7 @@ def main(): current_buffer = ctx.current_buffer console = InteractiveInterpreter(locals()) console.runsource('from editing import *') + # We are hardcoding precedence here while running: dt = clock.tick(120) / 1000.0 screen.fill('white') @@ -132,41 +115,52 @@ def main(): cursor_on = True cursor_flash = 0 curr_line = buffer_line_current(current_buffer) - if event.mod == 64: # Right Ctrl - # this is inefficient, might as well just pass the mod number - # and make the mapping mod_num -> config_key - mod_key = translate_keyname('left ctrl') + kb_to_test = Keybinding(pg.key.name(event.key), event.mod) + # print(kb_to_test, event.key, pg.key.name(event.key)) + for i in range(len(keymap_stack) - 1, -1, -1): + keymap = keymap_stack[i] + # check if a modifier is pressed + if kb_to_test in keymap.mapping: + keymap.mapping[kb_to_test]() + # if mod_key and event.unicode: + # print(mod_key + pg.key.name(event.key)) + # print(pg.key.name(event.key)) - if event.unicode: - # print(mod_key, event.key, event.unicode) - key = mod_key + pg.key.name(event.key) - if key in keymap: - keymap[key]() - # if event.key == pg.K_e: - # cursor_go_line_end() - # if event.key == pg.K_a: - # cursor_go_line_beginning() - if event.key == pg.K_RETURN: - src = buffer_line_current(current_buffer).piece.get_text() - # try: - # eval(src[3:]) - # except: - # print('error evaluating line: ' + src[3:]) - console.runsource(src[3:]) - elif pg.key.name(event.key) == 'escape': - pass - elif event.key == pg.K_RETURN: - buffer_newline_at_pos() - elif event.key == pg.K_BACKSPACE: - buffer_delete_character_under_cursor() - elif event.key == pg.K_DELETE: - buffer_delete_next_character() - elif pg.key.name(event.key) in keymap: - keymap[pg.key.name(event.key)]() + # if event.mod == 64: # Right Ctrl + # # this is inefficient, might as well just pass the mod number + # # and make the mapping mod_num -> config_key + # mod_key = translate_keyname('left ctrl') + + # if event.unicode: + # # print(mod_key, event.key, event.unicode) + # key = mod_key + pg.key.name(event.key) + # if key in keymappings[current_buffer.mode]: + # keymappings[current_buffer.mode][key]() + # # if event.key == pg.K_e: + # # cursor_go_line_end() + # # if event.key == pg.K_a: + # # cursor_go_line_beginning() + # if event.key == pg.K_RETURN: + # src = buffer_line_current(current_buffer).piece.get_text() + # # try: + # # eval(src[3:]) + # # except: + # # print('error evaluating line: ' + src[3:]) + # console.runsource(src[3:]) + # elif pg.key.name(event.key) == 'escape': + # pass + # elif event.key == pg.K_RETURN: + # buffer_newline_at_pos() + # elif event.key == pg.K_BACKSPACE: + # buffer_delete_character_under_cursor() + # elif event.key == pg.K_DELETE: + # buffer_delete_next_character() + # elif pg.key.name(event.key) in keymappings[current_buffer.mode]: + # keymappings[current_buffer.mode][pg.key.name(event.key)]() # pass # elif event.mod < 2: - elif event.unicode: - buffer_insert_char(event.unicode) + # elif event.unicode: + # buffer_insert_char(event.unicode) for i,curr_line in enumerate(current_buffer.lines): render = text_renderer.render(curr_line.piece.get_text(), True, 'black') diff --git a/fide.todo b/fide.todo new file mode 100644 index 0000000..4d6b110 --- /dev/null +++ b/fide.todo @@ -0,0 +1,11 @@ +#-*- mode: org -*- +#+TODO: TODO | DONE +#+STARTUP: show2levels + +* DONE Render lines +* DONE Basic buffer implementation +* DONE Basic text editing functions +* DONE Basic keybindings system +* DONE Parse keymaps so config matches pygame keys +* TODO Keymap precedence system + diff --git a/keybindings.py b/keybindings.py new file mode 100644 index 0000000..0de9b9c --- /dev/null +++ b/keybindings.py @@ -0,0 +1,100 @@ +from fide import * +from typing import List, Dict, Callable +from dataclasses import dataclass, field + +@dataclass(slots=True, frozen=True) +class Keybinding: + key: str + modifiers: int + +@dataclass(slots=True) +class Keymap: + name: str + pinned: bool + passthrough: bool + pop_not_found: bool + mapping: Dict[[int,str],Callable] = field(default_factory=dict) + + +global keymappings +global keymap_stack + +def modsym_to_flag(sym: str): + match sym: + case 'S': return 1 + case 'C': return 64 + case 'RC': return 128 + case 'A': return 256 + case 'RA': return 512 + case 'M': return 1024 + # TODO Get which keycode these are + case 'right meta': + return 'RM' + case 'right shift': + return 'RS' + return None + +def symbol_to_num(sym: str): + match sym: + case '!': return '1' + case '@': return '2' + case '#': return '3' + case '$': return '4' + case '%': return '5' + case '^': return '6' + case '&': return '7' + case '*': return '8' + case '(': return '9' + case ')': return '0' + # TODO Get which keycode these are + return sym + +def parse_keybinding(keybinding: str) -> str: + keys = keybinding.replace(' ', '').split('-') + if not keys: + # TODO: We need to return some kind of error + return None + keys_len = len(keys) + if keys_len > 1: # We have modifiers + modifiers = 0 + for i in range(keys_len - 1): + flag = modsym_to_flag(keys[i]) + if flag: + modifiers |= flag + else: + # TODO: Also throw some kind of parsing error + return None + else: + modifiers = 0 + if len(keys[-1]) > 1: # We have a multi-stroke keybinding, we don't handle that yet + key = keys[-1] + else: + key = keys[-1][0] + if key.isupper(): + modifiers |= 1 + key = key.lower() + if not key.isalnum(): + key = symbol_to_num(key) + + return Keybinding(key, modifiers) + + +keymap_stack = [ + Keymap('insert', pinned=True, passthrough=False, pop_not_found=False), + Keymap('normal', pinned=True, passthrough=False, pop_not_found=False), +] + +keymappings = { + 'insert': keymap_stack[0], + 'normal': keymap_stack[1], + } + +def kb(mode: str, binding: str, f: Callable): + b = parse_keybinding(binding) + keymappings[mode].mapping[b] = f + +def keymap_push(keymap: Keymap): + keymap_stack.append(keymap_push) + +def keymap_pop(): + keymap_stack.pop()