Parse config keybindings and iterate over a stack of keymappings

This commit is contained in:
Joseph Ferano 2023-11-08 19:11:21 +07:00
parent 5cb2fd0fd7
commit 7228bf3201
6 changed files with 212 additions and 60 deletions

24
DESIGN.org Normal file
View File

@ -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

View File

@ -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)

View File

@ -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'

98
fide.py
View File

@ -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')

11
fide.todo Normal file
View File

@ -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

100
keybindings.py Normal file
View File

@ -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()