├── .gitignore ├── README.org ├── assets ├── structure.drawio ├── structure.png ├── structure_small ├── structure_small.png ├── tmux-rime.png └── tmux-rime_screenshot.png ├── build.sh ├── scripts ├── bindings.sh ├── config.sh ├── env.sh ├── setup.sh ├── tmux_rime.sh └── utils.sh ├── slide └── presentation.pdf ├── tmux_rime.tmux ├── tmux_rime ├── rime_wrapper │ ├── Makefile │ ├── __init__.py │ ├── raw.py │ ├── rime.c │ ├── rime.h │ └── structs.py ├── tmux_rime_client.py └── tmux_rime_server.py └── utils └── debugging.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packaging 10 | build/ 11 | 12 | # SWIG intermediate files 13 | tmux_rime/rime.py 14 | lib/rime_wrap.c 15 | 16 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Tmux-Rime 2 | #+html: logo.png 3 | 4 | 5 | ** Description 6 | ~tmux-rime~ is a tmux plugin that let you type CJK chars within terminal. 7 | 8 | You may refer to the introduction slide: [[file:slide/presentation.pdf]] 9 | 10 | 11 | ** Program Structure 12 | #+html: structure.png 13 | 14 | ** Screenshot 15 | #+html: screenshot.png 16 | 17 | ** Installation 18 | *** Prerequisite 19 | **** FreeBSD 20 | 21 | Install required packages: 22 | #+begin_src sh 23 | pkg install git gcc gmake bash tmux zh-librime zh-fcitx-rime 24 | #+end_src 25 | 26 | You may need to create a symobol link called ~python~ to link to ~python3.X~ 27 | #+begin_src sh 28 | mkdir ~/bin 29 | ln -s /usr/local/bin/python3.8 ~/bin/python 30 | #+end_src 31 | 32 | 33 | Create a empty directory ~/usr/share/rime-data~ (I know it's a bad practice in FreeBSD), and put all your rime configs into ~/.config/tmux_rime/rime/~. 34 | 35 | You may also copy the default configs (under ~/usr/local/share/rime-data~): 36 | #+begin_src 37 | sudo touch /usr/share/rime-data 38 | mkdir -p ~/.config/tmux_rime 39 | cp -r /usr/local/share/rime-data ~/.config/tmux_rime/rime 40 | #+end_src 41 | 42 | *** Building 43 | 44 | Clone the repo and build it: 45 | #+begin_src sh 46 | mkdir -p ~/.tmux/plugins/ 47 | git clone https://github.com/Cycatz/tmux-rime.git ~/.tmux/plugins/tmux-rime 48 | cd ~/.tmux/plugins/tmux-rime 49 | ./build.sh 50 | #+end_src 51 | 52 | Add the following line to your tmux config, e.g., =~/.tmux.conf=: 53 | #+begin_src sh 54 | run-shell '~/.tmux/plugins/tmux-rime/tmux_rime.tmux' 55 | #+end_src 56 | 57 | 58 | ** Usage 59 | + ~Prefix + t~ to enable the rime mode 60 | + ~Enter~ to commit raw characters 61 | + ~Esc~ to exit the rime mode 62 | 63 | 64 | ** Todo 65 | *** IME functions 66 | + +Preedit chars deleting+ 67 | + +Raw input committing+ 68 | + Candidate selection 69 | + Schema selection 70 | 71 | *** Customization 72 | + Custom IME status bar 73 | + Custom plugin options 74 | + Use tmux user options (prefixed with ~@~) to define variables 75 | + Custom key mappings 76 | + Define what keys need to be bound in tmux 77 | 78 | *** Misc 79 | + Write tests 80 | + Write documentation 81 | + Set up plugin installation process 82 | -------------------------------------------------------------------------------- /assets/structure.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /assets/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cycatz/tmux-rime/501ce7754a271d3a3d2474cd6de46a261e54741f/assets/structure.png -------------------------------------------------------------------------------- /assets/structure_small: -------------------------------------------------------------------------------- 1 | 7VtZb+M2EP41BnYfYuj08Wg7dlKgxQZ1gd19WtASLbGRRJWir/76khRl3fKtOE2CAKGG13C+4cznsdLRJ/72iYDQ/QPb0Otoir3t6I8dTdNMTevwX8XexRK1ryuxxCHIlrJUMEf/QilMhq2QDaPcQIqxR1GYF1o4CKBFczJACN7khy2xl981BA4sCeYW8MrS78imrpSqipJ2PEPkuHLrgSk7fJAMloLIBTbeZET6tKNPCMY0bvnbCfS49RK7xPNmNb17xQgM6DETxh7eKL2ZEX3/5v54+dV/Gi8WDxKeNfBW8sBSWbpLLAADe8QNyZ4sD0QRsjr62KW+xwQqa0aU4Ne9cdixxmXNpLLQzhlb6vkEsQ8p2bEBm9TEiR3djHETGYEeoGidhwhIpJ39cvsdXjBimmiKdEujN4ynSKd80BI0kzUivCIWlNOyNj20kl5ciQLiQFpaiTUyB09FArMT8NM/In5mEhxqrX4sfqWVSp5wY/yMCvx6HuWwhCDIAdn7Z8VDxXiJA/oQiUA5YgM0I9ymnazl8L8W9v1VgCxAoYCFocLWwtYrpMkGTN94j3hGyW8o3NK8pwAPOQF3I+YXkDDBGhLK9vBGssNHts2njwlkCoKFWEphzyG3n7CoOe6Yj3ytFcXxIcTS/FAz4COPA/EMvTXkC8uOuVRKk88T7GEilGTAm+Zs1uSzXEe4bfTaxBm0ojP0pSDj1tqgwq81pd6Fcx7T4B6mauv2egpm6+fpcjGaP4bmpsE9uB2OcQ91WOUef/krJlX+RD53jzkkzErJyguSusQxrriUsPHdWBfwQzFV1w32FwUsMUeYXXdQ6iur9aXWN3OHttlqD/E5iZVTyKWU84IRt7o240OiroOx40EQoqjLbgUTWxEbMkvUfvwtp+JeoxaOSxkKv4jAYBYJDLrhLmOC+LhvhMXXen9oRsfa3850SX25VC3VKu8yAeISSEcEgS2yQxQyReEBS1TqlR9YiGgErwIb2jImbVxE4TwEFu/dMPp6KC3WxqiaQFMRjmpjj24WiIShVsSeXkXsUYsZ62rBRzPLXIIRgCQaY0Jd7OAAeNNUOs7bOB3zO8ahtOzfkNKdZPo8DZxt9+ZcUCArlckh5gkNNuhXo3YhgekbhUyj6sataEf9sW6eVyYeggGtv69yryWw8lvVReRW4n4hE7WZcspZwRIWrM8KzUG6KmqeE6TnUMTlGNWZxJgzTBavow8dpdVBv2uW43S/Mk7vh149Ug9auc6RRVB4+Dq3w86ODRplYtlwK+4xEsVWZ9dstg8M3cj9evK1O+fiT9cifisieX+ge943i/m5+p5X1Thuec8TTthOuSAAPuRxP0Rhlov/74oGF7lKqWjQK/N23WizZqD1Pmm7WgPahbxdL8UFXe/2C7e9hrqPCAG7zDDp3fV76TWfEepqmYcmsEasw1U/SahJme38kNTIPRYosFHgiMwVLJETNSachrLEPeb261jqyKrZmaun/ENCETH6cVGR6tCOp1eeTlVDkiCuAnEWXzTTFPWyCc+Pg0zzK2/HdPMYxee8uK6sGFVSXuEuMdcB3S+z1Qmmegf0697o1s2+nYmrnpJnsXyx8qFIBp8kq5JkqcPDJGtPqNohWWoJlRZIFjMX2f3g87tGT08EP4VgONyPeNzKLeKnXfbpBfJPkMJJhPDOeJusZMQc6pYE7zL09TdG38xifwj3dwrx4C0RVtspqU05UGHM499jlTwtSfFWW9XoPcuCwRoRHPiZbxk+QnmqWIau+qqw7RI0n//Jle6BKxUKUsM3p0qfLPo+PKNn3JtnDN6CRl2BDp3LxK5Io7T+sUz5Uh5V8+brsJCDegUvufEbDNrBVxgqSYUifm5ViZPkLiTYglEk3uqKIoSDU8tXhUsRuSDkzWTdg3RkAaxXR1yUbyvqoQBez/WvQl+MvOsMKgJR1WuW57zqxB7TV+xj50v/U0Gf/gc= -------------------------------------------------------------------------------- /assets/structure_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cycatz/tmux-rime/501ce7754a271d3a3d2474cd6de46a261e54741f/assets/structure_small.png -------------------------------------------------------------------------------- /assets/tmux-rime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cycatz/tmux-rime/501ce7754a271d3a3d2474cd6de46a261e54741f/assets/tmux-rime.png -------------------------------------------------------------------------------- /assets/tmux-rime_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cycatz/tmux-rime/501ce7754a271d3a3d2474cd6de46a261e54741f/assets/tmux-rime_screenshot.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | gmake -C "$CURRENT_DIR"/tmux_rime/rime_wrapper/ 6 | -------------------------------------------------------------------------------- /scripts/bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | TMUX_RIME_CLIENT="$CURRENT_DIR"/../tmux_rime/tmux_rime_client.py 6 | 7 | source "$CURRENT_DIR"/env.sh 8 | source "$CURRENT_DIR"/utils.sh 9 | 10 | 11 | tmux_rime_bind() { 12 | local key="$1" 13 | local command="$2" 14 | 15 | tmux bind-key -T tmux_rime "$key" run-shell -b "'${TMUX_RIME_CLIENT}' ${command}" 16 | } 17 | 18 | bind_reg_keys() { 19 | # for char in $TMUX_RIME_REG_KEYS; do 20 | for char in {0..9} {a..z} {A..Z} "." "," "/" ; do 21 | tmux_rime_bind "$char" "key -k '$char'" 22 | tmux_rime_bind "C-$char" "key -k '$char' -m ctrl" 23 | tmux_rime_bind "M-$char" "key -k '$char' -m alt" 24 | done 25 | 26 | # Special case for semicolon ';' 27 | tmux_rime_bind "\;" "key -k ';'" 28 | tmux_rime_bind "C-\;" "key -k ';' -m ctrl" 29 | tmux_rime_bind "M-\;" "key -k ';' -m alt" 30 | 31 | # Special case for Space 32 | tmux_rime_bind "Space" "key -k Space" 33 | } 34 | 35 | bind_special_keys() { 36 | tmux_rime_bind "DC" "delete-char" 37 | tmux_rime_bind "BSpace" "delete-char" 38 | tmux_rime_bind "Enter" "commit-raw" 39 | tmux_rime_bind "Escape" "exit" 40 | tmux_rime_bind "?" "toggle-help" 41 | tmux_rime_bind "Any" "noop" 42 | } 43 | 44 | main() { 45 | bind_reg_keys 46 | bind_special_keys 47 | } 48 | 49 | main "$@" 50 | exit 0 51 | -------------------------------------------------------------------------------- /scripts/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | source "$CURRENT_DIR"/env.sh 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TMUX_RIME_PREFIX="T" 4 | # TMUX_RIME_REG_KEYS="{a..z} {A..Z} {0..9} Enter Space" 5 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | source "$CURRENT_DIR"/utils.sh 7 | 8 | 9 | commands_pipe='/tmp/tmux-rime.client' 10 | 11 | 12 | create_rime_window() { 13 | local current_pane 14 | local rime_window_pane 15 | current_pane="$(tmux display-message -p -F ':#{window_id}.#{pane_id}')" 16 | rime_window_pane=$(tmux new-window -F ":#{window_id}.#{pane_id}" -P -d -n "[rime]" "bash --norc --noprofile") 17 | pane_exec "$rime_window_pane" "$CURRENT_DIR/tmux_rime.sh '$current_pane' '$rime_window_pane'" 18 | } 19 | 20 | setup_status_bar() { 21 | current_status="$(tmux show -gqv status-format[0])" 22 | tmux set-option -g status-format[0] "[ㄓ]" 23 | tmux set-option -g status-format[1] "$current_status" 24 | tmux set-option -g status 2 25 | } 26 | 27 | create_commands_pipe() { 28 | # TODO consider the condition that "$commands_pipe" is a regular file 29 | if [[ ! -p "$commands_pipe" ]]; then 30 | mkfifo -m 600 "$commands_pipe" 31 | fi 32 | } 33 | 34 | main() { 35 | create_rime_window 36 | create_commands_pipe 37 | setup_status_bar 38 | exit 0 39 | } 40 | 41 | main "$@" 42 | -------------------------------------------------------------------------------- /scripts/tmux_rime.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | source "$CURRENT_DIR"/utils.sh 6 | TMUX_RIME_SERVER="$CURRENT_DIR"/../tmux_rime/tmux_rime_server.py 7 | 8 | current_window_pane="" 9 | rime_window_pane="" 10 | tmux_prefix="" 11 | commands_pipe='/tmp/tmux-rime.client' 12 | 13 | enable_rime_mode() { 14 | tmux set-window-option key-table tmux_rime 15 | tmux switch-client -T tmux_rime 16 | tmux_prefix="$(tmux show -gqv prefix)" # Save tmux prefix 17 | tmux set-option -g prefix None 18 | } 19 | 20 | handle_exit() { 21 | # Restore status bar 22 | # TODO: Save status and status-format before start 23 | tmux set-option -g status on 24 | tmux set-option -g -u status-format 25 | 26 | # Restore prefix and key table 27 | tmux set-option -g prefix "$tmux_prefix" 28 | tmux set-window-option key-table root 29 | tmux switch-client -Troot 30 | 31 | local rime_window_id 32 | rime_window_id="$(echo $rime_window_pane | cut -d "." -f 2)" 33 | tmux kill-window -t "$rime_window_id" 34 | } 35 | 36 | init_rime_mode() { 37 | trap "handle_exit" EXIT 38 | "$TMUX_RIME_SERVER" & 39 | enable_rime_mode 40 | } 41 | 42 | insert_text() { 43 | local inserted_text 44 | inserted_text="${1#insert }" 45 | tmux send-key -l -t "$current_window_pane" "$inserted_text" 46 | } 47 | 48 | update_status() { 49 | local status_str 50 | 51 | if [[ "$1" == "status" ]]; then 52 | status_str="[ㄓ]" 53 | else 54 | status_str="${1#status }" 55 | fi 56 | tmux set -g status-format[0] "$status_str" 57 | } 58 | 59 | 60 | main() { 61 | current_window_pane="$1" 62 | rime_window_pane="$2" 63 | init_rime_mode 64 | 65 | while read -r -s statement; do 66 | echo "$current_window_pane" >> /tmp/pane 67 | echo "$statement" >> /tmp/statement 68 | case $statement in 69 | insert*) 70 | insert_text "$statement" 71 | ;; 72 | status*) 73 | update_status "$statement" 74 | ;; 75 | toggle-help) 76 | : 77 | ;; 78 | noop) 79 | : 80 | ;; 81 | exit) 82 | break 83 | ;; 84 | esac 85 | done < <(tail -f "$commands_pipe") 86 | exit 0 87 | } 88 | 89 | main "$@" 90 | -------------------------------------------------------------------------------- /scripts/utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | create_pipe() { 4 | local pipe_path=$(mktemp -u "${TMPDIR:-/tmp}/tmux_rime.XXXXXXXX") 5 | mkfifo -m 600 "$pipe_path" 6 | echo "$pipe_path" 7 | } 8 | 9 | pane_exec() { 10 | local pane_id="$1" 11 | local pane_command="$2" 12 | 13 | tmux send-keys -t "$pane_id" "$pane_command" 14 | tmux send-keys -t "$pane_id" Enter 15 | } 16 | -------------------------------------------------------------------------------- /slide/presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cycatz/tmux-rime/501ce7754a271d3a3d2474cd6de46a261e54741f/slide/presentation.pdf -------------------------------------------------------------------------------- /tmux_rime.tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PLUGIN_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | source "$PLUGIN_DIR"/scripts/utils.sh 6 | source "$PLUGIN_DIR"/scripts/env.sh 7 | 8 | 9 | tmux run-shell -b "bash --norc --noprofile $PLUGIN_DIR/scripts/config.sh" 10 | tmux run-shell -b "$PLUGIN_DIR/scripts/bindings.sh" 11 | 12 | tmux bind-key "$TMUX_RIME_PREFIX" run-shell "$PLUGIN_DIR/scripts/setup.sh" 13 | -------------------------------------------------------------------------------- /tmux_rime/rime_wrapper/Makefile: -------------------------------------------------------------------------------- 1 | CXX = gcc 2 | CXXFLAGS = -fPIC -shared -Wall -Wextra -Wstrict-prototypes -O2 -std=c11 3 | LDLIBS += -lrime 4 | 5 | SRC_EXT := c 6 | SOURCE := rime.$(SRC_EXT) 7 | TARGET := libwrime.so 8 | 9 | .PHONY = clean 10 | 11 | all: $(TARGET) 12 | 13 | $(TARGET): $(SOURCE) 14 | @echo "$(CXX) $(CXXFLAGS) $(LDLIBS) $< -o $@"; $(CXX) $(CXXFLAGS) $(LDLIBS) $< -o $@ 15 | clean: 16 | @echo "$(RM) $(TARGET)"; $(RM) $(TARGET) 17 | -------------------------------------------------------------------------------- /tmux_rime/rime_wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ctypes import * 4 | from rime_wrapper.raw import * 5 | 6 | class RimeWrapper: 7 | def __init__(self): 8 | self.rime_wrapper_ptr = rime_wrapper_init() 9 | 10 | def start(self): 11 | rime_wrapper_start(self.rime_wrapper_ptr, 0) 12 | 13 | def has_composition(self, _context=None): 14 | new_created = False 15 | if _context is None: 16 | new_created = True 17 | _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 18 | res = _context.contents.composition is not None and _context.contents.composition.length > 0 19 | if new_created: 20 | rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 21 | 22 | return res 23 | 24 | def has_candidates(self, _context=None): 25 | new_created = False 26 | if _context is None: 27 | new_created = True 28 | _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 29 | 30 | res = _context.contents.menu.page_size > 0 31 | if new_created: 32 | rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 33 | 34 | return res 35 | 36 | def get_schema_list(self): 37 | _schema_list = rime_wrapper_get_schema_list(self.rime_wrapper_ptr) 38 | 39 | list_size = _schema_list.contents.size 40 | schema_list = _schema_list.contents.list 41 | 42 | items = [] 43 | for i in range(list_size): 44 | schema = schema_list[i] 45 | items.append((schema.schema_id.decode('utf-8'), 46 | schema.name.decode('utf-8'))) 47 | rime_wrapper_free_schema_list(self.rime_wrapper_ptr, _schema_list) 48 | return items 49 | 50 | def set_schema(self, schema_id): 51 | return rime_wrapper_set_schema(self.rime_wrapper_ptr, c_char_p(schema_id.encode('utf-8'))) 52 | 53 | def get_commit_text_preview(self): 54 | _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 55 | commit_text_preview = _context.contents.commit_text_preview.decode('utf-8') 56 | rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 57 | 58 | return commit_text_preview 59 | 60 | def get_candidates(self): 61 | _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 62 | menu = _context.contents.menu 63 | 64 | candidates = [] 65 | 66 | if self.has_candidates(): 67 | page_size = menu.page_size 68 | page_no = menu.page_no 69 | num_candidates = menu.num_candidates 70 | _candidates = menu.candidates 71 | 72 | for i in range(num_candidates): 73 | candidate = _candidates[i] 74 | 75 | text = candidate.text.decode('utf-8') 76 | comment = '' 77 | if candidate.comment is not None: 78 | comment = candidate.comment.decode('utf-8') 79 | candidates.append((text, comment)) 80 | 81 | rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 82 | return candidates 83 | 84 | def get_composition(self): 85 | _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 86 | 87 | composition = RimeComposition() 88 | if self.has_composition(_context): 89 | # Deep copy 90 | composition = RimeComposition(_context.contents.composition.length, 91 | _context.contents.composition.cursor_pos, 92 | _context.contents.composition.sel_start, 93 | _context.contents.composition.sel_end, 94 | _context.contents.composition.preedit) 95 | 96 | rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 97 | 98 | return composition 99 | 100 | def get_composition_preedit(self): 101 | _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 102 | 103 | composition_preedit = '' 104 | if self.has_composition(_context): 105 | composition_preedit = _context.contents.composition.preedit.decode('utf-8') 106 | 107 | rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 108 | return composition_preedit 109 | 110 | 111 | def get_commit_text_preview(self): 112 | _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 113 | 114 | commit_text_preview = '' 115 | _commit_text_preview = _context.contents.commit_text_preview 116 | if _commit_text_preview is not None: 117 | commit_text_preview = _commit_text_preview.decode('utf-8') 118 | rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 119 | return commit_text_preview 120 | # def get_context(self): 121 | # _context = rime_wrapper_get_context(self.rime_wrapper_ptr) 122 | # commit_text_preview = _context.contents.commit_text_preview.decode('utf-8') 123 | # select_labels = _context.contents.select_labels 124 | # rime_wrapper_free_context(self.rime_wrapper_ptr, _context) 125 | 126 | def get_commit(self): 127 | _commit = rime_wrapper_get_commit(self.rime_wrapper_ptr) 128 | 129 | text = '' 130 | if _commit is None: 131 | print('Error') 132 | else: 133 | try: 134 | data_size = _commit.contents.data_size 135 | text = _commit.contents.text.decode('utf-8') 136 | except ValueError: # null pointer 137 | print('Error!') 138 | pass 139 | 140 | rime_wrapper_free_commit(self.rime_wrapper_ptr, _commit) 141 | return text 142 | 143 | def process_key(self, keycode, mask): 144 | return rime_wrapper_process_key(self.rime_wrapper_ptr, keycode, mask) 145 | 146 | 147 | def commit_composition(self): 148 | return rime_wrapper_commit_composition(self.rime_wrapper_ptr) 149 | 150 | def clear_composition(self): 151 | rime_wrapper_clear_composition(self.rime_wrapper_ptr) 152 | 153 | def get_input_str(self): 154 | _input_str = rime_wrapper_get_input_str(self.rime_wrapper_ptr) 155 | if _input_str is None: 156 | print('Error!') 157 | return '' 158 | else: 159 | input_str = cast(_input_str, c_char_p).value.decode('utf-8') 160 | rime_wrapper_free_str(_input_str) 161 | return input_str 162 | 163 | def finish(self): 164 | rime_wrapper_finish(self.rime_wrapper_ptr) 165 | 166 | 167 | # rime_wrapper.start() 168 | # rime_wrapper.finish() 169 | -------------------------------------------------------------------------------- /tmux_rime/rime_wrapper/raw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import platform 4 | from ctypes import * 5 | import ctypes.util 6 | 7 | import rime_wrapper 8 | from rime_wrapper.structs import * 9 | 10 | system = platform.system() 11 | 12 | if system == 'Windows': 13 | library_name = 'libwrime.dll' 14 | elif system == 'Darwin': 15 | library_name = 'libwrime.dylib' 16 | else: 17 | library_name = 'libwrime.so' 18 | 19 | filename = os.path.join(os.path.dirname(rime_wrapper.__file__), library_name) 20 | 21 | try: 22 | _lib = ctypes.CDLL(filename) 23 | except (OSError, TypeError): 24 | _lib = None 25 | raise RuntimeError('Rime wrapper library not found') 26 | 27 | rime_wrapper_init = _lib.rime_wrapper_init 28 | rime_wrapper_init.argtypes = [] 29 | rime_wrapper_init.restype = c_void_p 30 | 31 | rime_wrapper_start = _lib.rime_wrapper_start 32 | rime_wrapper_start.argtypes = [c_void_p, c_int] 33 | rime_wrapper_start.restype = c_int 34 | 35 | rime_wrapper_finish = _lib.rime_wrapper_finish 36 | rime_wrapper_finish.argtypes = [c_void_p] 37 | rime_wrapper_finish.restype = c_int 38 | 39 | rime_wrapper_get_input_str = _lib.rime_wrapper_get_input_str 40 | rime_wrapper_get_input_str.argtypes = [c_void_p] 41 | rime_wrapper_get_input_str.restype = c_void_p 42 | 43 | rime_wrapper_free_str = _lib.rime_wrapper_free_str 44 | rime_wrapper_free_str.argtypes = [c_void_p] 45 | rime_wrapper_free_str.restype = None 46 | 47 | rime_wrapper_set_cursor_pos = _lib.rime_wrapper_set_cursor_pos 48 | rime_wrapper_set_cursor_pos.argtypes = [c_void_p, c_int] 49 | rime_wrapper_set_cursor_pos.restype = None 50 | 51 | rime_wrapper_clear_composition = _lib.rime_wrapper_clear_composition 52 | rime_wrapper_clear_composition.argtypes = [c_void_p] 53 | rime_wrapper_clear_composition.restype = None 54 | 55 | rime_wrapper_get_commit = _lib.rime_wrapper_get_commit 56 | rime_wrapper_get_commit.argtypes = [c_void_p] 57 | rime_wrapper_get_commit.restype = POINTER(RimeCommit) 58 | 59 | rime_wrapper_free_commit = _lib.rime_wrapper_free_commit 60 | rime_wrapper_free_commit.argtypes = [c_void_p, POINTER(RimeCommit)] 61 | rime_wrapper_free_commit.restype = None 62 | 63 | rime_wrapper_get_context = _lib.rime_wrapper_get_context 64 | rime_wrapper_get_context.argtypes = [c_void_p] 65 | rime_wrapper_get_context.restype = POINTER(RimeContext) 66 | 67 | rime_wrapper_free_context = _lib.rime_wrapper_free_context 68 | rime_wrapper_free_context.argtypes = [c_void_p, POINTER(RimeContext)] 69 | rime_wrapper_free_context.restype = None 70 | 71 | rime_wrapper_get_schema_list = _lib.rime_wrapper_get_schema_list 72 | rime_wrapper_get_schema_list.argtypes = [c_void_p] 73 | rime_wrapper_get_schema_list.restype = POINTER(RimeSchemaList) 74 | 75 | rime_wrapper_free_schema_list = _lib.rime_wrapper_free_schema_list 76 | rime_wrapper_free_schema_list.argtypes = [c_void_p, POINTER(RimeSchemaList)] 77 | rime_wrapper_free_schema_list.restype = None 78 | 79 | rime_wrapper_set_schema = _lib.rime_wrapper_set_schema 80 | rime_wrapper_set_schema.argtypes = [c_void_p, c_char_p] 81 | rime_wrapper_set_schema.restype = c_int 82 | 83 | 84 | rime_wrapper_commit_composition = _lib.rime_wrapper_commit_composition 85 | rime_wrapper_commit_composition.argtypes = [c_void_p] 86 | rime_wrapper_commit_composition.restype = c_int 87 | 88 | rime_wrapper_process_key = _lib.rime_wrapper_process_key 89 | rime_wrapper_process_key.argtypes = [c_void_p, c_int, c_int] 90 | rime_wrapper_process_key.restype = c_int 91 | 92 | -------------------------------------------------------------------------------- /tmux_rime/rime_wrapper/rime.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "rime.h" 4 | 5 | // Redefine related macros for the pointer type 6 | // #define RIME_STRUCT_INIT_P(Type, var) ((var)->data_size = sizeof(Type) - sizeof((var)->data_size)) 7 | #define RIME_STRUCT_P(Type, var) *var = (Type){0}; RIME_STRUCT_INIT(Type, *var); 8 | 9 | static char *copy_string(const char *src) 10 | { 11 | char *dst; 12 | size_t len = strlen(src); 13 | 14 | rime_wrapper_malloc(dst, len + 1, NULL); 15 | strcpy(dst, src); 16 | dst[len] = '\0'; 17 | 18 | return dst; 19 | } 20 | 21 | void rime_wrapper_notification_handler(void* context_object, 22 | RimeSessionId session_id, 23 | const char* message_type, 24 | const char* message_value) 25 | 26 | { 27 | } 28 | 29 | int rime_wrapper_process_key(RimeWrapper *rime_wrapper, int keycode, int mask) 30 | { 31 | if (!rime_wrapper->api->process_key(rime_wrapper->session_id, keycode, mask)) { 32 | return -1; 33 | } 34 | return 0; 35 | } 36 | 37 | int rime_wrapper_commit_composition(RimeWrapper *rime_wrapper) 38 | { 39 | if (!rime_wrapper->api->commit_composition(rime_wrapper->session_id)) { 40 | return -1; 41 | } 42 | return 0; 43 | } 44 | 45 | char *rime_wrapper_get_input_str(RimeWrapper *rime_wrapper) 46 | { 47 | const char* input = rime_wrapper->api->get_input(rime_wrapper->session_id); 48 | if (!input) { 49 | return NULL; 50 | } 51 | return copy_string(input); 52 | } 53 | 54 | void rime_wrapper_free_str(char *str) 55 | { 56 | free(str); 57 | } 58 | 59 | void rime_wrapper_set_cursor_pos(RimeWrapper *rime_wrapper, int pos) 60 | { 61 | rime_wrapper->api->set_caret_pos(rime_wrapper->session_id, pos); 62 | } 63 | 64 | void rime_wrapper_clear_composition(RimeWrapper *rime_wrapper) 65 | { 66 | rime_wrapper->api->clear_composition(rime_wrapper->session_id); 67 | } 68 | 69 | RimeCommit *rime_wrapper_get_commit(RimeWrapper *rime_wrapper) 70 | { 71 | RimeCommit *commit; 72 | 73 | rime_wrapper_malloc(commit, sizeof(RimeCommit), NULL); 74 | 75 | RIME_STRUCT_P(RimeCommit, commit); 76 | 77 | if (!rime_wrapper->api->get_commit(rime_wrapper->session_id, commit)) { 78 | return NULL; 79 | } 80 | 81 | return commit; 82 | } 83 | 84 | void rime_wrapper_free_commit(RimeWrapper *rime_wrapper, RimeCommit *commit) 85 | { 86 | rime_wrapper->api->free_commit(commit); 87 | free(commit); 88 | } 89 | 90 | 91 | RimeContext *rime_wrapper_get_context(RimeWrapper *rime_wrapper) 92 | { 93 | RimeContext *context; 94 | 95 | rime_wrapper_malloc(context, sizeof(RimeContext), NULL); 96 | 97 | RIME_STRUCT_P(RimeContext, context); 98 | 99 | if (!rime_wrapper->api->get_context(rime_wrapper->session_id, context)) { 100 | return NULL; 101 | } 102 | 103 | return context; 104 | } 105 | 106 | void rime_wrapper_free_context(RimeWrapper *rime_wrapper, RimeContext *context) 107 | { 108 | rime_wrapper->api->free_context(context); 109 | free(context); 110 | } 111 | 112 | RimeSchemaList *rime_wrapper_get_schema_list(RimeWrapper *rime_wrapper) 113 | { 114 | RimeSchemaList *schema_list; 115 | 116 | rime_wrapper_malloc(schema_list, sizeof(schema_list), NULL); 117 | 118 | if (!rime_wrapper->api->get_schema_list(schema_list)) { 119 | return NULL; 120 | } 121 | return schema_list; 122 | } 123 | 124 | void rime_wrapper_free_schema_list(RimeWrapper *rime_wrapper, RimeSchemaList *schema_list) 125 | { 126 | rime_wrapper->api->free_schema_list(schema_list); 127 | free(schema_list); 128 | } 129 | 130 | int rime_wrapper_set_schema(RimeWrapper *rime_wrapper, const char *schema_id) 131 | { 132 | 133 | if (!rime_wrapper->api->select_schema(rime_wrapper->session_id, schema_id)) { 134 | return -1; 135 | } 136 | 137 | return 0; 138 | } 139 | int rime_wrapper_start(RimeWrapper *rime_wrapper, int fullcheck) 140 | { 141 | const char *shared_data_dir = RIME_DATA_DIR; 142 | const char *user_data_dir = RIME_USER_DIR; 143 | 144 | RIME_STRUCT(RimeTraits, rime_wrapper_traits); 145 | rime_wrapper_traits.shared_data_dir = shared_data_dir; 146 | rime_wrapper_traits.user_data_dir = user_data_dir; 147 | rime_wrapper_traits.app_name = "rime.tmux-rime"; 148 | rime_wrapper_traits.distribution_name = "Rime"; 149 | rime_wrapper_traits.distribution_code_name = "tmux-rime"; 150 | rime_wrapper_traits.distribution_version = RIME_WRAPPER_VERSION; 151 | 152 | if (rime_wrapper->first_run) { 153 | rime_wrapper->api->setup(&rime_wrapper_traits); 154 | rime_wrapper->first_run = 0; 155 | } 156 | rime_wrapper->api->initialize(&rime_wrapper_traits); 157 | rime_wrapper->api->set_notification_handler(rime_wrapper_notification_handler, rime_wrapper); 158 | rime_wrapper->api->start_maintenance(fullcheck); 159 | 160 | rime_wrapper->session_id = rime_wrapper->api->create_session(); 161 | 162 | // wait for deploy 163 | rime_wrapper->api->join_maintenance_thread(); 164 | 165 | return 0; 166 | } 167 | 168 | int rime_wrapper_finish(RimeWrapper *rime_wrapper) 169 | { 170 | if (rime_wrapper->session_id) { 171 | rime_wrapper->api->destroy_session(rime_wrapper->session_id); 172 | rime_wrapper->session_id = 0; 173 | } 174 | rime_wrapper->api->finalize(); 175 | free(rime_wrapper); 176 | return 0; 177 | } 178 | 179 | RimeWrapper *rime_wrapper_init(void) 180 | { 181 | RimeWrapper *rime_wrapper; 182 | rime_wrapper_malloc(rime_wrapper, sizeof(RimeWrapper), NULL); 183 | 184 | rime_wrapper->first_run = 1; 185 | rime_wrapper->api = rime_get_api(); 186 | if (!rime_wrapper->api) { 187 | free(rime_wrapper); 188 | return NULL; 189 | } 190 | 191 | return rime_wrapper; 192 | // rime_wrapper_start(rime_wrapper, 0); 193 | // rime_wrapper_get_schemas(rime_wrapper); 194 | // rime_wrapper_get_commit(rime_wrapper); 195 | // rime_wrapper_finish(rime_wrapper); 196 | // return 0; 197 | } 198 | 199 | /* 200 | int main() 201 | { 202 | rime_wrapper_init(); 203 | } 204 | */ 205 | -------------------------------------------------------------------------------- /tmux_rime/rime_wrapper/rime.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #define RIME_DATA_DIR "/usr/share/rime-data" 7 | #define RIME_USER_DIR "/home/cycatz/.config/tmux_rime/rime" 8 | #define RIME_WRAPPER_VERSION "0.0.1" 9 | 10 | #define rime_wrapper_malloc(x, size, ret) \ 11 | do { \ 12 | (x) = malloc((size)); \ 13 | if ((x) == NULL) { \ 14 | perror("malloc\n"); \ 15 | return (ret); \ 16 | } \ 17 | } while (0) 18 | 19 | typedef struct _RimeWrapper { 20 | RimeSessionId session_id; 21 | RimeApi *api; 22 | int first_run; 23 | } RimeWrapper; 24 | 25 | void rime_wrapper_notification_handler(void*, RimeSessionId, const char*, const char*); 26 | int rime_wrapper_process_key(RimeWrapper *, int, int); 27 | int rime_wrapper_commit_composition(RimeWrapper *); 28 | 29 | char *rime_wrapper_get_input_str(RimeWrapper *); 30 | void rime_wrapper_free_str(char *); 31 | 32 | void rime_wrapper_set_cursor_pos(RimeWrapper *, int); 33 | void rime_wrapper_clear_composition(RimeWrapper *); 34 | 35 | RimeCommit *rime_wrapper_get_commit(RimeWrapper *); 36 | void rime_wrapper_free_commit(RimeWrapper *, RimeCommit *); 37 | 38 | RimeContext *rime_wrapper_get_context(RimeWrapper *); 39 | void rime_wrapper_free_context(RimeWrapper *, RimeContext *); 40 | 41 | RimeSchemaList *rime_wrapper_get_schema_list(RimeWrapper *); 42 | void rime_wrapper_free_schema_list(RimeWrapper *, RimeSchemaList *); 43 | 44 | int rime_wrapper_set_schema(RimeWrapper *, const char *); 45 | int rime_wrapper_start(RimeWrapper *, int); 46 | int rime_wrapper_finish(RimeWrapper *); 47 | RimeWrapper *rime_wrapper_init(void); 48 | -------------------------------------------------------------------------------- /tmux_rime/rime_wrapper/structs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from ctypes import * 4 | 5 | class RimeSchemaListItem(Structure): 6 | _fields_ = [('schema_id', c_char_p), 7 | ('name', c_char_p), 8 | ('reserved', c_void_p)] 9 | 10 | 11 | class RimeSchemaList(Structure): 12 | _fields_ = [('size', c_size_t), 13 | ('list', POINTER(RimeSchemaListItem))] 14 | 15 | 16 | class RimeCommit(Structure): 17 | _fields_ = [('data_size', c_int), 18 | ('text', c_char_p)] 19 | 20 | class RimeCandidate(Structure): 21 | _fields_ = [('text', c_char_p), 22 | ('comment', c_char_p), 23 | ('reserved', c_void_p)] 24 | 25 | 26 | class RimeMenu(Structure): 27 | _fields_ = [('page_size', c_int), 28 | ('page_no', c_int), 29 | ('is_last_page', c_bool), 30 | ('highlighted_candidate_index', c_int), 31 | ('num_candidates', c_int), 32 | ('candidates', POINTER(RimeCandidate)), 33 | ('select_keys', c_char_p)] 34 | 35 | class RimeComposition(Structure): 36 | _fields_ = [('length', c_int), 37 | ('cursor_pos', c_int), 38 | ('sel_start', c_int), 39 | ('sel_end', c_int), 40 | ('preedit', c_char_p)] 41 | 42 | 43 | 44 | class RimeContext(Structure): 45 | _fields_ = [('data_size', c_int), 46 | ('composition', RimeComposition), 47 | ('menu', RimeMenu), 48 | # v0.9.2 49 | ('commit_text_preview', c_char_p), 50 | ('select_labels', POINTER(c_char_p))] 51 | -------------------------------------------------------------------------------- /tmux_rime/tmux_rime_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import logging 5 | import socket 6 | import argparse 7 | 8 | 9 | # Fix the FIFO variable for testing 10 | TMUX_RIME_FIFO = '/tmp/tmux-rime.client' 11 | 12 | 13 | def init_logging(): 14 | logging_dir = os.path.dirname(os.path.realpath(sys.argv[0])) 15 | logging_path = os.path.join(logging_dir, 'tmux_rime_client.log') 16 | logging.basicConfig(filename=logging_path, 17 | format='[%(asctime)s] %(levelname)-8s %(message)s', 18 | datefmt='%m-%d %H:%M:%S', 19 | level=logging.INFO) 20 | 21 | 22 | class TmuxRimeParser: 23 | def __init__(self, client): 24 | self.client = client 25 | 26 | def start_session(self, args): 27 | self.client.start_session() 28 | 29 | def exit_session(self, args): 30 | self.client.exit_session() 31 | 32 | def send_key(self, args): 33 | self.client.send_key(key=args.key, 34 | modifier=args.modifier) 35 | 36 | def delete_char(self, args): 37 | self.client.delete_char() 38 | 39 | def commit_raw(self, args): 40 | self.client.commit_raw() 41 | 42 | def output(self, args): 43 | self.client.output() 44 | 45 | def parse(self): 46 | parser = argparse.ArgumentParser() 47 | subparser = parser.add_subparsers() 48 | 49 | parser_start = subparser.add_parser('start', help='Start a rime session') 50 | parser_start.set_defaults(func=self.start_session) 51 | 52 | parser_key = subparser.add_parser('key', help='Send keys to a session') 53 | parser_key.add_argument('-k', '--key', type=str, required=True, help='Specify the key') 54 | parser_key.add_argument('-m', '--modifier', type=str, help='Specify the key modifier') 55 | parser_key.set_defaults(func=self.send_key) 56 | 57 | parser_delete_char = subparser.add_parser('delete-char', 58 | help='Delete a char') 59 | parser_delete_char.set_defaults(func=self.delete_char) 60 | 61 | parser_commit_raw = subparser.add_parser('commit-raw', 62 | help='Commit the raw input') 63 | parser_commit_raw.set_defaults(func=self.commit_raw) 64 | 65 | parser_output = subparser.add_parser('output', help='Get inserted text') 66 | parser_output.set_defaults(func=self.output) 67 | 68 | parser_exit = subparser.add_parser('exit', help='Exit a rime session') 69 | parser_exit.set_defaults(func=self.exit_session) 70 | 71 | args = parser.parse_args() 72 | args.func(args) 73 | 74 | 75 | class TmuxRimeClient: 76 | def __init__(self): 77 | self.server_address = '/tmp/tmux-rime-socket' 78 | # [test] start session when initializing 79 | # self.start(0) 80 | 81 | def send_data(self, data, recv=False): 82 | with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: 83 | s.connect(self.server_address) 84 | try: 85 | s.sendall(data) 86 | if recv: 87 | res = s.recv(1024) 88 | return res 89 | finally: 90 | s.close() 91 | 92 | def write_pipe(self, data): 93 | with open(TMUX_RIME_FIFO, 'w') as f: 94 | f.write(data) 95 | 96 | def start_session(self): 97 | logging.info('Start session') 98 | self.send_data('start'.encode('utf-8')) 99 | 100 | def exit_session(self): 101 | logging.info('Exit session') 102 | self.send_data('exit'.encode('utf-8')) 103 | self.write_pipe('exit\n') 104 | 105 | def tmux_insert_text(self): 106 | inserted_text = self.send_data('output'.encode('utf-8'), True) 107 | if inserted_text is not None and len(inserted_text) > 0: 108 | message = 'insert {}'.format(inserted_text.decode('utf-8')) 109 | logging.info(message) 110 | self.write_pipe(message + '\n') 111 | 112 | def tmux_update_status(self, status_str): 113 | if status_str is not None: 114 | message = 'status {}'.format(status_str.decode('utf-8')) 115 | logging.info(message) 116 | self.write_pipe(message + '\n') 117 | 118 | def send_key(self, key, modifier): 119 | logging.info('Get key: {}, modifier: {}'.format(key, modifier)) 120 | 121 | status_str = self.send_data('key {} {}'.format(key, modifier) 122 | .encode('utf-8'), True) 123 | 124 | self.tmux_update_status(status_str) 125 | self.tmux_insert_text() 126 | 127 | def delete_char(self): 128 | logging.info('Delete a char') 129 | status_str = self.send_data('delete'.encode('utf-8'), True) 130 | self.tmux_update_status(status_str) 131 | 132 | def commit_raw(self): 133 | logging.info('Commit the raw input') 134 | status_str = self.send_data('raw'.encode('utf-8'), True) 135 | 136 | self.tmux_update_status(status_str) 137 | self.tmux_insert_text() 138 | 139 | def output(self): 140 | res = self.send_data('output'.encode('utf-8'), True) 141 | if res is not None: 142 | logging.info(res) 143 | 144 | 145 | if __name__ == '__main__': 146 | init_logging() 147 | client = TmuxRimeClient() 148 | parser = TmuxRimeParser(client) 149 | parser.parse() 150 | -------------------------------------------------------------------------------- /tmux_rime/tmux_rime_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import re 4 | import sys 5 | import logging 6 | import socketserver 7 | from rime_wrapper import RimeWrapper 8 | 9 | 10 | def init_logging(): 11 | logging_dir = os.path.dirname(os.path.realpath(sys.argv[0])) 12 | logging_path = os.path.join(logging_dir, 'tmux_rime_server.log') 13 | 14 | file_handler = logging.FileHandler(filename=logging_path) 15 | stderr_handler = logging.StreamHandler(sys.stderr) 16 | handlers = [file_handler, stderr_handler] 17 | 18 | logging.basicConfig(format='[%(asctime)s] %(levelname)-8s %(message)s', 19 | datefmt='%m-%d %H:%M:%S', 20 | level=logging.INFO, 21 | handlers=handlers) 22 | 23 | 24 | class TmuxRimeSession: 25 | def start(self): 26 | self.rime = RimeWrapper() 27 | self.rime.start() 28 | self.rime.set_schema('bopomofo') 29 | self.output_text = '' 30 | 31 | def finish(self): 32 | self.rime.finish() 33 | 34 | def handle_key(self, key, modifier): 35 | # Special case for space 36 | self.output_text = '' 37 | if key == 'Space': 38 | key = ' ' 39 | if key == ' ' and not self.rime.has_candidates(): 40 | self.commit_text() 41 | else: 42 | self.rime.process_key(ord(key), 0) 43 | 44 | def delete_char(self): 45 | self.rime.process_key(65288, 0) 46 | 47 | def commit_text(self): 48 | """ Commit the text """ 49 | commit_text_str = self.rime.get_commit_text_preview() 50 | self.output_text = commit_text_str 51 | self.clear_state() 52 | 53 | def commit_raw_str(self): 54 | """ Get the raw input """ 55 | self.output_text = self.rime.get_input_str() 56 | self.clear_state() 57 | 58 | def clear_state(self): 59 | """ Clear the composition """ 60 | self.rime.clear_composition() 61 | 62 | def get_output_text(self): 63 | return self.output_text 64 | 65 | def get_status_str(self): 66 | res = '' 67 | composition_preedit = self.rime.get_composition_preedit() 68 | 69 | if composition_preedit: 70 | res = '{} | '.format(composition_preedit) 71 | candidates = self.rime.get_candidates() 72 | for i, candidate in enumerate(candidates): 73 | text, comment = candidate 74 | res += '{}. {} '.format(i + 1 , text) 75 | return res 76 | 77 | 78 | class TmuxRimeRequestHandler(socketserver.BaseRequestHandler): 79 | def handle(self): 80 | data = self.request.recv(1024).strip().decode('utf-8') 81 | if data.startswith('key'): 82 | res = self.server.handle_key(data) 83 | if res is not None: 84 | self.request.sendall(res.encode('utf-8')) 85 | elif data.startswith('output'): 86 | logging.info('Start with output!') 87 | res = self.server.get_output_text() 88 | if res is not None: 89 | self.request.sendall(res.encode('utf-8')) 90 | elif data.startswith('raw'): 91 | res = self.server.commit_raw() 92 | if res is not None: 93 | self.request.sendall(res.encode('utf-8')) 94 | elif data.startswith('delete'): 95 | res = self.server.delete_char() 96 | if res is not None: 97 | self.request.sendall(res.encode('utf-8')) 98 | elif data.startswith('exit'): 99 | self.server.exit_session() 100 | else: 101 | command = data.split(' ')[0] 102 | logging.warning('Unknown command: {}'.format(command)) 103 | 104 | 105 | class TmuxRimeServer(socketserver.UnixStreamServer): 106 | def __init__(self): 107 | # Currently not support multiple sessions 108 | # self.sessions = {} 109 | self.session = TmuxRimeSession() 110 | self.session.start() 111 | self.server_address = '/tmp/tmux-rime-socket' 112 | try: 113 | os.unlink(self.server_address) 114 | except OSError: 115 | if os.path.exists(self.server_address): 116 | raise 117 | socketserver.UnixStreamServer.__init__(self, 118 | self.server_address, 119 | TmuxRimeRequestHandler) 120 | 121 | def start_server(self): 122 | self.serve_forever() 123 | 124 | def close_server(self): 125 | self._BaseServer__shutdown_request = True 126 | 127 | def exit_session(self): 128 | logging.info('Exit session') 129 | self.session.finish() 130 | self.close_server() 131 | 132 | def handle_key(self, data): 133 | key, modifier = tuple(re.split('\s', data)[1:]) 134 | 135 | logging.info('Received key: {}, modifier: {}' 136 | .format(key, modifier)) 137 | self.session.handle_key(key, modifier) 138 | return self.session.get_status_str() 139 | 140 | def commit_raw(self): 141 | logging.info('Commit the raw input') 142 | self.session.commit_raw_str() 143 | 144 | def delete_char(self): 145 | logging.info('Delete a char') 146 | self.session.delete_char() 147 | return self.session.get_status_str() 148 | 149 | def get_output_text(self): 150 | text = self.session.get_output_text() 151 | logging.info('Get output text: {}'.format(text)) 152 | return text 153 | 154 | 155 | if __name__ == '__main__': 156 | init_logging() 157 | with TmuxRimeServer() as server: 158 | server.start_server() 159 | -------------------------------------------------------------------------------- /utils/debugging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | tmux_rime_directory="$1" 5 | tmux_rime_server_log="$tmux_rime_directory/tmux_rime/tmux_rime_server.log" 6 | tmux_rime_client_log="$tmux_rime_directory/tmux_rime/tmux_rime_client.log" 7 | tmux_session_name="tmux_rime_debug" 8 | 9 | tmux kill-session -t "$tmux_session_name" 10 | tmux new-session -d -s "$tmux_session_name" -c "$tmux_rime_directory" 11 | tmux split-window -t "$tmux_session_name" -h "tail -f $tmux_rime_server_log" 12 | tmux split-window -t "$tmux_session_name" -v "tail -f $tmux_rime_client_log" 13 | tmux attach-session -d -t tmux_rime_debug 14 | --------------------------------------------------------------------------------