├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── autoload ├── fterm.vim └── fterm │ ├── colorscheme.vim │ ├── issue.vim │ └── terminal.vim ├── bin └── fterm ├── doc └── fterm.txt ├── plugin └── fterm.vim └── python └── fterm ├── __init__.py ├── ftermline.py ├── manager.py ├── terminal.py └── utils.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | * Make sure to search for a solution in old issues before posting a new one. 11 | * Learn at least a minimum of Markdown formatting (https://guides.github.com/features/mastering-markdown). 12 | 13 | **Information** 14 | - platform and distribution (windows 10, ubuntu 18.04, ...) 15 | - terminal (gnome terminal, xterm, alacritty, ...) 16 | - vim version (for example: vim 8.2 included patch 2017 or the output of `vim --version`) 17 | - error message (output of `:messages`) 18 | 19 | **Describe the bug** 20 | A clear and concise description of what the bug is. Maybe need some screenshots. 21 | 22 | **To Reproduce** 23 | First you need a minimal setting file: `minimal.vim` 24 | ```vim 25 | set nocompatible 26 | " change to your own path 27 | let &runtimepath = '~/.vim/bundles/vim-float-terminal,' . &runtimepath 28 | ``` 29 | Then run vim via `vim -u minimal.vim somefile`. 30 | Steps to reproduce the behavior in vim: 31 | 1. In normal mode, press ... 32 | 2. run ex commands ... 33 | 3. ... 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Vim ### 2 | # Swap 3 | [._]*.s[a-v][a-z] 4 | [._]*.sw[a-p] 5 | [._]s[a-v][a-z] 6 | [._]sw[a-p] 7 | 8 | # Session 9 | Session.vim 10 | 11 | # Temporary 12 | .netrwhist 13 | *~ 14 | # Auto-generated tag files 15 | tags 16 | 17 | ### Python ### 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | 118 | # toc 119 | *.md.orig* 120 | *.md.toc* 121 | 122 | # tags 123 | tags 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ZhiyuanLck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fterm 2 | 3 | Simple vim terminal in popup window with a **termline**. 4 | 5 | ## Screenshots 6 | 7 | ![buffer][1] 8 | 9 | ## Overview 10 | 11 | - Create/toggle/kill terminal in popup window handily. 12 | - Termline support just like tabline. 13 | - Blocked mapping only for fterm. 14 | 15 | ## Installation 16 | 17 | - Requirement: `+python3`, `+popup`, `+terminal` 18 | - Best experience: include patch >= 8.2.1997 19 | - Support for nvim: **not yet** 20 | 21 | For vim-plug: 22 | 23 | ```vim 24 | Plug 'ZhiyuanLck/vim-float-terminal' 25 | ``` 26 | 27 | ## Usage 28 | 29 | All commands are derived from command `:Fterm` 30 | 31 | ``` 32 | usage: Fterm [-h] {new,toggle,kill,select,settitle,move,quit} ... 33 | 34 | positional arguments: 35 | {new,toggle,kill,select,settitle,move} 36 | new create a new terminal 37 | toggle toggle a terminal to show or hidden 38 | kill kill a terminal 39 | settitle set the title of current terminal 40 | move move the place of terminal on termline 41 | ``` 42 | 43 | All subcommands have separate options, see more details via `:h fterm-commands`. 44 | 45 | ## Global Variables 46 | 47 | - Check normal global options by `:h fterm-options`. 48 | - Check mapping options by `:h fterm-mappings`. 49 | 50 | ## Mappings 51 | 52 | Fterm sets the default mappings for you. You can change them by mapping 53 | variables like `g:fterm_map_xxx`. There is a little difference when you change 54 | the mapping of selecting terminal, see more details by `:h g:fterm_map_select`. 55 | 56 | You can also disable all default mappings by `let g:fterm_disable_map = 1`, 57 | and then customize your own mapping through `g:fterm_custom_map`, which make 58 | it possible to define **blocked mapping** that takes effect only in fterm. See 59 | more details by `:h g:fterm_custom_map`. 60 | 61 | ## Quick quit 62 | 63 | You can quit the terminal quickly by command `FtermQuit` which is mapped to the key specified by `g:fterm_map_quit` (by default `q`). The keymap is disabled when certain pattern is matched in command to start the terminal. This work is done by vim built-in function `match()`. The default pattern list is 64 | 65 | ```vim 66 | let g:fterm_noquit=[ 67 | \ '\v(\w|/)*bash$', 68 | \ '\v(\w|/)*zsh$', 69 | \ '\v(\w|/)*ksh$', 70 | \ '\v(\w|/)*csh$', 71 | \ '\v(\w|/)*tcsh$' 72 | \ ] 73 | ``` 74 | 75 | ## Highlights 76 | 77 | You can customize highlights by `g:fterm_highlights`. See all default 78 | highlights by `:h g:fterm_highlights`. 79 | 80 | ## Edit file from terminal 81 | 82 | When in float terminal, you can use `fterm` to open a file in vim. 83 | 84 | ## Support for [asyncrun.vim](https://github.com/skywind3000/asyncrun.vim) 85 | 86 | ```vim 87 | let g:asyncrun_runner = get(g:, 'asyncrun_runner', {}) 88 | let g:asyncrun_runner.fterm = function('fterm#async_runner') 89 | ``` 90 | 91 | ## Known issues 92 | 93 | 1. Cursor position wrong in terminal popup with finished job. Fixed in [patch 8.2.1990](https://github.com/vim/vim/commit/6a07644db30cb5f3d0c6dc5eb2c348b6289da553). 94 | 95 | 2. Patch 8.2.1990 cause new problem: window changes when using bufload() while in a terminal popup. Fixed in [patch 8.2.1997](https://github.com/vim/vim/commit/8adc8d9b73121b647476a33d91d31d25e1c2d987). This issue will **cause the plugin to not work properly**. 96 | 97 | ## Reference 98 | [vim-floaterm](https://github.com/voldikss/vim-floaterm) 99 | 100 | [1]: https://github.com/ZhiyuanLck/images/blob/master/fterm/fterm.gif 101 | -------------------------------------------------------------------------------- /autoload/fterm.vim: -------------------------------------------------------------------------------- 1 | let g:ft_py = "py3 " 2 | exec g:ft_py "<< END" 3 | import vim, sys 4 | from pathlib import Path 5 | cwd = vim.eval('expand(":p:h")') 6 | cwd = Path(cwd) / '..' / 'python' 7 | cwd = cwd.resolve() 8 | sys.path.insert(0, str(cwd)) 9 | from fterm.utils import * 10 | from fterm.manager import * 11 | END 12 | 13 | let s:home = fnamemodify(resolve(expand(':p')), ':h') 14 | let s:script = fnamemodify(s:home . '/../bin', ':p') 15 | 16 | if stridx($PATH, s:script) < 0 17 | let $PATH .= ':' . s:script 18 | endif 19 | 20 | function! fterm#py(cmd) abort 21 | exec g:ft_py "".cmd 22 | endfunction 23 | 24 | function! fterm#cmd(...) abort 25 | exec g:ft_py "fterm_manager.start(vimeval('a:000'))" 26 | endfunction 27 | 28 | function! fterm#complete(A, L, P) abort 29 | return ['new', 'toggle', 'kill', 'select', 'settitle', 'move'] 30 | endfunction 31 | 32 | function! fterm#set_title() abort 33 | echohl WarningMsg 34 | let title = input('title: ') 35 | echohl None 36 | exec "FtermSetTitle ".title 37 | endfunction 38 | 39 | function! fterm#async_runner(opts) abort 40 | exec g:ft_py "fterm_manager.async_run()" 41 | endfunction 42 | 43 | function! fterm#edit(bufnr, path) abort 44 | exec g:ft_py printf("fterm_manager.edit_in_vim('%s')", a:path) 45 | endfunction 46 | 47 | function! fterm#map(modes, nore, args, lhs, rhs, block) abort 48 | let all_modes = 'nvxsoilct' 49 | for mode in split(a:modes == '' ? 'nvo' : a:modes, '\zs') 50 | if stridx(all_modes, mode) != -1 51 | if a:block 52 | let old_map = maparg(a:lhs, mode, 0, 1) 53 | endif 54 | exec printf("%s%smap %s %s %s", 55 | \ mode, a:nore ? 'nore' : '', a:args, a:lhs, a:rhs 56 | \ ) 57 | if a:block 58 | let g:fterm_blocked_mapping = get(g:, "fterm_blocked_mapping", []) 59 | call add(g:fterm_blocked_mapping, {'map': maparg(a:lhs, mode, 0, 1), 'mode': mode, 'old_map': old_map}) 60 | if old_map == {} 61 | exec mode."unmap ".a:lhs 62 | else 63 | call mapset(mode, 0, old_map) 64 | endif 65 | endif 66 | endif 67 | endfor 68 | endfunction 69 | 70 | function! s:set_map(map_dict) abort 71 | endfunction 72 | 73 | function! fterm#block_map() abort 74 | for map_dict in g:fterm_blocked_mapping 75 | call mapset(map_dict.mode, 0, map_dict.map) 76 | endfor 77 | endfunction 78 | 79 | function! fterm#restore_map() abort 80 | for map_dict in g:fterm_blocked_mapping 81 | let old_map = map_dict.old_map 82 | let map = map_dict.map 83 | let mode = map_dict.mode 84 | try 85 | if old_map == {} 86 | exec mode."unmap ".map.lhsraw 87 | else 88 | call mapset(mode, 0, old_map) 89 | endif 90 | catch /^Vim\%((\a\+)\)\=:E31:/ 91 | endtry 92 | endfor 93 | endfunction 94 | 95 | function! fterm#custom_map() abort 96 | try 97 | for m in g:fterm_custom_mapping 98 | call fterm#map(m[0], m[1], m[2], m[3], m[4], m[5]) 99 | endfor 100 | catch 101 | echohl Error 102 | echom "wrong cumstom mapping in 'g:fterm_custom_mapping'!" 103 | echohl None 104 | endtry 105 | endfunction 106 | 107 | augroup FtermWinLeave 108 | autocmd! 109 | autocmd WinLeave * exec g:ft_py "fterm_manager.winleave_cb()" 110 | augroup END 111 | -------------------------------------------------------------------------------- /autoload/fterm/colorscheme.vim: -------------------------------------------------------------------------------- 1 | let s:cterm_fg = synIDattr(hlID("Normal"), "fg", "cterm") ? "fg" : 251 2 | let s:cterm_bg = synIDattr(hlID("Normal"), "bg", "cterm") ? "bg" : 235 3 | let s:default_highlights = { 4 | \ "fterm_hl_border": { 5 | \ "ctermfg": 10, 6 | \ }, 7 | \ "fterm_hl_termline_info": { 8 | \ "ctermfg": 255, 9 | \ "ctermbg": 245, 10 | \ }, 11 | \ "fterm_hl_termline_normal": { 12 | \ "ctermfg": 252, 13 | \ "ctermbg": 240, 14 | \ }, 15 | \ "fterm_hl_termline_current": { 16 | \ "ctermfg": 0, 17 | \ "ctermbg": 84, 18 | \ }, 19 | \ "fterm_hl_termline_sep_a": { 20 | \ "ctermfg": 255, 21 | \ "ctermbg": 245, 22 | \ }, 23 | \ "fterm_hl_termline_sep_b": { 24 | \ "ctermfg": 255, 25 | \ "ctermbg": 245, 26 | \ }, 27 | \ "fterm_hl_termline_sep_c": { 28 | \ "ctermfg": 255, 29 | \ "ctermbg": 245, 30 | \ }, 31 | \ "fterm_hl_terminal_body": { 32 | \ "ctermfg": s:cterm_fg, 33 | \ "ctermbg": s:cterm_bg, 34 | \ }, 35 | \ "fterm_hl_termline_body": { 36 | \ "ctermfg": s:cterm_fg, 37 | \ "ctermbg": s:cterm_fg, 38 | \ }, 39 | \ } 40 | 41 | function! s:get_cmd(name, hl) abort 42 | let cmd = map(a:hl, v:key.'='.v:val) 43 | let cmd = join(cmd, ' ') 44 | return 'highlight! '.a:name.' '.cmd 45 | endfunction 46 | 47 | function! fterm#colorscheme#set() abort 48 | let hl_list = s:default_highlights 49 | let extra_list = get(g:, "fterm_highlights", {}) 50 | call extend(hl_list, extra_list) 51 | for [name, hl] in items(hl_list) 52 | let cmd = "highlight! ".name 53 | for [k, v] in items(hl) 54 | let cmd .= printf(" %s=%s", k, v) 55 | endfor 56 | exec cmd 57 | endfor 58 | endfunction 59 | -------------------------------------------------------------------------------- /autoload/fterm/issue.vim: -------------------------------------------------------------------------------- 1 | function! fterm#issue#patch_821990() abort 2 | if has('patch-8.2.1990') && !has('patch-8.2.1997') 3 | echohl Error 4 | echom "You're using vim with patch-8.2.1990 which has serious issue of terminal popup that was fixed in patch-8.2.1997. Please update to latest vim to ensure that vim has patch-8.2.1997." 5 | echohl None 6 | return 1 7 | else 8 | return 0 9 | endif 10 | endfunction 11 | -------------------------------------------------------------------------------- /autoload/fterm/terminal.vim: -------------------------------------------------------------------------------- 1 | function! fterm#terminal#kill(bufnr) abort 2 | let job = term_getjob(a:bufnr) 3 | if job == v:null 4 | return 5 | endif 6 | if job_status(job) !=# 'dead' 7 | call job_stop(job) 8 | endif 9 | if bufexists(a:bufnr) 10 | execute a:bufnr . 'bwipeout!' 11 | endif 12 | endfunction 13 | -------------------------------------------------------------------------------- /bin/fterm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # from https://github.com/voldikss/vim-floaterm/blob/master/bin/floaterm 3 | 4 | absolute_path() { 5 | local result="" 6 | if [ -x "$(which realpath 2> /dev/null)" ]; then 7 | result="$(realpath -s """$1""" 2> /dev/null)" 8 | fi 9 | if [ -z "$result" ] && [ -x "$(which perl 2> /dev/null)" ]; then 10 | result="$(perl -MCwd -e 'print Cwd::realpath($ARGV[0])' """$1""")" 11 | fi 12 | if [ -z "$result" ] && [ -x "$(which python 2> /dev/null)" ]; then 13 | result="$(python -c 'import sys, os;sys.stdout.write(os.path.abspath(sys.argv[1]))' """$1""")" 14 | fi 15 | if [ -z "$result" ] && [ -x "$(which python3 2> /dev/null)" ]; then 16 | result="$(python3 -c 'import sys, os;sys.stdout.write(os.path.abspath(sys.argv[1]))' """$1""")" 17 | fi 18 | if [ -z "$result" ] && [ -x "$(which python2 2> /dev/null)" ]; then 19 | result="$(python2 -c 'import sys, os;sys.stdout.write(os.path.abspath(sys.argv[1]))' """$1""")" 20 | fi 21 | if [ -z "$result" ] && [ -x "$(which realpath 2> /dev/null)" ]; then 22 | result="$(realpath """$1""")" 23 | fi 24 | if [ -z "$result" ]; then 25 | result="$1" 26 | fi 27 | echo $result 28 | } 29 | 30 | name="$(absolute_path """$1""")" 31 | printf '\033]51;["call", "fterm#edit", "%s"]\007' "$name" 32 | -------------------------------------------------------------------------------- /doc/fterm.txt: -------------------------------------------------------------------------------- 1 | *fterm.txt* simple vim popup terminal plugin 2 | ============================================================================== 3 | 4 | Author : ZhiyuanLck 5 | License: MIT license 6 | 7 | ============================================================================== 8 | 9 | CONTENTS *fterm-contents* 10 | Introduction |fterm-introduction| 11 | Options |fterm-options| 12 | Commands |fterm-commands| 13 | Mappings |fterm-mappings| 14 | Highlights |fterm-highlights| 15 | Quick quit |fterm-quick-quit| 16 | Edit file from terminal |fterm-edit-file| 17 | Support for asyncrun |fterm-support-asyncrun| 18 | Known issues |fterm-issues| 19 | Reference |fterm-reference| 20 | 21 | ============================================================================== 22 | Introduction *fterm-introduction* 23 | 24 | Use vim built-in terminal in popup with a termline. 25 | 26 | - Create/toggle/kill terminal in popup quickly. 27 | - Termline support just like tabline. 28 | - Blocked mapping only for fterm. 29 | - Support for |asyncrun.vim| 30 | 31 | ============================================================================== 32 | Options *fterm-options* 33 | 34 | g:fterm_shell *g:fterm_shell* 35 | Specify the shell to be used in terminal. 36 | 37 | g:fterm_width *g:fterm_width* 38 | Specify the width of the terminal. 39 | If the value is between 0 and 1, the width is relative to 'columns'. 40 | If the value is greater than 1, number of columns of the popup is set to the 41 | value. 42 | If the value is invalid, the width is set to the default value. 43 | Default value is 0.75. 44 | 45 | g:fterm_height *g:fterm_height* 46 | Specify the height of the terminal. 47 | If the value is between 0 and 1, the height is relative to 'lines'. 48 | If the value is greater than 1, number of lines of the popup is set to the 49 | value. 50 | If the value is invalid, the height is set to the default value. 51 | Default value is 0.75. 52 | 53 | g:fterm_autoquit *g:fterm_autoquit* 54 | If set to 1, you will quit vim with no warning about the running job. 55 | Use this option with caution, you may lost your job! 56 | Default 0. 57 | 58 | g:fterm_open_cmd *g:fterm_open_cmd* 59 | Command used when openning a file from the terminal in popup. 60 | Default 'tabedit'. 61 | 62 | g:fterm_use_root *g:fterm_use_root* 63 | If set to 1, fterm will try to search the root directory of the project and 64 | use it as current working directory of the terminal. 65 | Default 1. 66 | 67 | g:fterm_root_marker *g:fterm_root_marker* 68 | Markers that will mark current directory as root directory. 69 | Default ['.root', '.git', '.svn', '.hg', '.project']. 70 | 71 | g:fterm_root_search_level *g:fterm_root_search_level* 72 | Max level when trying to search the root directory. 73 | Default 5. 74 | 75 | g:fterm_expanduser *g:fterm_expanduser* 76 | If set to 1, fterm will try to expand every part of string specified by 77 | '--cmd' option that contains '~'. For example, '~' will be expanded to 78 | '/home/username'. 79 | Default 1. 80 | 81 | g:fterm_toggle_default *g:fterm_toggle_default* 82 | Save the command that is used by |:FtermToggle| when there is no terminal to 83 | toggle. 84 | Default "FtermNew". 85 | 86 | g:fterm_exclude_cmdline *g:fterm_exclude_cmdline* 87 | Exclude the cmdline when calculating the height of terminal. 88 | Default 1. 89 | 90 | g:fterm_exclude_statusline *g:fterm_exclude_statusline* 91 | Exclude the statusline when calculating the height of terminal. 92 | Default 1. 93 | 94 | g:fterm_exclude_tabline *g:fterm_exclude_tabline* 95 | Exclude the tabline when calculating the height of terminal. 96 | Default 1. 97 | 98 | g:fterm_exclude_signcolumns *g:fterm_exclude_signcolumns* 99 | Exclude the signcolumns when calculating the width of terminal. 100 | Default 0. 101 | 102 | g:fterm_borderchars *g:fterm_borderchars* 103 | Characters of the whole window border. 104 | Default ["─", "│", "─", "│", "┌", "┐", "┘", "└"]. 105 | 106 | g:fterm_termline_pos *g:fterm_termline_pos* 107 | Pos of the termline. 108 | Value can be one of ["outertop", "innertop", "outerbottom", "innerbottom"]. 109 | Default "outertop". 110 | 111 | g:fterm_title *g:fterm_title* 112 | Default title name that is shown in termline. 113 | Default "fterm". 114 | 115 | g:fterm_restore_curpos *g:fterm_restore_curpos* 116 | If set to 1, fterm will try to restore the cursor position after reopening 117 | the terminal in popup, otherwill fterm will always enter terminal mode. 118 | Require patch 8.2.1978: https://github.com/vim/vim/releases/tag/v8.2.1978 119 | Default 1. 120 | 121 | ============================================================================== 122 | Commands *fterm-commands* 123 | 124 | :Fterm [-h] {new,toggle,kill,select,settitle,move,quit} ... *Fterm* 125 | Main command used to control the terminal. 126 | 127 | :FtermNew *:FtermNew* 128 | Create a new terminal. Alias for 129 | > 130 | :Fterm new 131 | < 132 | *Fterm-new* 133 | :Fterm new [-h] [--cwd CWD] [--cmd CMD [CMD ...]] [--title title] 134 | [--width width] [--height height] 135 | Create a new terminal. 136 | If '--cwd' is set, cwd of terminal is set to the specified path. 137 | If '--cmd' is set, run commands otherwise run command specified by 138 | |g:fterm_shell|. Commands are processed by so you don't need to 139 | quote them. 140 | If '--title' is set, the title of the terminal will be set to the value of 141 | this option. 142 | If '--width' and/or '--height' is set, global value of width and/or height 143 | of the popup will be covered. Width and height are saved for every popup 144 | terminal respectively. 145 | 146 | Create terminal with default shell: 147 | > 148 | :Fterm new 149 | < 150 | Create terminal with custom title: 151 | > 152 | :Fterm new --title custom 153 | < 154 | Create terminal under home directory: 155 | > 156 | :Fterm new --cwd ~ 157 | < 158 | Create terminal with specified command: 159 | > 160 | :Fterm new --cmd man ls 161 | < 162 | Create terminal with command with options: 163 | In this case, you must escape white space to ensure the parser to receive 164 | ['new', '--cmd', 'ls -la'] rather than ['new', '--cmd', 'ls', '-la']. The 165 | latter will throw error of unrecognized argumnets. More details see 166 | ||. 167 | > 168 | :Fterm new --cmd ls\ -la 169 | < 170 | Create terminal with specified height and width: 171 | > 172 | :Fterm new --width 0.8 --height 20 173 | < 174 | 175 | :FtermToggle *:FtermToggle* 176 | Toggle an existed terminal or run default command. Alias for 177 | > 178 | :Fterm toggle 179 | < 180 | 181 | :Fterm toggle [-h] *Fterm-toggle* 182 | If there exists no terminal created by |:FtermNew|, run commands specified 183 | by |g:fterm_toggle_default|. 184 | If fterm is visible, hide current terminal, else show the current terminal. 185 | 186 | :FtermKill *:FtermKill* 187 | Kill current terminal. Alias for 188 | > 189 | :Fterm kill 190 | < 191 | 192 | :FtermKillAll *:FtermKillAll* 193 | Kill all terminals. Alias for 194 | > 195 | :Fterm kill --all 196 | < 197 | 198 | :Fterm kill [-h] [--all] *Fterm-kill* 199 | Kill current terminal and show next terminal if possible. 200 | If '--all' is set, kill all terminals. 201 | 202 | :FtermSelect [-h] terminal_number *:FtermSelect* 203 | Show the terminal with specified number. Alias for 204 | > 205 | :Fterm select 206 | < 207 | 208 | :Fterm select [-h] terminal_number *Fterm-select* 209 | Show the terminal with specified number. 210 | 211 | :FtermSetTitle [-h] title *FtermSetTitle* 212 | Set the title of current terminal. Alias for 213 | > 214 | :Fterm settitle 215 | < 216 | 217 | :Fterm settitle [-h] title *Fterm-settitle* 218 | Set the title of current terminal. 219 | 220 | :Fterm move [-h] [--left N] [--right N] [--to N] [--end] *Fterm-move* 221 | Move the termtab on termline. 222 | If '--left' is set, move current termtab left N tabs. 223 | If '--right' is set, move current termtab right N tabs. 224 | If '--to' is set, move current termtab to pos N of the termline. If N is 225 | invalid, set N to the remainder of the division of N by number of terminals. 226 | If '--end' is set, move current termtab to end of the termline. 227 | Priority of above arguments is 228 | > 229 | --end > --to > --left > --right 230 | < 231 | 232 | :FtermMoveTo N *:FtermMoveTo* 233 | Move current termtab to pos N of the termline. See |Fterm-move|. Alias for 234 | > 235 | :Fterm move --to N 236 | < 237 | 238 | :FtermMoveStart *:FtermMoveStart* 239 | Move current termtab to start of the termline. Alias for 240 | > 241 | :Fterm move --to 1 242 | < 243 | 244 | :FtermMoveEnd *:FtermMoveEnd* 245 | Move current termtab to end of the termline. Alias for 246 | > 247 | :Fterm move --end 248 | < 249 | 250 | :FtermMoveLeft *:FtermMoveLeft* 251 | Exchange the position with the left terminal. Alias for 252 | > 253 | :Fterm move --left 1 254 | < 255 | 256 | :FtermMoveRight *:FtermMoveRight* 257 | Exchange the position with the right terminal. Alias for 258 | > 259 | :Fterm move --right 1 260 | < 261 | 262 | :Lazygit *:Lazygit* 263 | Run lazygit if it exists and |g:fterm_cmd_lazygit| is 1. 264 | > 265 | command! -bar -nargs=0 Lazygit FtermNew 266 | \ --cmd lazygit --title git --width 1 --height 1 267 | < 268 | 269 | g:fterm_cmd_lazygit *g:fterm_cmd_lazygit* 270 | If set to 1, define |:Lazygit| command. 271 | Default 0. 272 | 273 | ============================================================================== 274 | Mappings *fterm-mappings* 275 | 276 | Fterm sets some default mappings for you. You can change single mapping by 277 | change the value of corresponding variable. If you don't like this mappings 278 | set |g:fterm_disable_map| to 1 to disable all mappings and define your own 279 | mappings by |g:fterm_custom_map|, which makes it possible to define mappings 280 | that only work when fterm is shown. 281 | 282 | g:fterm_disable_map *g:fterm_disable_map* 283 | If set to 1, disable all mappings. Then you need to define mappings by 284 | yourself. 285 | Default 0. 286 | 287 | g:fterm_map_new *g:fterm_map_new* 288 | Mapping to create new terminal. See |Fterm-new| and |:FtermNew|. 289 | Default 'c'. 290 | 291 | g:fterm_map_toggle *g:fterm_map_toggle* 292 | Mapping to toggle current terminal. See |Fterm-toggle| and |:FtermToggle|. 293 | Default 's'. 294 | 295 | g:fterm_map_kill *g:fterm_map_kill* 296 | Mapping to kill current terminal and show next one. See |Fterm-kill| and 297 | |:FtermKill| 298 | Default 'k'. 299 | 300 | g:fterm_map_killall *g:fterm_map_killall* 301 | Mapping to kill all terminals. See |Fterm-kill| and |:FtermKillAll|. 302 | Default 'a'. 303 | 304 | g:fterm_map_settitle *g:fterm_map_settitle* 305 | Mapping to set the title of current terminal. See |Fterm-settitle| and 306 | |:FtermSetTitle|. 307 | Default ','. 308 | 309 | g:fterm_map_moveright *g:fterm_map_moveright* 310 | Mapping to exchange the position of current terminal with the right one. See 311 | |Fterm-move| and |FtermMoveRight|. 312 | Default 'l'. 313 | 314 | g:fterm_map_moveleft *g:fterm_map_moveleft* 315 | Mapping to exchange the position of current terminal with the left one. See 316 | |Fterm-move| and |FtermMoveLeft|. 317 | Default 'h'. 318 | 319 | g:fterm_map_movestart *g:fterm_map_movestart* 320 | Mapping to move current terminal to the start of the termline. See 321 | |Fterm-move| and |FtermMoveStart|. 322 | Default 'a'. 323 | 324 | g:fterm_map_moveend *g:fterm_map_moveend* 325 | Mapping to move current terminal to the end of the termline. See 326 | |Fterm-move| and |FtermMoveEnd|. 327 | Default 'e'. 328 | 329 | g:fterm_map_select *g:fterm_map_select* 330 | Prefix of mappings to show terminal with specified number. 331 | 'm*' is for meta key and 'c*' is for control CTRL, others are normal prefix. 332 | 333 | g:fterm_custom_map *g:fterm_custom_map* 334 | Two-dimen list used to define custom mappings. Every element of list is an 335 | array of lenth 6. These 6 elements are fed to the function |fterm#map()|, 336 | which accepts 6 arguments: 337 | fterm#map({modes}, {nore}, {args}, {lhs}, {rhs}, {block}) 338 | {modes} is a string, valid characters of which are "nvxsoilct". Invalid 339 | charater will be ignored and empty string will be replaced with "nvo". 340 | {nore} is 0 or 1. See |:nore| for details. 341 | {args} is string of |:map-arguments|. 342 | {lhs} means left-hand-side. 343 | {rhs} means right-hand-side. 344 | {block} is 0 or 1. Decide whether to block mappings. Blocking mapping 345 | means the mapping will cover the original mapping when fterm is shown and 346 | then the mapping will be restored after fterm is hidden. 347 | 348 | Example mapping definition: 349 | > 350 | let g:fterm_custom_mapping = [ 351 | \ [ '', 1, '', 'c', ':FtermNew', 0], 352 | \ [ 't', 1, '', 'c', ':FtermNew', 0], 353 | \ ] 354 | < 355 | If your vim has patch 8.2.1978 `:echo has('patch-8.2.1978')` 356 | > 357 | let g:fterm_custom_mapping = [ 358 | \ [ 'tnvo', 1, '', 'c', 'FtermNew', 0], 359 | \ ] 360 | < 361 | An expample of blocked mapping: 362 | > 363 | let g:fterm_custom_mapping = [ 364 | \ [ 'tnvo', 1, '', 'ta', 'FtermMoveStart', 1], 365 | \ ] 366 | < 367 | 368 | ============================================================================== 369 | Highlights *fterm-highlights* 370 | 371 | g:fterm_highlights *g:fterm_highlights* 372 | Highlights dict. 373 | Default value: 374 | > 375 | let g:fterm_highlights = { 376 | \ "fterm_hl_border": { 377 | \ "ctermfg": 10, 378 | \ }, 379 | \ "fterm_hl_termline_info": { 380 | \ "ctermfg": 255, 381 | \ "ctermbg": 245, 382 | \ }, 383 | \ "fterm_hl_termline_normal": { 384 | \ "ctermfg": 252, 385 | \ "ctermbg": 240, 386 | \ }, 387 | \ "fterm_hl_termline_current": { 388 | \ "ctermfg": 0, 389 | \ "ctermbg": 84, 390 | \ }, 391 | \ "fterm_hl_terminal_body": { 392 | \ "ctermfg": "fg", 393 | \ "ctermbg": "bg", 394 | \ }, 395 | \ "fterm_hl_termline_body": { 396 | \ "ctermfg": "fg", 397 | \ "ctermbg": "bg", 398 | \ }, 399 | \ } 400 | < 401 | 402 | ============================================================================== 403 | Quit quit *fterm-quick-quit* 404 | 405 | You can quit the terminal quickly by command |:FtermQuit| which is mapped to 406 | the key specified by |g:fterm_map_quit|. The mapping is disabled when certain 407 | pattern is matched in command to start the terminal. This work is done by vim 408 | built-in function |match()|. 409 | 410 | :Fterm quit *Fterm-quit* 411 | Quit current terminal and close popup. 412 | 413 | :FtermQuit *:FtermQuit* 414 | Quit current terminal and close popup. Alias for 415 | > 416 | :Fterm quit 417 | < 418 | 419 | g:fterm_map_quit *g:fterm_map_quit* 420 | Mapping to quit current terminal and close popup. See |Fterm-quit| and 421 | |:FtermQuit|. 422 | Default 'q'. 423 | 424 | g:fterm_noquit *g:fterm_noquit* 425 | Pattern list that define whether to map keys specified by |g:fterm_map_quit| 426 | to |:FtermQuit|. If pattern in the list is matched, do nothing. 427 | Default value: 428 | > 429 | let g:fterm_noquit=[ 430 | \ '\v(\w|/)*bash$', 431 | \ '\v(\w|/)*zsh$', 432 | \ '\v(\w|/)*ksh$', 433 | \ '\v(\w|/)*csh$', 434 | \ '\v(\w|/)*tcsh$' 435 | \ ] 436 | < 437 | 438 | ============================================================================== 439 | Edit file from terminal *fterm-edit-file* 440 | 441 | Inside terminal you can use command "fterm" instead of "vim" to open file in 442 | vim. You can customize the open command, see |g:fterm_open_cmd|. 443 | 444 | The shell script borrow the code from |floaterm|: 445 | https://github.com/voldikss/vim-floaterm/blob/master/bin/floaterm 446 | 447 | ============================================================================== 448 | Support for asyncrun *fterm-support-asyncrun* 449 | 450 | Add following lines to support running commands via |asyncrun| in fterm: 451 | > 452 | let g:asyncrun_runner = get(g:, 'asyncrun_runner', {}) 453 | let g:asyncrun_runner.fterm = function('fterm#async_runner') 454 | < 455 | 456 | ============================================================================== 457 | Known issues *fterm-issues* 458 | 459 | 1. Cursor position wrong in terminal popup with finished job. 460 | Fixed in patch-8.2.1990. 461 | https://github.com/vim/vim/commit/6a07644db30cb5f3d0c6dc5eb2c348b6289da553 462 | 463 | 2. Patch-8.2.1990 cause new problem: window changes when using |bufload()| 464 | while in a terminal popup. Fixed in patch-8.2.1997. This issue will cause 465 | the plugin to not work properly. 466 | https://github.com/vim/vim/commit/8adc8d9b73121b647476a33d91d31d25e1c2d987 467 | 468 | ============================================================================== 469 | vim:tw=78:ts=2:ft=help:fen: 470 | -------------------------------------------------------------------------------- /plugin/fterm.vim: -------------------------------------------------------------------------------- 1 | function! s:init_var(name, default) abort 2 | let var = get(g:, 'fterm_'.a:name, a:default) 3 | exec "let g:fterm_".a:name." = var" 4 | endfunction 5 | 6 | let s:windows = has("win32") || has("win64") 7 | let s:shell = s:windows ? "cmd" : &shell 8 | 9 | " attributes 10 | call s:init_var('shell', s:shell) 11 | call s:init_var('width', 0.75) 12 | call s:init_var('height', 0.75) 13 | call s:init_var('autoquit', 0) 14 | call s:init_var('exclude_cmdline', 1) 15 | call s:init_var('exclude_statusline', 1) 16 | call s:init_var('exclude_tabline', 1) 17 | call s:init_var('exclude_signcolumn', 0) 18 | call s:init_var('open_cmd', 'tabedit') 19 | call s:init_var('noquit', ['\v(\w|/)*bash$', '\v(\w|/)*zsh$', '\v(\w|/)*ksh$', '\v(\w|/)*csh$', '\v(\w|/)*tcsh$', 'lazygit']) 20 | call s:init_var('use_root', 1) 21 | call s:init_var('root_marker', ['.root', '.git', '.svn', '.hg', '.project']) 22 | call s:init_var('root_search_level', 5) 23 | call s:init_var('expanduser', 1) 24 | call s:init_var('toggle_default', 'FtermNew') 25 | call s:init_var('restore_curpos', 1) 26 | " termline 27 | call s:init_var('borderchars', 28 | \ ['─', '│', '─', '│', '┌', '┐', '┘', '└']) 29 | call s:init_var('termline_pos', 'outertop') 30 | call s:init_var('title', 'fterm') 31 | call s:init_var('title_max_width', 8) 32 | call s:init_var('termline_sep', '') 33 | " highlights 34 | call s:init_var('hl_terminal_border', ['fterm_hl_terminal_border']) 35 | call s:init_var('hl_termline_border', ['fterm_hl_termline_border']) 36 | call s:init_var('hl_terminal_body', 'fterm_hl_terminal_body') 37 | call s:init_var('hl_termline_body', 'fterm_hl_termline_body') 38 | " key map 39 | call s:init_var('disable_map', 0) 40 | call s:init_var('map_select', 'm*') 41 | call s:init_var('map_quit', 'q') 42 | call s:init_var('blocked_mapping', []) 43 | call s:init_var('custom_mapping', []) 44 | " cmd 45 | call s:init_var('cmd_lazygit', 1) 46 | 47 | " 0 for tmap and unblocked map, 1 for tmap and blocked map 48 | function! s:init_map(map, lhs, rhs, block=0) abort 49 | call s:init_var('map_'.a:map, a:lhs) 50 | exec "let l:lhs = g:fterm_map_".a:map 51 | let has_cmd = has("patch-8.2.1978") 52 | let t_pre = has_cmd ? '' : ':' 53 | let n_pre = has_cmd ? '' : ':' 54 | call fterm#map('t', 1, '', l:lhs, t_pre.a:rhs.'', 0) 55 | call fterm#map('nvo', 1, '', l:lhs, n_pre.a:rhs.'', a:block) 56 | endfunction 57 | 58 | function! s:get_pattern() abort 59 | let mode = g:fterm_map_select 60 | if mode == 'm*' 61 | return {k -> ''} 62 | elseif mode == 'c*' 63 | return {k -> ''} 64 | else 65 | return {k -> mode.k} 66 | endif 67 | endfunction 68 | 69 | command! -bar -complete=customlist,fterm#complete -nargs=+ Fterm call fterm#cmd() 70 | command! -bar -nargs=* FtermNew Fterm new 71 | command! -bar -nargs=* FtermToggle Fterm toggle 72 | command! -bar -nargs=? FtermKill Fterm kill 73 | command! -bar -nargs=0 FtermKillAll Fterm kill --all 74 | command! -bar -nargs=1 FtermSelect Fterm select 75 | command! -bar -nargs=1 FtermSetTitle Fterm settitle 76 | command! -bar -nargs=1 FtermMoveTo Fterm move --to 77 | command! -bar -nargs=0 FtermMoveStart Fterm move --to 1 78 | command! -bar -nargs=0 FtermMoveEnd Fterm move --end 79 | command! -bar -nargs=1 FtermMoveLeft Fterm move --left 80 | command! -bar -nargs=1 FtermMoveRight Fterm move --right 81 | command! -bar -nargs=0 FtermQuit Fterm quit 82 | 83 | if get(g:, 'fterm_cmd_lazygit', 1) && executable('lazygit') 84 | command! -bar -nargs=0 Lazygit FtermNew 85 | \ --cmd lazygit --title git --width 1 --height 1 86 | endif 87 | 88 | if g:fterm_disable_map < 1 89 | call s:init_map('new', 'c', 'FtermNew') 90 | call s:init_map('toggle', 's', 'FtermToggle') 91 | call s:init_map('kill', 'k', 'FtermKill') 92 | call s:init_map('killall', 'a', 'FtermKillAll') 93 | call s:init_map('settitle', ',', 'call fterm#set_title()', 1) 94 | call s:init_map('moveright', 'tl', 'FtermMoveRight 1', 1) 95 | call s:init_map('moveleft', 'th', 'FtermMoveLeft 1', 1) 96 | call s:init_map('movestart', 'ta', 'FtermMoveStart', 1) 97 | call s:init_map('moveend', 'te', 'FtermMoveEnd', 1) 98 | for i in range(1, 10) 99 | let Pattern = s:get_pattern() 100 | let num = i % 10 101 | call s:init_map('select'.num, Pattern(num), 'FtermSelect '.num, 1) 102 | endfor 103 | endif 104 | call fterm#custom_map() 105 | 106 | " 异步关闭窗口 107 | -------------------------------------------------------------------------------- /python/fterm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhiyuanLck/vim-float-terminal/90186a243c1ea03c7efcba5c5ef6ea21383f7fda/python/fterm/__init__.py -------------------------------------------------------------------------------- /python/fterm/ftermline.py: -------------------------------------------------------------------------------- 1 | import vim 2 | from functools import partial 3 | 4 | from .utils import * 5 | 6 | class FtermLine(object): 7 | def __init__(self, manager): 8 | self.manager = manager 9 | self.bufnr = vimeval("bufadd('')", 1) 10 | # a : fg = left normal , bg = right normal 11 | # b : fg = left normal , bg = right current 12 | # c : fg = left current , bg = right normal 13 | self.sep = ftget("termline_sep", "''") 14 | # self.prop_types = ["info", "normal", "current", "sep_a", "sep_b", "sep_c"] 15 | self.prop_types = ["info", "normal", "current"] 16 | self.init_highlights() 17 | self.init_text_prop() 18 | 19 | def init_highlights(self): 20 | vimcmd("call fterm#colorscheme#set()") 21 | 22 | def init_text_prop(self): 23 | # optsions = {"bufnr": self.bufnr} 24 | optsions = dict() 25 | for prop_type in self.prop_types: 26 | optsions["highlight"] = ftget("hl_termline_{}".format(prop_type), "'fterm_hl_termline_{}'".format(prop_type)) 27 | vimcmd("call prop_type_add('fterm_{}', {})".format(prop_type, str(optsions))) 28 | 29 | def set_buffer(self): 30 | setlocal = partial(vim_win_setlocal, self.winid) 31 | setlocal("nobuflisted") 32 | setlocal("buftype=nofile") 33 | setlocal("bufhidden=hide") 34 | setlocal("undolevels=-1") 35 | setlocal("noswapfile") 36 | setlocal("nolist") 37 | setlocal("nonumber norelativenumber") 38 | setlocal("nospell") 39 | setlocal("nofoldenable") 40 | setlocal("foldmethod=manual") 41 | setlocal("shiftwidth=4") 42 | setlocal("nocursorline") 43 | setlocal("foldcolumn=0") 44 | setlocal("signcolumn=no") 45 | setlocal("colorcolumn=") 46 | # setlocal("wincolor=...") 47 | setlocal("filetype=ftermline") 48 | 49 | def build_line(self): 50 | self.set_popup() 51 | self.set_buffer() 52 | self.set_content() 53 | self.set_text_prop() 54 | 55 | def get_coor(self): 56 | pos = get_termline_pos() 57 | term = self.manager.get_curterm() 58 | anchor_line = term.anchor_line 59 | self.col = term.col 60 | if pos == 'innertop' or pos == 'outertop': 61 | self.line = anchor_line 62 | if pos == 'innerbottom': 63 | self.line = anchor_line + term.height 64 | if pos == 'outerbottom': 65 | self.line = anchor_line + term.height + 2 # 2 borders 66 | 67 | def set_popup(self): 68 | self.get_coor() 69 | term = self.manager.get_curterm() 70 | opts = { 71 | "maxwidth": term.width, 72 | "minwidth": term.width, 73 | "maxheight": 1, 74 | "minheight": 1, 75 | "zindex": 1000, 76 | "pos": "topleft", 77 | "line": self.line, 78 | "col": self.col, 79 | "scrollbar": 0, 80 | "mapping": 0, 81 | } 82 | opts["borderhighlight"] = ftget("hl_terminal_border", "'fterm_hl_border'") 83 | opts["border"], opts["borderchars"], opts["padding"] = get_termline_border() 84 | opts["highlight"] = ftget("hl_terminal_body", "'fterm_hl_terminal_body'") 85 | self.winid = vimeval("popup_create({}, {})".format(self.bufnr, str(opts)), 1) 86 | self.bufnr = vimeval("winbufnr({})".format(self.winid), 1) 87 | 88 | def set_content(self): 89 | termline = [] 90 | curterm = self.manager.get_curterm() 91 | rest = curterm.width 92 | for i, term in enumerate(self.manager.term_list): 93 | # title = " {} {} ".format(i + 1, term.title) 94 | termline.append(" {} {} ".format(i + 1, term.title)) 95 | # termline.append(self.format_title(i + 1, term.title)) 96 | self.titles = termline 97 | termline = self.sep.join(termline) 98 | vimcmd("call setbufline({}, 1, '{}')".format(self.bufnr, termline)) 99 | 100 | # to be used 101 | def format_title(self, nr, title): 102 | max_w = ftget("title_max_width", 1, 1) 103 | title = title[:5] + "..." if len(title) > max_w else title 104 | return " {} {} ".format(nr, title) 105 | 106 | def set_text_prop(self): 107 | optsions = {"bufnr": self.bufnr} 108 | vimcmd("call prop_clear(1, 1, {})".format(str(optsions))) 109 | pos = 1 110 | for term in self.manager.term_list: 111 | length = len(term.title) + 4 112 | optsions["length"] = length 113 | optsions["type"] = "fterm_current" if term is self.manager.get_curterm() else "fterm_normal" 114 | vimcmd("call prop_add(1, {}, {})".format(pos, str(optsions))) 115 | pos += length 116 | 117 | def close_popup(self): 118 | vimcmd("call popup_close({})".format(self.winid)) 119 | 120 | def rebuild(self): 121 | self.close_popup() 122 | self.build_line() 123 | self.manager.get_curterm().restore() 124 | 125 | def set_title(self, title): 126 | manager = self.manager 127 | term = manager.get_curterm() 128 | if term is None: 129 | vimsg("Error", "No terminal exists!") 130 | return 131 | if not manager.show: 132 | vimsg("Error", "please toggle terminal first before setting its title") 133 | return 134 | term.title = title 135 | self.rebuild() 136 | 137 | def move_to(self, term_nr: int): 138 | manager = self.manager 139 | term_list = manager.term_list 140 | cur_termnr = manager.cur_termnr 141 | if cur_termnr == -1: 142 | return 143 | term_nr = term_nr % len(term_list) 144 | # term_nr = term_nr if term_nr >=0 else 0 145 | # term_nr = term_nr if term_nr < len(term_list) - 1 else len(term_list) - 1 146 | manager.term_list = change_list(term_list, cur_termnr, term_nr) 147 | manager.cur_termnr = term_nr 148 | self.rebuild() 149 | 150 | def move_left(self, n): 151 | manager = self.manager 152 | cur_termnr = manager.cur_termnr 153 | termnr = cur_termnr - n 154 | self.move_to(termnr) 155 | 156 | def move_right(self, n): 157 | self.move_left(-n) 158 | 159 | def move_end(self): 160 | self.move_to(len(self.manager.term_list) - 1) 161 | -------------------------------------------------------------------------------- /python/fterm/manager.py: -------------------------------------------------------------------------------- 1 | import vim 2 | import argparse 3 | import functools 4 | 5 | from .utils import * 6 | from .terminal import Fterm 7 | from .ftermline import FtermLine 8 | 9 | def internal_wrapper(func): 10 | @functools.wraps(func) 11 | def wrapper(*args, **kwargs): 12 | args[0].inner = True 13 | func(*args, **kwargs) 14 | args[0].inner = False 15 | return wrapper 16 | 17 | 18 | class ExtendAction(argparse.Action): 19 | 20 | def __call__(self, parser, namespace, values, option_string=None): 21 | items = getattr(namespace, self.dest) or [] 22 | items.extend(values) 23 | setattr(namespace, self.dest, items) 24 | 25 | class Manager(object): 26 | def __init__(self): 27 | self.term_list = [] 28 | self.termline = FtermLine(self) 29 | self.cur_termnr = -1 30 | self.show = False 31 | self.last_mode_t = True 32 | self.set_feature() 33 | self.init_parser() 34 | self.inner = False 35 | 36 | def set_feature(self): 37 | self.features = dict() 38 | self.features[''] = vimeval("has('patch-8.2.1978')") == '1' 39 | 40 | def issue(self): 41 | return vimeval("fterm#issue#patch_821990()", 1) 42 | 43 | def init_parser(self): 44 | parser = argparse.ArgumentParser(prog='Fterm') 45 | subparsers = parser.add_subparsers(dest='mode', help='Fterm sub-command help') 46 | default_width = ftget("width", 0.75, 2) 47 | default_height = ftget("height", 0.75, 2) 48 | default_title = ftget("title", "'fterm'") 49 | # new command 50 | parser_new = subparsers.add_parser('new') 51 | parser_new.register('action', 'extend', ExtendAction) 52 | parser_new.add_argument('--cwd', help='cwd of terminal') 53 | parser_new.add_argument('--cmd', nargs="+", type=str, action="extend", 54 | help='run command in new terminal') 55 | parser_new.add_argument('--width', metavar='width', type=float, 56 | default=default_width, help='width of the popup window') 57 | parser_new.add_argument('--height', metavar='height', type=float, 58 | default=default_height, help='height of the popup window') 59 | parser_new.add_argument('--title', metavar='title', type=str, 60 | dest='init_title', default=default_title, 61 | help='set the title of terminal') 62 | # toggle command 63 | parser_toggle = subparsers.add_parser('toggle') 64 | # kill command 65 | parser_kill = subparsers.add_parser('kill') 66 | parser_kill.add_argument('--all', default=False, dest="kill_all", action='store_true', help='kill all terminals') 67 | # select command 68 | parser_select = subparsers.add_parser('select') 69 | parser_select.add_argument('termnr', type=int, metavar='terminal_number', help='open the terminal by terminal number') 70 | # settitle command 71 | parser_set_title = subparsers.add_parser('settitle') 72 | parser_set_title.add_argument('title', metavar='title', type=str, help='set the title of current open terminal') 73 | # move command 74 | parser_move = subparsers.add_parser('move') 75 | parser_move.add_argument('--left', dest='move_left', metavar='N', type=int, help='move current tab to right') 76 | parser_move.add_argument('--right', dest='move_right', metavar='N', type=int, help='move current tab to right') 77 | parser_move.add_argument('--to', dest='move_to', metavar='N', type=int, help='move current tab to specified position') 78 | parser_move.add_argument('--end', default=False, dest="move_end", action='store_true', help='move current tab to end') 79 | # quit command 80 | parser_quit = subparsers.add_parser('quit') 81 | self.parser = parser 82 | 83 | def start(self, arglist): 84 | if self.issue(): 85 | return 86 | try: 87 | args = self.parser.parse_args(arglist) 88 | self.args = args 89 | except SystemExit: 90 | return 91 | try: 92 | mode = self.args.mode 93 | if mode == 'new': 94 | self.create_term() 95 | elif mode == 'toggle': 96 | self.toggle_term() 97 | elif mode == 'kill': 98 | if self.args.kill_all: 99 | self.kill_all_term() 100 | else: 101 | self.kill_single_term() 102 | elif mode == 'select': 103 | self.select_term(self.args.termnr) 104 | elif mode == 'settitle': 105 | self.termline.set_title(self.args.title) 106 | elif mode == 'move': 107 | self.move() 108 | elif mode == 'quit': 109 | self.quit() 110 | except AttributeError: 111 | pass 112 | 113 | def empty(self): 114 | return len(self.term_list) == 0 115 | 116 | def get_curterm(self): 117 | return None if self.empty() else self.term_list[self.cur_termnr] 118 | 119 | @internal_wrapper 120 | def create_term(self): 121 | """ 122 | 1. create from empty 123 | 2. create when fterm is hidden 124 | 3. create when fterm is show 125 | """ 126 | curterm = self.get_curterm() 127 | if self.show: 128 | self.show = False 129 | curterm.close_popup() 130 | term = Fterm(self) 131 | self.cur_termnr += 1 132 | self.term_list.insert(self.cur_termnr, term) 133 | term.create_popup() 134 | self.show = True 135 | 136 | @internal_wrapper 137 | def toggle_term(self): 138 | if self.empty(): 139 | default = ftget("toggle_default", "'FtermNew'") 140 | vimcmd(default) 141 | elif self.show: 142 | self.show = False # this line must be in front of the next line 143 | self.get_curterm().close_popup() 144 | else: 145 | self.get_curterm().create_popup() 146 | self.show = True 147 | 148 | @internal_wrapper 149 | def kill_single_term(self): 150 | if self.empty(): 151 | vimcmd("echom 'there is no terminal to kill'") 152 | return 153 | if self.show: 154 | self.get_curterm().close_popup() 155 | term = self.term_list.pop(self.cur_termnr) 156 | term.kill_term() 157 | if self.empty(): 158 | self.show = False 159 | if self.cur_termnr >= len(self.term_list): # no terminal on the right 160 | self.cur_termnr -= 1 161 | if self.show and not self.empty(): 162 | self.get_curterm().create_popup() 163 | 164 | @internal_wrapper 165 | def quit(self): 166 | if not self.show or self.empty(): 167 | return 168 | term = self.term_list.pop(self.cur_termnr) 169 | self.show = False 170 | term.close_popup() 171 | term.kill_term() 172 | if self.cur_termnr >= len(self.term_list): # no terminal on the right 173 | self.cur_termnr -= 1 174 | 175 | @internal_wrapper 176 | def kill_all_term(self): 177 | if self.empty(): 178 | vimcmd("echom 'there is no terminal to kill'") 179 | return 180 | if self.show: 181 | self.show = False 182 | self.get_curterm().close_popup() 183 | for term in self.term_list: 184 | term.kill_term() 185 | self.cur_termnr = -1 186 | self.term_list.clear() 187 | 188 | @internal_wrapper 189 | def select_term(self, termnr): 190 | if self.cur_termnr == termnr - 1: # do nothing 191 | return 192 | if termnr > len(self.term_list): 193 | vimsg("Error", "invalid argument: {}".format(termnr)) 194 | self.get_curterm().restore() 195 | return 196 | if self.show: 197 | self.get_curterm().close_popup() 198 | else: 199 | self.show = True 200 | self.cur_termnr = termnr - 1 201 | self.get_curterm().create_popup() 202 | 203 | @internal_wrapper 204 | def move(self): 205 | left = self.args.move_left 206 | right = self.args.move_right 207 | to = self.args.move_to 208 | end = self.args.move_end 209 | termline = self.termline 210 | if end: 211 | termline.move_end() 212 | return 213 | if to is not None: 214 | termline.move_to(to - 1) 215 | return 216 | if left is not None: 217 | termline.move_left(left) 218 | return 219 | if right is not None: 220 | termline.move_right(right) 221 | 222 | @internal_wrapper 223 | def edit_in_vim(self, path): 224 | if self.show: 225 | self.toggle_term() 226 | cmd = ftget("open_cmd", "'tabedit'") 227 | vimcmd("{} {}".format(cmd, path)) 228 | 229 | @internal_wrapper 230 | def async_run(self): 231 | cmd = [] 232 | cwd = vimeval("shellescape(getcwd())") 233 | cmd.append('cd {}'.format(cwd)) 234 | cmd.append(vimeval("a:opts.cmd")) 235 | cmd = '; '.join(cmd).replace('"', r'\"') 236 | need_return = False if self.empty() else True 237 | if self.empty(): 238 | vimcmd("FtermNew") 239 | if not self.show: 240 | self.get_curterm().create_popup() 241 | self.show = True 242 | bufnr = self.get_curterm().bufnr 243 | vimcmd(r"""call term_sendkeys({}, "{}\")""".format(bufnr, cmd)) 244 | if need_return: 245 | self.get_curterm().restore() 246 | 247 | def winleave_cb(self): 248 | if not self.inner and self.show: 249 | self.toggle_term() 250 | 251 | 252 | fterm_manager = Manager() 253 | 254 | __all__ = ['fterm_manager'] 255 | -------------------------------------------------------------------------------- /python/fterm/terminal.py: -------------------------------------------------------------------------------- 1 | import vim 2 | import re 3 | 4 | from pathlib import Path 5 | from .utils import * 6 | from .ftermline import FtermLine 7 | 8 | class Fterm(object): 9 | def __init__(self, manager): 10 | self.manager = manager 11 | self.termline = manager.termline 12 | self.args = vars(manager.args) 13 | self.features = manager.features 14 | # self.lastbuf = vimeval("bufnr('%')", 1) 15 | self.last_mode_t = True 16 | self.last_pos = [] 17 | self.set_cwd() 18 | self.set_title() 19 | self.set_exclude() 20 | self.set_geometry() 21 | self.init_term() 22 | 23 | def set_cwd(self): 24 | cwd = self.args["cwd"] 25 | if cwd is None: 26 | self.cwd = get_cwd() 27 | else: 28 | self.cwd = str(Path(cwd).expanduser().resolve()) 29 | 30 | def set_title(self): 31 | title = ftget("title", "'fterm'") 32 | self.title = self.args.get("init_title", title) 33 | 34 | def set_exclude(self): 35 | self.exclude_cmdline = ftget("fterm_exclude_cmdline", 1) == '1' 36 | self.exclude_statusline = vimeval("&laststatus") != '0' and ftget("exclude_statusline", 1) == '1' 37 | self.exclude_tabline = vimeval("&showtabline") != '0' and ftget("exclude_tabline", 1) == '1' 38 | self.exclude_signcolumn = vimeval("&signcolumn") == 'yes' and ftget("exclude_signcolumn", 0) == '1' 39 | 40 | def set_geometry(self): 41 | self.get_max() 42 | self.width = self.get_size(width=True) 43 | self.height = self.get_size(width=False) 44 | self.get_coor() 45 | 46 | def get_max(self): 47 | self.max_w = vimeval("&columns", 1) 48 | self.max_h = vimeval("&lines", 1) 49 | if self.exclude_cmdline: 50 | self.max_h -= vimeval("&cmdheight", 1) 51 | if self.exclude_statusline: 52 | self.max_h -= 1 53 | if self.exclude_tabline: 54 | self.max_h -= 1 55 | if self.exclude_signcolumn: 56 | self.max_w -= 2 57 | 58 | def get_size(self, width=True): 59 | max_n = vimeval("&{}".format("columns" if width else "lines"), 1) 60 | max_n = self.max_w if width else self.max_h 61 | key = 'width' if width else 'height' 62 | n = self.args[key] 63 | n = n if n > 0 else 0.8 # n must > 0 64 | n = int(n) if n > 1 else int(max_n * n) 65 | n = max(n, 10) # at least 10 cols or lines 66 | n = min(n, max_n) # at most max_n 67 | return n - 4 if width else n - 3 # exclude border, padding and temrline 68 | 69 | def get_anchor(self): 70 | """ 71 | Get the coordinates of topleft anchor. 72 | Note that calculation of anchor involves borders while setting the size the popup not. 73 | """ 74 | delta_w = self.max_w - self.width - 4 # border and padding 75 | if delta_w & 1: 76 | self.anchor_col = (delta_w - 1) // 2 + 1 77 | self.width += 1 78 | else: 79 | self.anchor_col = delta_w // 2 + 1 80 | delta_h = self.max_h - self.height - 3 # border and termline 81 | if delta_h & 1: 82 | self.anchor_line = (delta_h - 1) // 2 + 1 83 | self.height += 1 84 | else: 85 | self.anchor_line = delta_h // 2 + 1 86 | if self.exclude_tabline: 87 | self.anchor_line += 1 88 | if self.exclude_signcolumn: 89 | self.anchor_col += 2 90 | 91 | def get_coor(self): 92 | self.get_anchor() 93 | pos = get_termline_pos() 94 | self.col = self.anchor_col + 1 95 | if pos == 'innertop': 96 | self.line = self.anchor_line + 2 97 | if pos == 'outertop': 98 | self.line = self.anchor_line + 1 99 | if pos == 'innerbottom' or pos == 'outerbottom': 100 | self.line = self.anchor_line 101 | 102 | def init_term(self): 103 | cmd = self.get_cmd() 104 | opts = {"hidden": 1, "norestore": 1, "term_finish": "open"} 105 | opts["term_cols"] = self.width 106 | opts["term_rows"] = self.height 107 | opts["cwd"] = self.cwd 108 | opts["term_api"] = "fterm#edit" 109 | self.bufnr = vimeval(r"""term_start("{}", {})""".format(cmd, opts), 1) 110 | if ftget("autoquit", 0) == '1': 111 | vimcmd("call term_setkill({}, 'kill')".format(self.bufnr)) 112 | 113 | def get_cmd(self): 114 | shell = ftget("shell", "&shell") 115 | cmd_parts = self.args["cmd"] 116 | if cmd_parts is None: 117 | cmd = shell 118 | else: 119 | if ftget("expanduser", 1) == '1': 120 | cmd_parts = [expanduser(p) for p in cmd_parts] 121 | cmd = ' '.join(cmd_parts) 122 | return cmd 123 | 124 | def map_quit(self): 125 | cmd = self.get_cmd().replace('"', r'\"') 126 | noquit = ftget("noquit", [r'\v(\w|/)*bash$', r'\v(\w|/)*zsh$', r'\v(\w|/)*ksh$', r'\v(\w|/)*csh$', r'\v(\w|/)*tcsh$']) 127 | map = True 128 | for pattern in noquit: 129 | if vimeval(r"""match("{}", '{}')""".format(cmd, pattern)) != '-1': 130 | map = False 131 | break 132 | if map: 133 | quit = ftget("map_quit", "'q'") 134 | vimcmd(r"tnoremap {} :FtermQuit".format(quit)) 135 | vimcmd(r"noremap {} :FtermQuit".format(quit)) 136 | 137 | def create_popup(self): 138 | vimcmd("call fterm#block_map()") 139 | opts = { 140 | "maxwidth": self.width, 141 | "minwidth": self.width, 142 | "maxheight": self.height, 143 | "minheight": self.height, 144 | "padding": [0, 1, 0, 1], 145 | "zindex": 1000, 146 | "pos": "topleft", 147 | "line": self.line, 148 | "col": self.col, 149 | "scrollbar": 0, 150 | "mapping": 0, 151 | } 152 | opts["borderhighlight"] = ftget("hl_terminal_border", "'fterm_hl_border'") 153 | opts["border"], opts["borderchars"] = get_terminal_border() 154 | opts["highlight"] = ftget("hl_terminal_body", "'fterm_hl_terminal_body'") 155 | self.winid = vimeval("popup_create({}, {})".format(self.bufnr, str(opts)), 1) 156 | self.map_quit() 157 | self.termline.build_line() 158 | self.restore() 159 | 160 | def close_popup(self): 161 | self.record() 162 | vimcmd("call popup_close({})".format(self.winid)) 163 | self.termline.close_popup() 164 | vimcmd("call fterm#restore_map()") 165 | 166 | def kill_term(self): 167 | vimcmd("call fterm#terminal#kill({})".format(self.bufnr)) 168 | 169 | def record(self): 170 | self.last_mode_t = self.features[''] and vimeval("mode()") == 't' 171 | ignore = ftget("restore_curpos", 1) == '0' or self.last_mode_t 172 | self.last_pos = [] if ignore else vimeval("getcurpos()") 173 | 174 | def restore(self): 175 | if ftget("restore_curpos", 1) == '0' or not self.features['']: 176 | if vimeval("mode()") == 'n': 177 | vimcmd("call feedkeys('a')") 178 | return 179 | if not self.last_mode_t and self.last_pos: 180 | vimcmd("call setpos('.', {})".format(self.last_pos)) 181 | -------------------------------------------------------------------------------- /python/fterm/utils.py: -------------------------------------------------------------------------------- 1 | import vim 2 | 3 | from functools import partial 4 | from pathlib import Path 5 | 6 | vimcmd = vim.command 7 | vimeval = vim.eval 8 | 9 | class InvalidPos(Exception): 10 | def __init__(self, pos): 11 | self.pos = pos 12 | 13 | def __str__(self): 14 | return "invalid pos: {}".format(self.pos) 15 | 16 | def vimeval(cmd, to_int=0): 17 | """ 18 | :to_int: 19 | 0 for original 20 | 1 for int 21 | 2 for float 22 | """ 23 | r = vim.eval(cmd) 24 | if to_int == 0: 25 | return r 26 | if to_int == 1: 27 | return int(r) 28 | return float(r) 29 | 30 | def vimget(namespace, prefix, var, default, eval_mode=0): 31 | return vimeval("get({}, '{}_{}', {})".format(namespace, prefix, var, default), eval_mode) 32 | 33 | ftget = partial(vimget, 'g:', 'fterm') 34 | 35 | def vim_win_setlocal(winid, cmd): 36 | vimcmd("call win_execute({}, 'setlocal {}')".format(winid, cmd)) 37 | 38 | def expanduser(path): 39 | p = Path(path).expanduser().resolve() 40 | return str(p) if p.exists() else path 41 | 42 | def get_cwd(): 43 | cwd = vimeval("fnamemodify(resolve(expand('%:p')), ':p:h')") 44 | use_root = ftget("use_root", 1) == '1' 45 | if not use_root: 46 | return cwd 47 | is_root = False 48 | root_marker = ftget("root_marker", ['.root', '.git', '.svn', '.hg', '.project']) 49 | level = ftget("root_search_level", 5, 1) 50 | cur = Path(cwd) 51 | n = 1 52 | while n <= level and cur != cur.parent: 53 | if n > 1: 54 | cur = cur.parent 55 | for marker in root_marker: 56 | f = cur / marker 57 | if not f.is_symlink() and f.exists(): 58 | is_root = True 59 | break 60 | if is_root: 61 | break 62 | n += 1 63 | return str(cur) if is_root and cur != Path.home() else cwd 64 | 65 | def vimsg(type, msg): 66 | vimcmd(r"""echohl {} | echom "{}" | echohl None""".format(type, msg)) 67 | 68 | def get_termline_pos(): 69 | pos = ['innertop', 'outertop', 'innerbottom', 'outerbottom'] 70 | termline_pos = ftget("termline_pos", "'innertop'") 71 | if termline_pos in pos: 72 | return termline_pos 73 | else: 74 | vimsg('WarningMsg', "invalid pos: '{}', using 'innertop' instead".format(termline_pos)) 75 | return 'innertop' 76 | 77 | def get_borderchars() -> list: 78 | default_borderchars = ['─', '│', '─', '│', '┌', '┐', '┘', '└'] 79 | return ftget("borderchars", default_borderchars) 80 | 81 | def get_terminal_border(): 82 | pos = get_termline_pos() 83 | border = [1, 1, 1, 1] 84 | borderchars = get_borderchars() 85 | if pos == 'innertop': 86 | border[0] = 0 87 | borderchars[0] = '' 88 | borderchars[4] = borderchars[3] 89 | borderchars[5] = borderchars[1] 90 | if pos == 'innerbottom': 91 | border[2] = 0 92 | borderchars[2] = '' 93 | borderchars[6] = borderchars[1] 94 | borderchars[7] = borderchars[3] 95 | return border, borderchars 96 | 97 | def get_termline_border(): 98 | pos = get_termline_pos() 99 | border = [1, 1, 1, 1] 100 | borderchars = get_borderchars() 101 | padding = [0, 0, 0, 0] 102 | if pos == 'outertop' or pos == 'outerbottom': 103 | border = [0, 0, 0, 0] 104 | padding = [0, 1, 0, 1] 105 | if pos == 'innertop': 106 | border[2] = 0 107 | borderchars[2] = '' 108 | borderchars[6] = borderchars[1] 109 | borderchars[7] = borderchars[3] 110 | if pos == 'innerbottom': 111 | border[0] = 0 112 | borderchars[0] = '' 113 | borderchars[4] = borderchars[3] 114 | borderchars[5] = borderchars[1] 115 | 116 | return border, borderchars, padding 117 | 118 | def change_list(list, old, to): 119 | """ 120 | list = [0, 1, 2, 3, 4] 121 | 1. old = 1, to = 3, change to [0, 2, 3, 1, 4] 122 | 1. old = 3, to = 1, change to [0, 3, 1, 2, 4] 123 | """ 124 | to = to % len(list) 125 | if to == old: 126 | return list 127 | # to = 0 if to < 0 else to 128 | # to = to if to < len(list) else len(list) - 1 129 | if old < to: 130 | a = list[0:old] 131 | b = list[old + 1:to + 1] 132 | if to < len(list) - 1: 133 | c = list[to + 1 - len(list):] 134 | else: 135 | c = [] 136 | x = list[old] 137 | return [*a, *b, x, *c] 138 | else: 139 | a = list[0:to] 140 | b = list[to:old] 141 | if old < len(list) - 1: 142 | c = list[old + 1:] 143 | else: 144 | c = [] 145 | x = list[old] 146 | return [*a, x, *b, *c] 147 | --------------------------------------------------------------------------------