├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── Makefile
├── README.md
├── build.rs
├── com.github.Lyude.neovim-gtk.yaml
├── desktop
├── com.github.Lyude.neovim-gtk-symbolic.svg
├── com.github.Lyude.neovim-gtk.desktop
├── com.github.Lyude.neovim-gtk.metainfo.xml
├── com.github.Lyude.neovim-gtk.svg
├── com.github.Lyude.neovim-gtk_128.png
├── com.github.Lyude.neovim-gtk_48.png
└── dejavu_font
│ ├── DejaVu Fonts License.txt
│ ├── DejaVuSansMono-Bold.ttf
│ ├── DejaVuSansMono-BoldOblique.ttf
│ ├── DejaVuSansMono-Oblique.ttf
│ └── DejaVuSansMono.ttf
├── resources
└── neovim.ico
├── runtime
└── plugin
│ └── nvim_gui_shim.vim
├── rustfmt.toml
├── screenshots
└── neovimgtk-screen.png
├── src
├── cmd_line
│ ├── mod.rs
│ └── viewport.rs
├── color.rs
├── cursor.rs
├── dirs.rs
├── error.rs
├── file_browser
│ ├── mod.rs
│ └── tree_view.rs
├── grid.rs
├── highlight.rs
├── input.rs
├── main.rs
├── misc.rs
├── mode.rs
├── nvim
│ ├── client.rs
│ ├── ext.rs
│ ├── handler.rs
│ ├── mod.rs
│ └── redraw_handler.rs
├── nvim_config.rs
├── nvim_viewport.rs
├── plug_manager
│ ├── manager.rs
│ ├── mod.rs
│ ├── plugin_settings_dlg.rs
│ ├── store.rs
│ ├── ui.rs
│ ├── vim_plug.rs
│ └── vimawesome.rs
├── popup_menu
│ ├── list_row.rs
│ ├── mod.rs
│ ├── popover.rs
│ └── popupmenu_model.rs
├── project.rs
├── render
│ ├── context.rs
│ ├── itemize.rs
│ └── mod.rs
├── settings.rs
├── shell.rs
├── shell_dlg.rs
├── style.css
├── subscriptions.rs
├── tabline.rs
├── ui.rs
├── ui_model
│ ├── cell.rs
│ ├── item.rs
│ ├── line.rs
│ ├── mod.rs
│ ├── model_layout.rs
│ └── model_rect.rs
└── value.rs
└── tests
├── cli_tests.rs
├── cmd
└── arg_parsing.trycmd
└── cmd_unix
└── unix_pipes.trycmd
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | Description of what the bug is.
9 |
10 | **Technical information (please complete the following information):**
11 | - OS: [e.g. Windows, Linux Ubuntu]
12 | - Neovim version: [e.g. .0.0.3]
13 | - Neovim-Gtk build version: [e.g. flatpak, commit hash]
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ "main", "*stable", "wip/*" ]
6 | pull_request:
7 | branches: [ "main", "*stable", "wip/*" ]
8 | workflow_dispatch:
9 | branches: [ "main", "*stable", "wip/*" ]
10 |
11 | env:
12 | CARGO_TERM_COLOR: always
13 | RUST_BACKTRACE: 1
14 | NVIM_GTK_LOG_FILE: nvim-gtk-stdout.log
15 | NVIM_GTK_LOG_LEVEL: debug
16 | NVIM_GTK_STDERR: nvim-gtk-stderr.log
17 | # Brew update breaks otherwise
18 | HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
19 | RUSTFLAGS: -C opt-level=0 -D warnings
20 |
21 | jobs:
22 | fedora:
23 | runs-on: [ubuntu-latest]
24 | container:
25 | image: fedora:40
26 |
27 | strategy:
28 | fail-fast: false
29 | matrix:
30 | os: [ubuntu-latest]
31 | include:
32 | - os: ubuntu-latest
33 |
34 | steps:
35 | - uses: actions/checkout@v3
36 |
37 | - uses: Lyude/setup-dnf@main
38 | with:
39 | update: true
40 | install: >-
41 | rust
42 | cargo
43 | rustfmt
44 | clippy
45 | atk-devel
46 | glib2-devel
47 | pango-devel
48 | gtk4-devel
49 |
50 | - uses: Swatinem/rust-cache@v2
51 |
52 | - name: Run cargo test
53 | run: cargo test --locked
54 |
55 | - name: Check that rust-fmt is happy
56 | run: cargo fmt --check -v
57 |
58 | - name: Check for clippy lints
59 | run: cargo clippy -- -Dwarnings
60 |
61 | osx:
62 | runs-on: [macos-latest]
63 |
64 | strategy:
65 | fail-fast: false
66 | matrix:
67 | os: [macos-latest]
68 | rust: [stable]
69 | include:
70 | - os: macos-latest
71 |
72 | steps:
73 | - uses: actions/checkout@v3
74 |
75 | - name: Update brew
76 | run: brew update
77 |
78 | - name: Install dependencies
79 | run: brew install gtk4 pkg-config
80 |
81 | - uses: Swatinem/rust-cache@v2
82 |
83 | - name: Run cargo test
84 | run: cargo test --locked
85 |
86 | windows:
87 | runs-on: [windows-latest]
88 |
89 | strategy:
90 | fail-fast: false
91 | matrix:
92 | os: [windows-latest]
93 | include:
94 | - os: windows-latest
95 | defaults:
96 | run:
97 | shell: msys2 {0}
98 |
99 | steps:
100 | - uses: actions/checkout@v3
101 |
102 | - uses: msys2/setup-msys2@v2
103 | with:
104 | update: true
105 | install: >-
106 | mingw-w64-x86_64-gtk4
107 | mingw-w64-x86_64-binutils
108 | mingw-w64-x86_64-gcc
109 | mingw-w64-x86_64-rust
110 | mingw-w64-x86_64-atk
111 | mingw-w64-x86_64-pango
112 | mingw-w64-x86_64-glib2
113 | mingw-w64-x86_64-pkg-config
114 |
115 | - uses: Swatinem/rust-cache@v2
116 |
117 | - name: Run cargo test
118 | run: cargo test --locked
119 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | *.swp
3 |
4 | # Some build output from the CI's container caching I believe…
5 | container-images.txt
6 | container-images-key.txt
7 | container-images-uris.txt
8 | container-images.txt
9 | cached-container-images.tar
10 |
11 | # Flatpak
12 | .flatpak-builder
13 |
14 | # vim: tw=100 colorcolumn=100
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v1.0.3
2 |
3 | ## Bugs fixed:
4 |
5 | * Cargo.lock was out of sync, oops 😳. Will look into setting up a release workflow to prevent this
6 | from happening in the future. (fixes #58)
7 |
8 | ---
9 |
10 | # v1.0.2
11 |
12 | New stable release, bugs fixed:
13 |
14 | * OSX (thanks @jacobmischka)
15 | * #46 - Workaround transparent windows bug in GTK4
16 | * Disable default shortcuts
17 | * All
18 | * Fix appearance of error window that displays when initialization fails, which regressed since
19 | GTK4
20 |
21 | ## Additional thanks to
22 |
23 | * @jacobmischka
24 |
25 | ---
26 |
27 | # v1.0.1
28 |
29 | * Update dependencies
30 | * Drop some leftover dead cairo code I missed, and our explicit cairo dependency
31 | * Some bugfixes:
32 | * Fix funny window sizing issue with long file names
33 | (https://github.com/Lyude/neovim-gtk/issues/41)
34 | * Fix scrolling speed on devices like touchpads (https://github.com/Lyude/neovim-gtk/pull/40)
35 | * Aesthetic improvements to the new underline style (underline should no longer look like it's
36 | moving under the text cursor)
37 | * Fix issue with underlines disappearing under the text cursor if no pango item was below the
38 | cursor
39 |
40 | ## Additional thanks to
41 |
42 | * @jadahl
43 |
44 | ---
45 |
46 | # v1.0.0
47 |
48 | We're finally ready to move to GTK4! There's a number of other nice changes that come with this:
49 |
50 | - We now use a `gtk::Snapshot` based renderer instead of cairo, and introduce a new `NvimViewport`
51 | widget that implements the new renderer
52 | - We also convert the `ext_cmdline` over to using the new renderer
53 | - The `ext_popupmenu` popover now uses a `GtkListView` instead of a `GtkTreeView`
54 | - We actually make use of nvim's `flush` event now for screen redraws, which probably should have
55 | been done from the start. Supporting this means we're dramatically less likely to display screen
56 | updates to the user before we've finished parsing a full batch of GUI events from nvim
57 | - We also use the `flush` event for popup menu updates, in addition to flattening all of the which
58 | replaces the previous hacks that were in place to prevent the user from seeing intermediate
59 | `popup_menu` events. This also allows us to avoid having to use a timed delay for displaying the
60 | popup menu, which makes things a bit faster :)
61 | - Long taps from touchscreens should register as right clicks now
62 |
63 | ## Additional thanks to
64 |
65 | - @baco
66 | - @jadahl
67 | - Company and the other folks in `#gtk` who helped a ton with answering questions
68 |
69 | ---
70 |
71 | # v0.4.1
72 |
73 | ## Bug fixes
74 |
75 | - Revert default background type back to dark (#21)
76 |
77 | ---
78 |
79 | # v0.4.0
80 |
81 | Note: this is the first version being maintained by Lyude, and as a result I didn't make a thorough
82 | attempt at coming up with a changelog for history that came before me maintaining things (besides
83 | things that were already written in the changelog by @daa84). Therefore, this changelog may be
84 | incomplete. I've also decided to skip v0.3.0 and go directly to v0.4.0, to indicate the difference
85 | in maintenance since things were stuck on v0.3.0 for so long. Future version bumps won't skip
86 | numbers like this.
87 |
88 | ## Enhancements
89 |
90 | - Migration to new ext_linegrid api [ui-linegrid](https://neovim.io/doc/user/ui.html#ui-linegrid)
91 | - New option --cterm-colors [#190](https://github.com/daa84/neovim-gtk/issues/190)
92 | - Migrate to using nvim-rs instead of neovim-lib, this allows us to use async code and handle
93 | blocking operations far more easily.
94 | - Resize requests are sent immediately vs. intervallically, resulting in much smoother resizing
95 | - We now print RPC request errors as normal neovim error messages
96 | - Closing neovim-gtk is now inhibited during a blocking prompt
97 | - UI elements are now disabled when opening files via the command line, through one of the GUI
98 | elements, or while neovim-gtk is initializing. This prevents potential RPC request timeouts.
99 | - Don't change nvim directory when changing file browser directory, this behavior wasn't immediately
100 | obvious and was more confusing then useful.
101 | - Added support for standout highlighting
102 | - Started populating most of the client info for neovim
103 | - Implemented working maps of some neovim arguments which typically hang the GUI if passed directly
104 | to neovim via `neovim-gtk -- --foo=bar`, including:
105 | - `-c` (execute command after opening files)
106 | - `-d` (diff-mode)
107 | - Start using `nvim_input_mouse()`
108 | - Update GTK+ version to 3.26
109 | - Update crates
110 | - Preliminary work for [GTK+4 support](#8):
111 | - Use `PopoverMenu`s instead of `GtkMenu`s
112 | - Start using `PopoverMenu` and `Action`s for the file browser
113 | - Use `Action`s for building the context menu for the drawing area
114 | - Use a `MenuButton` for the Open button rather than a `Button`
115 | - Use CSS margins instead of `border_width()` where possible
116 | - Stop using `size_allocate` events where we can help it
117 | - Various misc. refactoring
118 | - Use the special color for rendering underline
119 | - Add support for the `:cq` command (#15, @bk2204)
120 | - Improve algorithm for determining popup menu column sizes
121 | - Update GTK+ tabling visibility on tabline option changes
122 | - Make info in the completion popup scrollable
123 |
124 | ## Bug fixes
125 |
126 | - `VimLeavePre` and `VimLeave` are now correctly executed upon exiting
127 | - `E365 ("File already opened in another process")` prompts no longer hang when opening files via
128 | the command line
129 | - The runtime path for our various vim scripts is now correctly set when using `cargo run`
130 | - Resizing while neovim is stuck on a blocking prompt no longer hangs
131 | - Focus changes while neovim is stuck on a blocking prompt no longer hang
132 | - Use the special color for rendering underlines and undercurls when it's explicitly set, otherwise
133 | fallback to the foreground color (except for undercurls, where we default to red in this case).
134 | (#10)
135 | - Fix issues with various unicode graphemes being misaligned when rendered using certain fonts (#7,
136 | #5, @medicalwei)
137 | - Fix crashes and most rendering issues when rendering combining glyphs that require a fallback font
138 | to be used
139 | - Round up background cell width (#1, @jacobmischka)
140 | - Silently ignore the blend attribute for highlights, at least until we've added support for it
141 | (#17, @bk2204).
142 | - Don't use predictably named temporary files (#20, @bk2204)
143 | - Fix undercurl rendering with certain fonts (#11)
144 | - Stop completion popups from changing colors changing when they shouldn't be
145 | - Fix GTK+ tabline visibility issues when trying to disable the external tabline
146 | - Fix undercurl rendering for double width graphemes under the cursor
147 | - Fix coloring with respect to the `background` option in neovim when either one or both of the
148 | foreground and background colors are missing.
149 |
150 | ## Special thanks to those who contributed patches this release
151 |
152 | - @medicalwei
153 | - @bk2204
154 | - @jacobmischka
155 |
156 |
158 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | This is a short write-up on the preferred contribution style of this project! Being a rather small
2 | project, we have pretty simple guidelines. Before we go into that though, we must first emphasize
3 | one thing:
4 |
5 | **Don't be afraid!**
6 |
7 | Being a new contributor to a project can be intimidating, and you may feel like you're "not ready
8 | yet" to contribute. But that's OK, the only way to know the answer to that is to give it your best
9 | shot. It's nearly always more valuable from a maintainer's perspective to teach someone how to
10 | contribute to your project then it is to turn them down.
11 |
12 | Now onto the style guidelines:
13 |
14 | * Listen to rustfmt mostly, with the following exceptions:
15 | * Sometimes, particularly when dealing with lots of rendering coordinates, it may be beneficial to
16 | slightly deviate from rustfmt and instead format things by hand. An example of this can be found
17 | in src/cursor.rs:
18 |
19 | ```rust
20 | // …
21 | if state.anim_phase == AnimPhase::NoFocus {
22 | #[rustfmt::skip]
23 | {
24 | let bg = hl.cursor_bg().to_rgbo(filled_alpha);
25 | snapshot.append_color(&bg, &Rect::new( x, y, w, 1.0));
26 | snapshot.append_color(&bg, &Rect::new( x, y, 1.0, h));
27 | snapshot.append_color(&bg, &Rect::new( x, y + h - 1.0, w, 1.0));
28 | snapshot.append_color(&bg, &Rect::new(x + w - 1.0, y, 1.0, h));
29 | };
30 | false
31 | } else {
32 | // …
33 | ```
34 |
35 | There's a lot of variables and arithematic happening here, so it's much easier to read if we
36 | prevent rustfmt from mangling things. Large tables of static data very often fall into this
37 | category.
38 | * `gtk::*Builder` usage patterns. We use these all over the place, and the majority of the time
39 | rustfmt is going to split the method chains here onto separate lines. As a result, single-line
40 | invocations of Builder types tend to look out of place and are less visually intuitive. So, feel
41 | free to have rustfmt skip these whenever it tries combining these method chains onto a single
42 | line. If it's the only Builder invocation in it's scope and it's really, seriously short though,
43 | feel free to use your best judgement.
44 |
45 | * They're guidelines: use your best judgement and don't be afraid of making the wrong decision, if a
46 | piece of code seems like it'd be much more legible without rustfmt mangling it - feel free to
47 | throw a `[rustfmt::skip]` onto it. If a maintainer disagrees, they'll just let you know and the
48 | worst thing you'll have to do is change it ♥.
49 |
50 | vim: tw=100 ts=2 sts=2 sw=2 expandtab
51 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nvim-gtk"
3 | version = "1.1.0-devel"
4 | authors = [
5 | "daa84 ",
6 | "Lyude Paul ",
7 | ]
8 | build = "build.rs"
9 | edition = "2018"
10 | license = "GPLv3"
11 |
12 | [features]
13 | default = []
14 | flatpak = []
15 |
16 | [dependencies]
17 | clap = { version = "4.0", features = ["color", "help", "std", "usage", "error-context", "suggestions", "wrap_help", "derive"] }
18 | glib = "0.20.0"
19 | gio = "0.20.0"
20 | async-trait = "0.1.0"
21 | futures = { version = "0.3.0", features = ["io-compat", "thread-pool"] }
22 | tokio = { version = "1.0", features = ["full"] }
23 | tokio-util = { version = "0.7.0", features = ["full"] }
24 | nvim-rs = { version = "0.9.0", features = ["use_tokio"] }
25 | phf = "0.11.0"
26 | log = "0.4.0"
27 | env_logger = "0.11.0"
28 | html-escape = "0.2.0"
29 | rmpv = { version = "1.0", features = ["with-serde"] }
30 | percent-encoding = "2.0"
31 | regex = "1.0"
32 | unicode-width = "0.2"
33 | unicode-segmentation = "1.0"
34 | fnv = "1.0"
35 | once_cell = "1.0"
36 |
37 | serde = { version = "1.0", features = ["derive"] }
38 | toml = "0.8.0"
39 | serde_json = "1.0"
40 |
41 | is-terminal = "0.4.0"
42 |
43 | [target.'cfg(unix)'.dependencies]
44 | fork = "0.2.0"
45 |
46 | [target.'cfg(windows)'.build-dependencies]
47 | winres = "0.1.0"
48 |
49 | [build-dependencies]
50 | phf_codegen = "0.11.0"
51 | build-version = "0.1.0"
52 |
53 | [dev-dependencies]
54 | trycmd = "0.15.0"
55 |
56 | [dependencies.pango]
57 | features = ["v1_46"]
58 | version = "0.20.0"
59 |
60 | [dependencies.gtk]
61 | package = "gtk4"
62 | version = "0.9.0"
63 | features = ["v4_4"]
64 |
65 | [dependencies.gdk]
66 | package = "gdk4"
67 | version = "0.9.0"
68 | features = ["v4_4"]
69 |
70 | [dependencies.gsk]
71 | package = "gsk4"
72 | version = "0.9.0"
73 | features = ["v4_4"]
74 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PREFIX?=/usr/local
2 |
3 | test:
4 | RUST_BACKTRACE=1 cargo test
5 |
6 | run:
7 | RUST_LOG=warn RUST_BACKTRACE=1 cargo run $(CARGO_ARGS) -- --no-fork
8 |
9 | install: install-resources
10 | cargo install $(CARGO_ARGS) --path . --force --root $(DESTDIR)$(PREFIX)
11 |
12 | install-flatpak: install
13 | mkdir -p /app/share/metainfo/
14 | cp desktop/com.github.Lyude.neovim-gtk.metainfo.xml /app/share/metainfo/
15 |
16 | install-debug: install-resources
17 | cargo install $(CARGO_ARGS) --debug --path . --force --root $(DESTDIR)$(PREFIX)
18 |
19 | install-resources:
20 | mkdir -p $(DESTDIR)$(PREFIX)/share/nvim-gtk/
21 | cp -r runtime $(DESTDIR)$(PREFIX)/share/nvim-gtk/
22 | mkdir -p $(DESTDIR)$(PREFIX)/share/applications/
23 | sed -e "s|Exec=nvim-gtk|Exec=$(PREFIX)/bin/nvim-gtk|" \
24 | desktop/com.github.Lyude.neovim-gtk.desktop \
25 | >$(DESTDIR)$(PREFIX)/share/applications/com.github.Lyude.neovim-gtk.desktop
26 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/
27 | cp desktop/com.github.Lyude.neovim-gtk_128.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/com.github.Lyude.neovim-gtk.png
28 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/
29 | cp desktop/com.github.Lyude.neovim-gtk_48.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/com.github.Lyude.neovim-gtk.png
30 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/
31 | cp desktop/com.github.Lyude.neovim-gtk.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/
32 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/
33 | cp desktop/com.github.Lyude.neovim-gtk-symbolic.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/
34 |
35 | uninstall:
36 | rm $(DESTDIR)$(PREFIX)/bin/nvim-gtk
37 | rm -r $(DESTDIR)$(PREFIX)/share/nvim-gtk/
38 | rm $(DESTDIR)$(PREFIX)/share/applications/com.github.Lyude.neovim-gtk.desktop
39 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/com.github.Lyude.neovim-gtk.png
40 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/com.github.Lyude.neovim-gtk.png
41 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/desktop/com.github.Lyude.neovim-gtk.svg
42 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/desktop/com.github.Lyude.neovim-gtk-symbolic.svg
43 |
44 | .PHONY: all clean test uninstall
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # neovim-gtk
2 |
3 | [](https://github.com/Lyude/neovim-gtk/actions/workflows/ci.yml)
4 |
5 | GTK ui for neovim written in rust using gtk-rs bindings. With
6 | [ligatures](https://github.com/daa84/neovim-gtk/wiki/Configuration#ligatures) support. This project
7 | began as a fork of @daa84's neovim-gtk.
8 |
9 | There are a very large number of improvements from @daa84's version, including:
10 |
11 | * Lots of bugfixes
12 | * We're fully ported to GTK4, and have a Snapshot based renderer instead of a cairo based renderer
13 | * _Smooth_ resizing
14 |
15 | Note that I haven't set up the wiki pages for this repo yet, so wiki links still go to daa84's wiki
16 | repo.
17 |
18 | # Screenshot
19 | 
20 |
21 | For more screenshots and description of basic usage see [wiki](https://github.com/daa84/neovim-gtk/wiki/GUI)
22 |
23 | # Configuration
24 | To setup font add next line to `ginit.vim`
25 | ```vim
26 | call rpcnotify(1, 'Gui', 'Font', 'DejaVu Sans Mono 12')
27 | ```
28 | for more details see [wiki](https://github.com/daa84/neovim-gtk/wiki/Configuration)
29 |
30 | # Install
31 | ## From sources
32 | First check [build prerequisites](#build-prerequisites)
33 |
34 | By default to `/usr/local`:
35 | ```
36 | make install
37 | ```
38 | Or to some custom path:
39 | ```
40 | make PREFIX=/some/custom/path install
41 | ```
42 |
43 | ## Fedora
44 | TODO
45 | ## Arch Linux
46 | TODO
47 | ## openSUSE
48 | TODO
49 | ## Windows
50 | TODO
51 |
52 | # Build prerequisites
53 | ## Linux
54 | First install the GTK development packages. On Debian/Ubuntu derivatives
55 | this can be done as follows:
56 | ``` shell
57 | apt install libgtk-4-dev
58 | ```
59 |
60 | On Fedora:
61 | ```bash
62 | dnf install atk-devel glib2-devel pango-devel gtk4-devel
63 | ```
64 |
65 | Then install the latest rust compiler, best with the
66 | [rustup tool](https://rustup.rs/). The build command:
67 | ```
68 | cargo build --release
69 | ```
70 |
71 | As of writing this (Dec 16, 2022) the packaged rust tools in Fedora also work for building.
72 |
73 | ## Windows
74 | Neovim-gtk can be compiled using MSYS2 GTK packages. In this case use 'windows-gnu' rust toolchain.
75 | ```
76 | SET PKG_CONFIG_PATH=C:\msys64\mingw64\lib\pkgconfig
77 | cargo build --release
78 | ```
79 |
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::fs::File;
3 | use std::io::{BufWriter, Write};
4 | use std::path::{Path, PathBuf};
5 | use std::process::Command;
6 |
7 | fn main() {
8 | let out_dir = &env::var("OUT_DIR").unwrap();
9 |
10 | build_version::write_version_file().expect("Failed to write version.rs file");
11 |
12 | if cfg!(target_os = "windows") {
13 | println!("cargo:rustc-link-search=native=C:\\msys64\\mingw64\\lib");
14 |
15 | set_win_icon();
16 | }
17 |
18 | let path = Path::new(out_dir).join("key_map_table.rs");
19 | let mut file = BufWriter::new(File::create(path).unwrap());
20 |
21 | writeln!(
22 | &mut file,
23 | "static KEYVAL_MAP: phf::Map<&'static str, &'static str> = \n{};\n",
24 | phf_codegen::Map::new()
25 | .entry("F1", "\"F1\"")
26 | .entry("F2", "\"F2\"")
27 | .entry("F3", "\"F3\"")
28 | .entry("F4", "\"F4\"")
29 | .entry("F5", "\"F5\"")
30 | .entry("F6", "\"F6\"")
31 | .entry("F7", "\"F7\"")
32 | .entry("F8", "\"F8\"")
33 | .entry("F9", "\"F9\"")
34 | .entry("F10", "\"F10\"")
35 | .entry("F11", "\"F11\"")
36 | .entry("F12", "\"F12\"")
37 | .entry("Left", "\"Left\"")
38 | .entry("Right", "\"Right\"")
39 | .entry("Up", "\"Up\"")
40 | .entry("Down", "\"Down\"")
41 | .entry("Home", "\"Home\"")
42 | .entry("End", "\"End\"")
43 | .entry("BackSpace", "\"BS\"")
44 | .entry("Return", "\"CR\"")
45 | .entry("Escape", "\"Esc\"")
46 | .entry("Delete", "\"Del\"")
47 | .entry("Insert", "\"Insert\"")
48 | .entry("Page_Up", "\"PageUp\"")
49 | .entry("Page_Down", "\"PageDown\"")
50 | .entry("Enter", "\"CR\"")
51 | .entry("Tab", "\"Tab\"")
52 | .entry("ISO_Left_Tab", "\"Tab\"")
53 | .build()
54 | )
55 | .unwrap();
56 |
57 | println!(
58 | "cargo:rustc-env=RUNTIME_PATH={}",
59 | if let Some(prefix) = option_env!("PREFIX") {
60 | PathBuf::from(prefix).join("share/nvim-gtk/runtime")
61 | } else {
62 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("runtime")
63 | }
64 | .to_str()
65 | .unwrap()
66 | );
67 |
68 | if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
69 | println!(
70 | "cargo:rustc-env=GIT_COMMIT={}",
71 | String::from_utf8(output.stdout).unwrap()
72 | );
73 | }
74 | }
75 |
76 | #[cfg(windows)]
77 | fn set_win_icon() {
78 | let mut res = winres::WindowsResource::new();
79 | res.set_icon("resources/neovim.ico");
80 | if let Err(err) = res.compile() {
81 | eprintln!("Error set icon: {}", err);
82 | }
83 | }
84 |
85 | #[cfg(unix)]
86 | fn set_win_icon() {}
87 |
--------------------------------------------------------------------------------
/com.github.Lyude.neovim-gtk.yaml:
--------------------------------------------------------------------------------
1 | app-id: com.github.Lyude.neovim-gtk
2 | runtime: org.gnome.Platform
3 | runtime-version: '45'
4 | sdk: org.gnome.Sdk
5 | sdk-extensions:
6 | - org.freedesktop.Sdk.Extension.rust-stable
7 | command: nvim-gtk
8 | finish-args:
9 | - --share=ipc
10 | - --socket=fallback-x11
11 | - --socket=wayland
12 | - --device=dri
13 | - --socket=session-bus # for `flatpak-spawn --host nvim`
14 | build-options:
15 | append-path: "/usr/lib/sdk/rust-stable/bin"
16 | build-args:
17 | - "--share=network" # for cargo fetching dependencies
18 | env:
19 | CARGO_HOME: "/run/build/neovim-gtk" # for caching
20 | CARGO_ARGS: "--features flatpak"
21 | PREFIX: "/app"
22 | modules:
23 | - name: neovim-gtk
24 | buildsystem: simple
25 | build-commands:
26 | - make install-flatpak
27 | sources:
28 | - type: archive
29 | #url: https://github.com/Lyude/neovim-gtk/archive/refs/tags/v1.0.4.tar.gz
30 | #sha256: d0d0dacfbfca16168361f517dee20259785379910173cc33d7d48bd301d30f18
31 | url: https://github.com/Lyude/neovim-gtk/archive/3739f961d28d2a7c98b1fd8be912fc4bb9d9d216.tar.gz
32 | sha256: 78f0a12bdbf5d085fdbc0a57d877b695c5ae4873d036dd5e190141a927da2819
33 |
--------------------------------------------------------------------------------
/desktop/com.github.Lyude.neovim-gtk-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
207 |
--------------------------------------------------------------------------------
/desktop/com.github.Lyude.neovim-gtk.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=NeovimGtk
3 | Comment=Gtk GUI for Neovim text editor
4 | Exec=nvim-gtk -- %F
5 | Icon=com.github.Lyude.neovim-gtk
6 | Type=Application
7 | Terminal=false
8 | Categories=GTK;Utility;TextEditor;
9 | StartupNotify=true
10 | MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
11 |
--------------------------------------------------------------------------------
/desktop/com.github.Lyude.neovim-gtk.metainfo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | com.github.Lyude.neovim-gtk
5 | neovim-gtk
6 | GTK UI for neovim written in rust using gtk-rs bindings
7 | Lyude Paul, @daa84
8 |
9 | GTK ui for neovim written in rust using gtk-rs bindings. With ligatures support. This project began as a fork of @daa84's neovim-gtk.
10 | There are a very large number of improvements from @daa84's version, including:
11 |
12 | - Lots of bugfixes
13 | - We're fully ported to GTK4, and have a Snapshot based renderer instead of a cairo based renderer
14 | - Smooth resizing
15 |
16 |
17 |
18 | GPL-3.0
19 |
20 | neovim
21 | vim
22 | editor
23 |
24 |
25 | network
26 | web
27 |
28 | https://github.com/Lyude/neovim-gtk
29 | https://github.com/Lyude/neovim-gtk/issues/new
30 |
31 |
32 | https://raw.githubusercontent.com/Lyude/neovim-gtk/main/screenshots/neovimgtk-screen.png
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/desktop/com.github.Lyude.neovim-gtk.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
162 |
--------------------------------------------------------------------------------
/desktop/com.github.Lyude.neovim-gtk_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/com.github.Lyude.neovim-gtk_128.png
--------------------------------------------------------------------------------
/desktop/com.github.Lyude.neovim-gtk_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/com.github.Lyude.neovim-gtk_48.png
--------------------------------------------------------------------------------
/desktop/dejavu_font/DejaVu Fonts License.txt:
--------------------------------------------------------------------------------
1 | Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
2 | Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
3 |
4 | Bitstream Vera Fonts Copyright
5 | ------------------------------
6 |
7 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
8 | a trademark of Bitstream, Inc.
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy
11 | of the fonts accompanying this license ("Fonts") and associated
12 | documentation files (the "Font Software"), to reproduce and distribute the
13 | Font Software, including without limitation the rights to use, copy, merge,
14 | publish, distribute, and/or sell copies of the Font Software, and to permit
15 | persons to whom the Font Software is furnished to do so, subject to the
16 | following conditions:
17 |
18 | The above copyright and trademark notices and this permission notice shall
19 | be included in all copies of one or more of the Font Software typefaces.
20 |
21 | The Font Software may be modified, altered, or added to, and in particular
22 | the designs of glyphs or characters in the Fonts may be modified and
23 | additional glyphs or characters may be added to the Fonts, only if the fonts
24 | are renamed to names not containing either the words "Bitstream" or the word
25 | "Vera".
26 |
27 | This License becomes null and void to the extent applicable to Fonts or Font
28 | Software that has been modified and is distributed under the "Bitstream
29 | Vera" names.
30 |
31 | The Font Software may be sold as part of a larger software package but no
32 | copy of one or more of the Font Software typefaces may be sold by itself.
33 |
34 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
35 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
37 | TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
38 | FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
39 | ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
40 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
41 | THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
42 | FONT SOFTWARE.
43 |
44 | Except as contained in this notice, the names of Gnome, the Gnome
45 | Foundation, and Bitstream Inc., shall not be used in advertising or
46 | otherwise to promote the sale, use or other dealings in this Font Software
47 | without prior written authorization from the Gnome Foundation or Bitstream
48 | Inc., respectively. For further information, contact: fonts at gnome dot
49 | org.
50 |
51 | Arev Fonts Copyright
52 | ------------------------------
53 |
54 | Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
55 |
56 | Permission is hereby granted, free of charge, to any person obtaining
57 | a copy of the fonts accompanying this license ("Fonts") and
58 | associated documentation files (the "Font Software"), to reproduce
59 | and distribute the modifications to the Bitstream Vera Font Software,
60 | including without limitation the rights to use, copy, merge, publish,
61 | distribute, and/or sell copies of the Font Software, and to permit
62 | persons to whom the Font Software is furnished to do so, subject to
63 | the following conditions:
64 |
65 | The above copyright and trademark notices and this permission notice
66 | shall be included in all copies of one or more of the Font Software
67 | typefaces.
68 |
69 | The Font Software may be modified, altered, or added to, and in
70 | particular the designs of glyphs or characters in the Fonts may be
71 | modified and additional glyphs or characters may be added to the
72 | Fonts, only if the fonts are renamed to names not containing either
73 | the words "Tavmjong Bah" or the word "Arev".
74 |
75 | This License becomes null and void to the extent applicable to Fonts
76 | or Font Software that has been modified and is distributed under the
77 | "Tavmjong Bah Arev" names.
78 |
79 | The Font Software may be sold as part of a larger software package but
80 | no copy of one or more of the Font Software typefaces may be sold by
81 | itself.
82 |
83 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
84 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
85 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
86 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
87 | TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
88 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
89 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
90 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
91 | OTHER DEALINGS IN THE FONT SOFTWARE.
92 |
93 | Except as contained in this notice, the name of Tavmjong Bah shall not
94 | be used in advertising or otherwise to promote the sale, use or other
95 | dealings in this Font Software without prior written authorization
96 | from Tavmjong Bah. For further information, contact: tavmjong @ free
97 | . fr.
--------------------------------------------------------------------------------
/desktop/dejavu_font/DejaVuSansMono-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono-Bold.ttf
--------------------------------------------------------------------------------
/desktop/dejavu_font/DejaVuSansMono-BoldOblique.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono-BoldOblique.ttf
--------------------------------------------------------------------------------
/desktop/dejavu_font/DejaVuSansMono-Oblique.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono-Oblique.ttf
--------------------------------------------------------------------------------
/desktop/dejavu_font/DejaVuSansMono.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono.ttf
--------------------------------------------------------------------------------
/resources/neovim.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/resources/neovim.ico
--------------------------------------------------------------------------------
/runtime/plugin/nvim_gui_shim.vim:
--------------------------------------------------------------------------------
1 | " A Neovim plugin that implements GUI helper commands
2 | if !has('nvim') || exists('g:GuiLoaded')
3 | finish
4 | endif
5 | let g:GuiLoaded = 1
6 |
7 | if exists('g:GuiInternalClipboard')
8 | let g:clipboard = {
9 | \ 'name': 'neovim-gtk',
10 | \ 'copy': {
11 | \ '+': { lines, regtype -> rpcnotify(1, 'Gui', 'Clipboard', 'Set', regtype, join(lines, '
')) },
12 | \ '*': { lines, regtype -> rpcnotify(1, 'Gui', 'Clipboard', 'Set', regtype, join(lines, '
')) },
13 | \ },
14 | \ 'paste': {
15 | \ '+': { -> rpcrequest(1, 'Gui', 'Clipboard', 'Get', '+') },
16 | \ '*': { -> rpcrequest(1, 'Gui', 'Clipboard', 'Get', '*') },
17 | \ },
18 | \ }
19 | endif
20 |
21 | " Set GUI font
22 | function! GuiFont(fname, ...) abort
23 | call rpcnotify(1, 'Gui', 'Font', s:NvimQtToPangoFont(a:fname))
24 | endfunction
25 |
26 | " Some subset of parse command from neovim-qt
27 | " to support interoperability
28 | function s:NvimQtToPangoFont(fname)
29 | let l:attrs = split(a:fname, ':')
30 | let l:size = -1
31 | for part in l:attrs
32 | if len(part) >= 2 && part[0] == 'h'
33 | let l:size = strpart(part, 1)
34 | endif
35 | endfor
36 |
37 | if l:size > 0
38 | return l:attrs[0] . ' ' . l:size
39 | endif
40 |
41 | return l:attrs[0]
42 | endf
43 |
44 |
45 | " The GuiFont command. For compatibility there is also Guifont
46 | function s:GuiFontCommand(fname, bang) abort
47 | if a:fname ==# ''
48 | if exists('g:GuiFont')
49 | echo g:GuiFont
50 | else
51 | echo 'No GuiFont is set'
52 | endif
53 | else
54 | call GuiFont(a:fname, a:bang ==# '!')
55 | endif
56 | endfunction
57 | command! -nargs=1 -bang Guifont call s:GuiFontCommand("", "")
58 | command! -nargs=1 -bang GuiFont call s:GuiFontCommand("", "")
59 |
60 | command! -nargs=? GuiFontFeatures call rpcnotify(1, 'Gui', 'FontFeatures', )
61 | command! -nargs=1 GuiLinespace call rpcnotify(1, 'Gui', 'Linespace', )
62 |
63 | command! NGToggleSidebar call rpcnotify(1, 'Gui', 'Command', 'ToggleSidebar')
64 | command! NGShowProjectView call rpcnotify(1, 'Gui', 'Command', 'ShowProjectView')
65 | command! -nargs=+ NGTransparency call rpcnotify(1, 'Gui', 'Command', 'Transparency', )
66 | command! -nargs=1 NGPreferDarkTheme call rpcnotify(1, 'Gui', 'Command', 'PreferDarkTheme', )
67 | command! -nargs=1 NGSetCursorBlink call rpcnotify(1, 'Gui', 'Command', 'SetCursorBlink', )
68 |
69 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | edition = "2018"
2 |
3 | use_try_shorthand = true
4 | use_field_init_shorthand = true
5 |
--------------------------------------------------------------------------------
/screenshots/neovimgtk-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/screenshots/neovimgtk-screen.png
--------------------------------------------------------------------------------
/src/cmd_line/viewport.rs:
--------------------------------------------------------------------------------
1 | use once_cell::sync::Lazy;
2 |
3 | use gtk::{
4 | graphene::{Point, Rect},
5 | prelude::*,
6 | subclass::prelude::*,
7 | };
8 |
9 | use std::{
10 | cell::RefCell,
11 | sync::{Arc, Weak},
12 | };
13 |
14 | use crate::{render::*, shell::TransparencySettings, ui::UiMutex};
15 |
16 | use crate::cmd_line::State;
17 |
18 | glib::wrapper! {
19 | pub struct CmdlineViewport(ObjectSubclass)
20 | @extends gtk::Widget,
21 | @implements gtk::Accessible;
22 | }
23 |
24 | impl CmdlineViewport {
25 | pub fn new() -> Self {
26 | glib::Object::new::()
27 | }
28 |
29 | pub fn set_state(&self, state: &Arc>) {
30 | self.set_property("cmdline-state", glib::BoxedAnyObject::new(state.clone()));
31 | }
32 |
33 | pub fn clear_snapshot_cache(&self) {
34 | self.set_property("snapshot-cached", false);
35 | }
36 | }
37 |
38 | #[derive(Default)]
39 | struct CmdlineViewportInner {
40 | state: Weak>,
41 | block_cache: Option,
42 | level_cache: Option,
43 | }
44 |
45 | #[derive(Default)]
46 | pub struct CmdlineViewportObject {
47 | inner: RefCell,
48 | }
49 |
50 | #[glib::object_subclass]
51 | impl ObjectSubclass for CmdlineViewportObject {
52 | const NAME: &'static str = "NvimCmdlineViewport";
53 | type Type = CmdlineViewport;
54 | type ParentType = gtk::Widget;
55 |
56 | fn class_init(klass: &mut Self::Class) {
57 | klass.set_css_name("widget");
58 | klass.set_accessible_role(gtk::AccessibleRole::TextBox);
59 | }
60 | }
61 |
62 | impl ObjectImpl for CmdlineViewportObject {
63 | fn properties() -> &'static [glib::ParamSpec] {
64 | static PROPERTIES: Lazy> = Lazy::new(|| {
65 | vec![
66 | glib::ParamSpecObject::builder::("cmdline-state")
67 | .write_only()
68 | .build(),
69 | glib::ParamSpecBoolean::builder("snapshot-cached").build(),
70 | ]
71 | });
72 |
73 | PROPERTIES.as_ref()
74 | }
75 |
76 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
77 | match pspec.name() {
78 | "cmdline-state" => {
79 | let mut inner = self.inner.borrow_mut();
80 | debug_assert!(inner.state.upgrade().is_none());
81 |
82 | inner.state =
83 | Arc::downgrade(&value.get::().unwrap().borrow());
84 | }
85 | "snapshot-cached" => {
86 | if !value.get::().unwrap() {
87 | let mut inner = self.inner.borrow_mut();
88 | inner.block_cache = None;
89 | inner.level_cache = None;
90 | }
91 | }
92 | _ => unreachable!(),
93 | }
94 | }
95 |
96 | fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
97 | match pspec.name() {
98 | "snapshot-cached" => {
99 | let inner = self.inner.borrow();
100 | (inner.level_cache.is_some() || inner.block_cache.is_some()).to_value()
101 | }
102 | _ => unreachable!(),
103 | }
104 | }
105 | }
106 |
107 | impl WidgetImpl for CmdlineViewportObject {
108 | fn snapshot(&self, snapshot: >k::Snapshot) {
109 | let obj = self.obj();
110 | let mut inner = self.inner.borrow_mut();
111 | let state = match inner.state.upgrade() {
112 | Some(state) => state,
113 | None => return,
114 | };
115 | let mut state = state.borrow_mut();
116 | let render_state = state.render_state.clone();
117 | let render_state = render_state.borrow();
118 | let font_ctx = &render_state.font_ctx;
119 | let hl = &render_state.hl;
120 |
121 | snapshot.append_color(
122 | &hl.bg().into(),
123 | &Rect::new(0.0, 0.0, obj.width() as f32, obj.height() as f32),
124 | );
125 |
126 | snapshot.save();
127 |
128 | let preferred_height = state.preferred_height();
129 | let gap = obj.height() - preferred_height;
130 | if gap > 0 {
131 | snapshot.translate(&Point::new(0.0, (gap / 2) as f32));
132 | }
133 |
134 | if let Some(block) = state.block.as_mut() {
135 | if inner.block_cache.is_none() {
136 | inner.block_cache = snapshot_nvim(font_ctx, &block.model_layout.model, hl);
137 | }
138 | if let Some(ref cache) = inner.block_cache {
139 | snapshot.append_node(cache);
140 | }
141 |
142 | snapshot.translate(&Point::new(0.0, block.preferred_height as f32));
143 | }
144 |
145 | if let Some(level) = state.levels.last_mut() {
146 | if inner.level_cache.is_none() {
147 | inner.level_cache = snapshot_nvim(font_ctx, &level.model_layout.model, hl);
148 | }
149 | if let Some(ref cache) = inner.level_cache {
150 | snapshot.append_node(cache);
151 | }
152 | }
153 |
154 | if let Some(level) = state.levels.last() {
155 | if let Some(ref cursor) = state.cursor {
156 | snapshot_cursor(
157 | snapshot,
158 | cursor,
159 | font_ctx,
160 | &level.model_layout.model,
161 | hl,
162 | TransparencySettings::new(), // FIXME
163 | );
164 | }
165 | }
166 |
167 | snapshot.restore();
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/color.rs:
--------------------------------------------------------------------------------
1 | #[derive(Clone, Copy, PartialEq, Debug, Default)]
2 | pub struct Color(pub f64, pub f64, pub f64);
3 |
4 | pub const COLOR_BLACK: Color = Color(0.0, 0.0, 0.0);
5 | pub const COLOR_WHITE: Color = Color(1.0, 1.0, 1.0);
6 | pub const COLOR_RED: Color = Color(1.0, 0.0, 0.0);
7 |
8 | impl From<&Color> for gdk::RGBA {
9 | fn from(color: &Color) -> Self {
10 | color.to_rgbo(1.0)
11 | }
12 | }
13 |
14 | impl Color {
15 | pub fn from_cterm(idx: u8) -> Color {
16 | let color = TERMINAL_COLORS[usize::from(idx)];
17 | Color(
18 | color.0 as f64 / 255.0,
19 | color.1 as f64 / 255.0,
20 | color.2 as f64 / 255.0,
21 | )
22 | }
23 |
24 | pub fn from_indexed_color(indexed_color: u64) -> Color {
25 | let r = ((indexed_color >> 16) & 0xff) as f64;
26 | let g = ((indexed_color >> 8) & 0xff) as f64;
27 | let b = (indexed_color & 0xff) as f64;
28 | Color(r / 255.0, g / 255.0, b / 255.0)
29 | }
30 |
31 | pub fn to_u16(self) -> (u16, u16, u16) {
32 | (
33 | (u16::MAX as f64 * self.0) as u16,
34 | (u16::MAX as f64 * self.1) as u16,
35 | (u16::MAX as f64 * self.2) as u16,
36 | )
37 | }
38 |
39 | pub fn to_hex(self) -> String {
40 | format!(
41 | "#{:02X}{:02X}{:02X}",
42 | (self.0 * 255.0) as u8,
43 | (self.1 * 255.0) as u8,
44 | (self.2 * 255.0) as u8,
45 | )
46 | }
47 |
48 | pub fn to_rgbo(self, alpha: f64) -> gdk::RGBA {
49 | gdk::RGBA::new(self.0 as f32, self.1 as f32, self.2 as f32, alpha as f32)
50 | }
51 |
52 | pub fn to_pango_bg(self) -> pango::AttrColor {
53 | let (r, g, b) = self.to_u16();
54 |
55 | pango::AttrColor::new_background(r, g, b)
56 | }
57 |
58 | pub fn to_pango_fg(self) -> pango::AttrColor {
59 | let (r, g, b) = self.to_u16();
60 |
61 | pango::AttrColor::new_foreground(r, g, b)
62 | }
63 |
64 | pub fn invert(&self) -> Self {
65 | Self(1.0 - self.0, 1.0 - self.1, 1.0 - self.2)
66 | }
67 |
68 | pub fn fade(&self, into: &Self, percentage: f64) -> Self {
69 | debug_assert!((0.0..=1.0).contains(&percentage));
70 |
71 | match percentage {
72 | _ if percentage <= 0.000001 => *self,
73 | _ if percentage >= 0.999999 => *into,
74 | _ => {
75 | let inv = (into.0 - self.0, into.1 - self.1, into.2 - self.2);
76 | Self(
77 | self.0 + (inv.0 * percentage),
78 | self.1 + (inv.1 * percentage),
79 | self.2 + (inv.2 * percentage),
80 | )
81 | }
82 | }
83 | }
84 | }
85 |
86 | /// From https://jonasjacek.github.io/colors/
87 | const TERMINAL_COLORS: [(u8, u8, u8); 256] = [
88 | (0, 0, 0),
89 | (128, 0, 0),
90 | (0, 128, 0),
91 | (128, 128, 0),
92 | (0, 0, 128),
93 | (128, 0, 128),
94 | (0, 128, 128),
95 | (192, 192, 192),
96 | (128, 128, 128),
97 | (255, 0, 0),
98 | (0, 255, 0),
99 | (255, 255, 0),
100 | (0, 0, 255),
101 | (255, 0, 255),
102 | (0, 255, 255),
103 | (255, 255, 255),
104 | (0, 0, 0),
105 | (0, 0, 95),
106 | (0, 0, 135),
107 | (0, 0, 175),
108 | (0, 0, 215),
109 | (0, 0, 255),
110 | (0, 95, 0),
111 | (0, 95, 95),
112 | (0, 95, 135),
113 | (0, 95, 175),
114 | (0, 95, 215),
115 | (0, 95, 255),
116 | (0, 135, 0),
117 | (0, 135, 95),
118 | (0, 135, 135),
119 | (0, 135, 175),
120 | (0, 135, 215),
121 | (0, 135, 255),
122 | (0, 175, 0),
123 | (0, 175, 95),
124 | (0, 175, 135),
125 | (0, 175, 175),
126 | (0, 175, 215),
127 | (0, 175, 255),
128 | (0, 215, 0),
129 | (0, 215, 95),
130 | (0, 215, 135),
131 | (0, 215, 175),
132 | (0, 215, 215),
133 | (0, 215, 255),
134 | (0, 255, 0),
135 | (0, 255, 95),
136 | (0, 255, 135),
137 | (0, 255, 175),
138 | (0, 255, 215),
139 | (0, 255, 255),
140 | (95, 0, 0),
141 | (95, 0, 95),
142 | (95, 0, 135),
143 | (95, 0, 175),
144 | (95, 0, 215),
145 | (95, 0, 255),
146 | (95, 95, 0),
147 | (95, 95, 95),
148 | (95, 95, 135),
149 | (95, 95, 175),
150 | (95, 95, 215),
151 | (95, 95, 255),
152 | (95, 135, 0),
153 | (95, 135, 95),
154 | (95, 135, 135),
155 | (95, 135, 175),
156 | (95, 135, 215),
157 | (95, 135, 255),
158 | (95, 175, 0),
159 | (95, 175, 95),
160 | (95, 175, 135),
161 | (95, 175, 175),
162 | (95, 175, 215),
163 | (95, 175, 255),
164 | (95, 215, 0),
165 | (95, 215, 95),
166 | (95, 215, 135),
167 | (95, 215, 175),
168 | (95, 215, 215),
169 | (95, 215, 255),
170 | (95, 255, 0),
171 | (95, 255, 95),
172 | (95, 255, 135),
173 | (95, 255, 175),
174 | (95, 255, 215),
175 | (95, 255, 255),
176 | (135, 0, 0),
177 | (135, 0, 95),
178 | (135, 0, 135),
179 | (135, 0, 175),
180 | (135, 0, 215),
181 | (135, 0, 255),
182 | (135, 95, 0),
183 | (135, 95, 95),
184 | (135, 95, 135),
185 | (135, 95, 175),
186 | (135, 95, 215),
187 | (135, 95, 255),
188 | (135, 135, 0),
189 | (135, 135, 95),
190 | (135, 135, 135),
191 | (135, 135, 175),
192 | (135, 135, 215),
193 | (135, 135, 255),
194 | (135, 175, 0),
195 | (135, 175, 95),
196 | (135, 175, 135),
197 | (135, 175, 175),
198 | (135, 175, 215),
199 | (135, 175, 255),
200 | (135, 215, 0),
201 | (135, 215, 95),
202 | (135, 215, 135),
203 | (135, 215, 175),
204 | (135, 215, 215),
205 | (135, 215, 255),
206 | (135, 255, 0),
207 | (135, 255, 95),
208 | (135, 255, 135),
209 | (135, 255, 175),
210 | (135, 255, 215),
211 | (135, 255, 255),
212 | (175, 0, 0),
213 | (175, 0, 95),
214 | (175, 0, 135),
215 | (175, 0, 175),
216 | (175, 0, 215),
217 | (175, 0, 255),
218 | (175, 95, 0),
219 | (175, 95, 95),
220 | (175, 95, 135),
221 | (175, 95, 175),
222 | (175, 95, 215),
223 | (175, 95, 255),
224 | (175, 135, 0),
225 | (175, 135, 95),
226 | (175, 135, 135),
227 | (175, 135, 175),
228 | (175, 135, 215),
229 | (175, 135, 255),
230 | (175, 175, 0),
231 | (175, 175, 95),
232 | (175, 175, 135),
233 | (175, 175, 175),
234 | (175, 175, 215),
235 | (175, 175, 255),
236 | (175, 215, 0),
237 | (175, 215, 95),
238 | (175, 215, 135),
239 | (175, 215, 175),
240 | (175, 215, 215),
241 | (175, 215, 255),
242 | (175, 255, 0),
243 | (175, 255, 95),
244 | (175, 255, 135),
245 | (175, 255, 175),
246 | (175, 255, 215),
247 | (175, 255, 255),
248 | (215, 0, 0),
249 | (215, 0, 95),
250 | (215, 0, 135),
251 | (215, 0, 175),
252 | (215, 0, 215),
253 | (215, 0, 255),
254 | (215, 95, 0),
255 | (215, 95, 95),
256 | (215, 95, 135),
257 | (215, 95, 175),
258 | (215, 95, 215),
259 | (215, 95, 255),
260 | (215, 135, 0),
261 | (215, 135, 95),
262 | (215, 135, 135),
263 | (215, 135, 175),
264 | (215, 135, 215),
265 | (215, 135, 255),
266 | (215, 175, 0),
267 | (215, 175, 95),
268 | (215, 175, 135),
269 | (215, 175, 175),
270 | (215, 175, 215),
271 | (215, 175, 255),
272 | (215, 215, 0),
273 | (215, 215, 95),
274 | (215, 215, 135),
275 | (215, 215, 175),
276 | (215, 215, 215),
277 | (215, 215, 255),
278 | (215, 255, 0),
279 | (215, 255, 95),
280 | (215, 255, 135),
281 | (215, 255, 175),
282 | (215, 255, 215),
283 | (215, 255, 255),
284 | (255, 0, 0),
285 | (255, 0, 95),
286 | (255, 0, 135),
287 | (255, 0, 175),
288 | (255, 0, 215),
289 | (255, 0, 255),
290 | (255, 95, 0),
291 | (255, 95, 95),
292 | (255, 95, 135),
293 | (255, 95, 175),
294 | (255, 95, 215),
295 | (255, 95, 255),
296 | (255, 135, 0),
297 | (255, 135, 95),
298 | (255, 135, 135),
299 | (255, 135, 175),
300 | (255, 135, 215),
301 | (255, 135, 255),
302 | (255, 175, 0),
303 | (255, 175, 95),
304 | (255, 175, 135),
305 | (255, 175, 175),
306 | (255, 175, 215),
307 | (255, 175, 255),
308 | (255, 215, 0),
309 | (255, 215, 95),
310 | (255, 215, 135),
311 | (255, 215, 175),
312 | (255, 215, 215),
313 | (255, 215, 255),
314 | (255, 255, 0),
315 | (255, 255, 95),
316 | (255, 255, 135),
317 | (255, 255, 175),
318 | (255, 255, 215),
319 | (255, 255, 255),
320 | (8, 8, 8),
321 | (18, 18, 18),
322 | (28, 28, 28),
323 | (38, 38, 38),
324 | (48, 48, 48),
325 | (58, 58, 58),
326 | (68, 68, 68),
327 | (78, 78, 78),
328 | (88, 88, 88),
329 | (98, 98, 98),
330 | (108, 108, 108),
331 | (118, 118, 118),
332 | (128, 128, 128),
333 | (138, 138, 138),
334 | (148, 148, 148),
335 | (158, 158, 158),
336 | (168, 168, 168),
337 | (178, 178, 178),
338 | (188, 188, 188),
339 | (198, 198, 198),
340 | (208, 208, 208),
341 | (218, 218, 218),
342 | (228, 228, 228),
343 | (238, 238, 238),
344 | ];
345 |
346 | #[cfg(test)]
347 | mod tests {
348 | use super::*;
349 |
350 | #[test]
351 | fn test_to_hex() {
352 | let col = Color(0.0, 1.0, 0.0);
353 | assert_eq!("#00FF00", &col.to_hex());
354 | }
355 | }
356 |
--------------------------------------------------------------------------------
/src/dirs.rs:
--------------------------------------------------------------------------------
1 | use std::path::*;
2 |
3 | use once_cell::sync::Lazy;
4 |
5 | pub fn app_config_dir_create() -> Result {
6 | let config_dir = app_config_dir();
7 |
8 | std::fs::create_dir_all(config_dir).map_err(|e| format!("{e}"))?;
9 |
10 | Ok(config_dir.to_path_buf())
11 | }
12 |
13 | pub fn app_config_dir() -> &'static Path {
14 | static DIR: Lazy = Lazy::new(|| glib::user_config_dir().join("nvim-gtk"));
15 | DIR.as_path()
16 | }
17 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::ops::Deref;
2 |
3 | use log::error;
4 |
5 | use html_escape::encode_text_minimal;
6 |
7 | use gtk::prelude::*;
8 |
9 | use crate::shell;
10 |
11 | pub struct ErrorArea {
12 | base: gtk::Box,
13 | label: gtk::Label,
14 | }
15 |
16 | impl ErrorArea {
17 | pub fn new() -> Self {
18 | let base = gtk::Box::builder()
19 | .orientation(gtk::Orientation::Horizontal)
20 | .spacing(10)
21 | .valign(gtk::Align::Center)
22 | .halign(gtk::Align::Center)
23 | .vexpand(true)
24 | .hexpand(true)
25 | .build();
26 |
27 | let label = gtk::Label::builder()
28 | .wrap(true)
29 | .selectable(true)
30 | .hexpand(true)
31 | .vexpand(true)
32 | .build();
33 |
34 | let error_image = gtk::Image::from_icon_name("dialog-error");
35 | error_image.set_icon_size(gtk::IconSize::Large);
36 | error_image.set_halign(gtk::Align::End);
37 | error_image.set_valign(gtk::Align::Center);
38 | error_image.set_hexpand(true);
39 | error_image.set_vexpand(true);
40 |
41 | base.append(&error_image);
42 | base.append(&label);
43 |
44 | ErrorArea { base, label }
45 | }
46 |
47 | pub fn show_nvim_init_error(&self, err: &str) {
48 | error!("Can't initialize nvim: {}", err);
49 | self.label.set_markup(&format!(
50 | "Can't initialize nvim:\n\
51 | {}\n\n\
52 | Possible error reasons:\n\
53 | ● Not supported nvim version (minimum supported version is {})\n\
54 | ● Error in configuration file (init.vim or ginit.vim)",
55 | encode_text_minimal(err),
56 | shell::MINIMUM_SUPPORTED_NVIM_VERSION
57 | ));
58 | self.base.show();
59 | }
60 |
61 | pub fn show_nvim_start_error(&self, err: &str, cmd: &str) {
62 | error!("Can't start nvim: {}\nCommand line: {}", err, cmd);
63 | self.label.set_markup(&format!(
64 | "Can't start nvim instance:\n\
65 | {}\n\
66 | {}\n\n\
67 | Possible error reasons:\n\
68 | ● Not supported nvim version (minimum supported version is {})\n\
69 | ● Error in configuration file (init.vim or ginit.vim)\n\
70 | ● Wrong nvim binary path \
71 | (right path can be passed with --nvim-bin-path=path_here)",
72 | encode_text_minimal(cmd),
73 | encode_text_minimal(err),
74 | shell::MINIMUM_SUPPORTED_NVIM_VERSION
75 | ));
76 | self.base.show();
77 | }
78 |
79 | pub fn show_nvim_tcp_connect_error(&self, err: &str, addr: &str) {
80 | error!("Can't connect to nvim on TCP address {}: {}\n", addr, err);
81 | self.label.set_markup(&format!(
82 | "Can't connect to nvim instance on TCP address {}:\n\
83 | {}\n\
84 | Possible error reasons:\n\
85 | ● Not supported nvim version (minimum supported version is {})\n\
86 | ● Error in configuration file (init.vim or ginit.vim)\n\
87 | ● Invalid TCP address",
88 | encode_text_minimal(addr),
89 | encode_text_minimal(err),
90 | shell::MINIMUM_SUPPORTED_NVIM_VERSION
91 | ));
92 | self.base.show();
93 | }
94 |
95 | #[cfg(unix)]
96 | pub fn show_nvim_unix_connect_error(&self, err: &str, addr: &str) {
97 | error!("Can't connect to nvim on Unix pipe {}: {}\n", addr, err);
98 | self.label.set_markup(&format!(
99 | "Can't connect to nvim instance on Unix pipe {}:\n\
100 | {}\n\
101 | Possible error reasons:\n\
102 | ● Not supported nvim version (minimum supported version is {})\n\
103 | ● Error in configuration file (init.vim or ginit.vim)\n\
104 | ● Invalid Unix pipe",
105 | encode_text_minimal(addr),
106 | encode_text_minimal(err),
107 | shell::MINIMUM_SUPPORTED_NVIM_VERSION
108 | ));
109 | self.base.show();
110 | }
111 | }
112 |
113 | impl Deref for ErrorArea {
114 | type Target = gtk::Box;
115 |
116 | fn deref(&self) -> >k::Box {
117 | &self.base
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/file_browser/tree_view.rs:
--------------------------------------------------------------------------------
1 | use once_cell::sync::Lazy;
2 |
3 | use gtk::{prelude::*, subclass::prelude::*};
4 |
5 | glib::wrapper! {
6 | pub struct TreeView(ObjectSubclass)
7 | @extends gtk::Widget, gtk::TreeView;
8 | }
9 |
10 | /// A popup-aware TreeView widget for the file browser pane
11 | impl TreeView {
12 | pub fn new() -> Self {
13 | glib::Object::new::()
14 | }
15 |
16 | pub fn set_context_menu(&self, context_menu: >k::PopoverMenu) {
17 | self.set_property("context-menu", context_menu);
18 | }
19 | }
20 |
21 | #[derive(Default)]
22 | pub struct TreeViewObject {
23 | context_menu: glib::WeakRef,
24 | }
25 |
26 | #[glib::object_subclass]
27 | impl ObjectSubclass for TreeViewObject {
28 | const NAME: &'static str = "NvimFileBrowserTreeView";
29 | type Type = TreeView;
30 | type ParentType = gtk::TreeView;
31 | }
32 |
33 | impl ObjectImpl for TreeViewObject {
34 | fn dispose(&self) {
35 | if let Some(context_menu) = self.context_menu.upgrade() {
36 | context_menu.unparent();
37 | }
38 | }
39 |
40 | fn properties() -> &'static [glib::ParamSpec] {
41 | static PROPERTIES: Lazy> = Lazy::new(|| {
42 | vec![glib::ParamSpecObject::builder::("context-menu").build()]
43 | });
44 |
45 | PROPERTIES.as_ref()
46 | }
47 |
48 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
49 | let obj = self.obj();
50 | match pspec.name() {
51 | "context-menu" => {
52 | if let Some(context_menu) = self.context_menu.upgrade() {
53 | context_menu.unparent();
54 | }
55 |
56 | let context_menu: gtk::PopoverMenu = value.get().unwrap();
57 | context_menu.set_parent(&*obj);
58 | self.context_menu.set(Some(&context_menu));
59 | }
60 | _ => unimplemented!(),
61 | }
62 | }
63 |
64 | fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
65 | match pspec.name() {
66 | "context-menu" => self.context_menu.upgrade().to_value(),
67 | _ => unimplemented!(),
68 | }
69 | }
70 | }
71 |
72 | impl WidgetImpl for TreeViewObject {
73 | fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
74 | self.parent_size_allocate(width, height, baseline);
75 | self.context_menu.upgrade().unwrap().present();
76 | }
77 | }
78 |
79 | impl gtk::subclass::prelude::TreeViewImpl for TreeViewObject {}
80 |
--------------------------------------------------------------------------------
/src/grid.rs:
--------------------------------------------------------------------------------
1 | use std::ops::{Index, IndexMut};
2 | use std::rc::Rc;
3 |
4 | use fnv::FnvHashMap;
5 |
6 | use nvim_rs::Value;
7 |
8 | use crate::highlight::{Highlight, HighlightMap};
9 | use crate::ui_model::{ModelRect, UiModel};
10 |
11 | const DEFAULT_GRID: u64 = 1;
12 |
13 | pub struct GridMap {
14 | grids: FnvHashMap,
15 | }
16 |
17 | impl Index for GridMap {
18 | type Output = Grid;
19 |
20 | fn index(&self, idx: u64) -> &Grid {
21 | &self.grids[&idx]
22 | }
23 | }
24 |
25 | impl IndexMut for GridMap {
26 | fn index_mut(&mut self, idx: u64) -> &mut Grid {
27 | self.grids.get_mut(&idx).unwrap()
28 | }
29 | }
30 |
31 | impl GridMap {
32 | pub fn new() -> Self {
33 | GridMap {
34 | grids: FnvHashMap::default(),
35 | }
36 | }
37 |
38 | pub fn current(&self) -> Option<&Grid> {
39 | self.grids.get(&DEFAULT_GRID)
40 | }
41 |
42 | pub fn current_model_mut(&mut self) -> Option<&mut UiModel> {
43 | self.grids.get_mut(&DEFAULT_GRID).map(|g| &mut g.model)
44 | }
45 |
46 | pub fn current_model(&self) -> Option<&UiModel> {
47 | self.grids.get(&DEFAULT_GRID).map(|g| &g.model)
48 | }
49 |
50 | pub fn get_or_create(&mut self, idx: u64) -> &mut Grid {
51 | if self.grids.contains_key(&idx) {
52 | return self.grids.get_mut(&idx).unwrap();
53 | }
54 |
55 | self.grids.insert(idx, Grid::new());
56 | self.grids.get_mut(&idx).unwrap()
57 | }
58 |
59 | pub fn destroy(&mut self, idx: u64) {
60 | self.grids.remove(&idx);
61 | }
62 |
63 | pub fn clear_glyphs(&mut self) {
64 | for grid in self.grids.values_mut() {
65 | grid.model.clear_glyphs();
66 | }
67 | }
68 |
69 | /// Flush any pending cursor position updates (e.g. updates we received before getting a 'flush'
70 | /// event)
71 | pub fn flush_cursor(&mut self) {
72 | for grid in self.grids.values_mut() {
73 | grid.flush_cursor();
74 | }
75 | }
76 | }
77 |
78 | pub struct Grid {
79 | model: UiModel,
80 | }
81 |
82 | impl Grid {
83 | pub fn new() -> Self {
84 | Grid {
85 | model: UiModel::default(),
86 | }
87 | }
88 |
89 | pub fn get_cursor(&self) -> (usize, usize) {
90 | self.model.get_real_cursor()
91 | }
92 |
93 | pub fn flush_cursor(&mut self) {
94 | self.model.flush_cursor();
95 | }
96 |
97 | pub fn cur_point(&self) -> ModelRect {
98 | self.model.cur_real_point()
99 | }
100 |
101 | pub fn resize(&mut self, columns: u64, rows: u64) {
102 | if self.model.columns != columns as usize || self.model.rows != rows as usize {
103 | self.model = UiModel::new(rows, columns);
104 | }
105 | }
106 |
107 | pub fn cursor_goto(&mut self, row: usize, col: usize) {
108 | self.model.set_cursor(row, col)
109 | }
110 |
111 | pub fn clear(&mut self, default_hl: &Rc) {
112 | self.model.clear(default_hl);
113 | }
114 |
115 | #[allow(clippy::get_first)] // get(0), get(1), get(2) more consistent than .first(), .get(1)
116 | pub fn line(
117 | &mut self,
118 | row: usize,
119 | col_start: usize,
120 | cells: Vec>,
121 | highlights: &HighlightMap,
122 | ) -> ModelRect {
123 | let mut hl_id = None;
124 | let mut col_end = col_start;
125 |
126 | for cell in cells {
127 | let ch = cell.get(0).unwrap().as_str().unwrap_or("");
128 | hl_id = cell.get(1).and_then(|h| h.as_u64()).or(hl_id);
129 | let repeat = cell.get(2).and_then(|r| r.as_u64()).unwrap_or(1) as usize;
130 |
131 | self.model.put(
132 | row,
133 | col_end,
134 | ch,
135 | ch.is_empty(),
136 | repeat,
137 | highlights.get(hl_id),
138 | );
139 | col_end += repeat;
140 | }
141 |
142 | ModelRect::new(row, row, col_start, col_end - 1)
143 | }
144 |
145 | pub fn scroll(
146 | &mut self,
147 | top: u64,
148 | bot: u64,
149 | left: u64,
150 | right: u64,
151 | rows: i64,
152 | _: i64,
153 | default_hl: &Rc,
154 | ) {
155 | self.model.scroll(
156 | top as i64,
157 | bot as i64 - 1,
158 | left as usize,
159 | right as usize - 1,
160 | rows,
161 | default_hl,
162 | )
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/input.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use log::debug;
4 |
5 | use crate::nvim::{ErrorReport, NvimSession};
6 |
7 | include!(concat!(env!("OUT_DIR"), "/key_map_table.rs"));
8 |
9 | pub fn keyval_to_input_string(in_str: &str, in_state: gdk::ModifierType) -> String {
10 | let mut val = in_str;
11 | let mut state = in_state;
12 | let empty = in_str.is_empty();
13 |
14 | if !empty {
15 | debug!("keyval -> {}", in_str);
16 | }
17 |
18 | // CTRL-^ and CTRL-@ don't work in the normal way.
19 | if state.contains(gdk::ModifierType::CONTROL_MASK)
20 | && !state.contains(gdk::ModifierType::SHIFT_MASK)
21 | && !state.contains(gdk::ModifierType::ALT_MASK)
22 | && !state.contains(gdk::ModifierType::META_MASK)
23 | {
24 | if val == "6" {
25 | val = "^";
26 | } else if val == "2" {
27 | val = "@";
28 | }
29 | }
30 |
31 | let chars: Vec = in_str.chars().collect();
32 |
33 | if chars.len() == 1 {
34 | let ch = chars[0];
35 |
36 | // Remove SHIFT
37 | if ch.is_ascii() && !ch.is_alphanumeric() {
38 | state.remove(gdk::ModifierType::SHIFT_MASK);
39 | }
40 | }
41 |
42 | if val == "<" {
43 | val = "lt";
44 | }
45 |
46 | let mut mod_chars = Vec::<&str>::with_capacity(3);
47 | if state.contains(gdk::ModifierType::SHIFT_MASK) {
48 | mod_chars.push("S");
49 | }
50 | if state.contains(gdk::ModifierType::CONTROL_MASK) {
51 | mod_chars.push("C");
52 | }
53 | if state.contains(gdk::ModifierType::ALT_MASK) || state.contains(gdk::ModifierType::META_MASK) {
54 | mod_chars.push("A");
55 | }
56 |
57 | let sep = if empty { "" } else { "-" };
58 | let input = [mod_chars.as_slice(), &[val]].concat().join(sep);
59 |
60 | if !empty && input.chars().count() > 1 {
61 | format!("<{input}>")
62 | } else {
63 | input
64 | }
65 | }
66 |
67 | pub fn convert_key(keyval: gdk::Key, modifiers: gdk::ModifierType) -> Option {
68 | if let Some(ref keyval_name) = keyval.name() {
69 | if let Some(cnvt) = KEYVAL_MAP.get(keyval_name.as_str()).cloned() {
70 | return Some(keyval_to_input_string(cnvt, modifiers));
71 | }
72 | }
73 |
74 | keyval
75 | .to_unicode()
76 | .map(|ch| keyval_to_input_string(&ch.to_string(), modifiers))
77 | }
78 |
79 | pub fn im_input(nvim: &NvimSession, input: &str) {
80 | debug!("nvim_input -> {}", input);
81 |
82 | let input: String = input
83 | .chars()
84 | .map(|ch| keyval_to_input_string(&ch.to_string(), gdk::ModifierType::empty()))
85 | .collect();
86 | nvim.block_timeout(nvim.input(&input))
87 | .ok_and_report()
88 | .expect("Failed to send input command to nvim");
89 | }
90 |
91 | pub fn gtk_key_press(
92 | nvim: &NvimSession,
93 | keyval: gdk::Key,
94 | modifiers: gdk::ModifierType,
95 | ) -> glib::Propagation {
96 | if let Some(input) = convert_key(keyval, modifiers) {
97 | debug!("nvim_input -> {}", input);
98 | nvim.block_timeout(nvim.input(&input))
99 | .ok_and_report()
100 | .expect("Failed to send input command to nvim");
101 | glib::Propagation::Stop
102 | } else {
103 | glib::Propagation::Proceed
104 | }
105 | }
106 |
107 | #[cfg(test)]
108 | mod tests {
109 | use super::*;
110 |
111 | #[test]
112 | fn test_keyval_to_input_string() {
113 | macro_rules! test {
114 | ( $( $in_str:literal $( , $( $mod:ident )|* )? == $out_str:literal );*; ) => {
115 | let mut modifier;
116 | $(
117 | modifier = gdk::ModifierType::empty() $( | $( gdk::ModifierType::$mod )|* )?;
118 | assert_eq!(keyval_to_input_string($in_str, modifier), $out_str)
119 | );*
120 | }
121 | }
122 |
123 | test! {
124 | "a" == "a";
125 | "" == "";
126 | "6" == "6";
127 | "2" == "2";
128 | "<" == "";
129 | "", SHIFT_MASK == "S";
130 | "", SHIFT_MASK | CONTROL_MASK | ALT_MASK == "SCA";
131 | "a", SHIFT_MASK == "";
132 | "a", SHIFT_MASK | CONTROL_MASK | ALT_MASK == "";
133 | "6", CONTROL_MASK == "";
134 | "6", CONTROL_MASK | META_MASK == "";
135 | "2", CONTROL_MASK == "";
136 | "2", CONTROL_MASK | ALT_MASK == "";
137 | "j", SUPER_MASK == "j";
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/misc.rs:
--------------------------------------------------------------------------------
1 | use std::borrow::Cow;
2 | use std::mem;
3 |
4 | use once_cell::sync::Lazy;
5 |
6 | use percent_encoding::percent_decode;
7 | use regex::Regex;
8 |
9 | use crate::shell;
10 |
11 | /// Split comma separated parameters with ',' except escaped '\\,'
12 | pub fn split_at_comma(source: &str) -> Vec {
13 | let mut items = Vec::new();
14 |
15 | let mut escaped = false;
16 | let mut item = String::new();
17 |
18 | for ch in source.chars() {
19 | if ch == ',' && !escaped {
20 | item = item.replace("\\,", ",");
21 |
22 | let mut new_item = String::new();
23 | mem::swap(&mut item, &mut new_item);
24 |
25 | items.push(new_item);
26 | } else {
27 | item.push(ch);
28 | }
29 | escaped = ch == '\\';
30 | }
31 |
32 | if !item.is_empty() {
33 | items.push(item.replace("\\,", ","));
34 | }
35 |
36 | items
37 | }
38 |
39 | /// Escape special ASCII characters with a backslash.
40 | pub fn escape_filename(filename: &str) -> Cow {
41 | static SPECIAL_CHARS: Lazy = Lazy::new(|| {
42 | if cfg!(target_os = "windows") {
43 | // On Windows, don't escape `:` and `\`, as these are valid components of the path.
44 | Regex::new(r"[[:ascii:]&&[^0-9a-zA-Z._:\\-]]").unwrap()
45 | } else {
46 | // Similarly, don't escape `/` on other platforms.
47 | Regex::new(r"[[:ascii:]&&[^0-9a-zA-Z._/-]]").unwrap()
48 | }
49 | });
50 | SPECIAL_CHARS.replace_all(filename, r"\$0")
51 | }
52 |
53 | /// Decode a file URI.
54 | ///
55 | /// - On UNIX: `file:///path/to/a%20file.ext` -> `/path/to/a file.ext`
56 | /// - On Windows: `file:///C:/path/to/a%20file.ext` -> `C:\path\to\a file.ext`
57 | pub fn decode_uri(uri: &str) -> Option {
58 | let path = match uri.split_at(8) {
59 | ("file:///", path) => path,
60 | _ => return None,
61 | };
62 | let path = percent_decode(path.as_bytes()).decode_utf8().ok()?;
63 | if cfg!(target_os = "windows") {
64 | static SLASH: Lazy = Lazy::new(|| Regex::new(r"/").unwrap());
65 | Some(String::from(SLASH.replace_all(&path, r"\")))
66 | } else {
67 | Some("/".to_owned() + &path)
68 | }
69 | }
70 |
71 | /// info text
72 | pub fn about_comments() -> String {
73 | format!(
74 | "Build on top of neovim\n\
75 | Minimum supported neovim version: {}",
76 | shell::MINIMUM_SUPPORTED_NVIM_VERSION
77 | )
78 | }
79 |
80 | /// Escape a VimL expression so that it may included in quotes
81 | #[rustfmt::skip]
82 | pub fn viml_escape(viml: &str) -> String {
83 | viml.replace('\\', r"\\")
84 | .replace('"', r#"\""#)
85 | }
86 |
87 | pub trait BoolExt {
88 | /// Parse a bool in a str represented by '0' or '1'
89 | fn from_int_str(int_str: &str) -> Option;
90 | }
91 |
92 | impl BoolExt for bool {
93 | fn from_int_str(int_str: &str) -> Option {
94 | match int_str {
95 | "0" => Some(false),
96 | "1" => Some(true),
97 | _ => None,
98 | }
99 | }
100 | }
101 |
102 | #[cfg(test)]
103 | mod tests {
104 | use super::*;
105 |
106 | #[test]
107 | fn test_comma_split() {
108 | let res = split_at_comma("a,b");
109 | assert_eq!(2, res.len());
110 | assert_eq!("a", res[0]);
111 | assert_eq!("b", res[1]);
112 |
113 | let res = split_at_comma("a,b\\,c");
114 | assert_eq!(2, res.len());
115 | assert_eq!("a", res[0]);
116 | assert_eq!("b,c", res[1]);
117 | }
118 |
119 | #[test]
120 | fn test_viml_escape() {
121 | assert_eq!(r#"a\"b\\"#, viml_escape(r#"a"b\"#));
122 | }
123 |
124 | #[test]
125 | fn test_bool_int_str() {
126 | assert_eq!(bool::from_int_str("0"), Some(false));
127 | assert_eq!(bool::from_int_str("1"), Some(true));
128 | assert_eq!(bool::from_int_str("2"), None);
129 | assert_eq!(bool::from_int_str("f"), None);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/mode.rs:
--------------------------------------------------------------------------------
1 | use log::error;
2 | use nvim_rs::Value;
3 | use std::collections::HashMap;
4 |
5 | #[derive(Clone, PartialEq, Eq)]
6 | pub enum NvimMode {
7 | Normal,
8 | Insert,
9 | Other,
10 | }
11 |
12 | pub struct Mode {
13 | mode: NvimMode,
14 | idx: usize,
15 | info: Option>,
16 | }
17 |
18 | impl Mode {
19 | pub fn new() -> Self {
20 | Mode {
21 | mode: NvimMode::Normal,
22 | idx: 0,
23 | info: None,
24 | }
25 | }
26 |
27 | pub fn is(&self, mode: &NvimMode) -> bool {
28 | self.mode == *mode
29 | }
30 |
31 | pub fn mode_info(&self) -> Option<&ModeInfo> {
32 | self.info.as_ref().and_then(|i| i.get(self.idx))
33 | }
34 |
35 | pub fn update(&mut self, mode: &str, idx: usize) {
36 | match mode {
37 | "normal" => self.mode = NvimMode::Normal,
38 | "insert" => self.mode = NvimMode::Insert,
39 | _ => self.mode = NvimMode::Other,
40 | }
41 |
42 | self.idx = idx;
43 | }
44 |
45 | pub fn set_info(&mut self, cursor_style_enabled: bool, info: Vec) {
46 | self.info = if cursor_style_enabled {
47 | Some(info)
48 | } else {
49 | None
50 | };
51 | }
52 | }
53 |
54 | #[derive(Debug, PartialEq, Eq, Clone)]
55 | pub enum CursorShape {
56 | Block,
57 | Horizontal,
58 | Vertical,
59 | Unknown,
60 | }
61 |
62 | impl CursorShape {
63 | fn new(shape_code: &Value) -> Result {
64 | let str_code = shape_code
65 | .as_str()
66 | .ok_or_else(|| "Can't convert cursor shape to string".to_owned())?;
67 |
68 | Ok(match str_code {
69 | "block" => CursorShape::Block,
70 | "horizontal" => CursorShape::Horizontal,
71 | "vertical" => CursorShape::Vertical,
72 | _ => {
73 | error!("Unknown cursor_shape {}", str_code);
74 | CursorShape::Unknown
75 | }
76 | })
77 | }
78 | }
79 |
80 | #[derive(Debug, PartialEq, Eq, Clone)]
81 | pub struct ModeInfo {
82 | cursor_shape: Option,
83 | cell_percentage: Option,
84 | pub blinkwait: Option,
85 | }
86 |
87 | impl ModeInfo {
88 | pub fn new(mode_info_map: &HashMap) -> Result {
89 | let cursor_shape = if let Some(shape) = mode_info_map.get("cursor_shape") {
90 | Some(CursorShape::new(shape)?)
91 | } else {
92 | None
93 | };
94 |
95 | Ok(ModeInfo {
96 | cursor_shape,
97 | cell_percentage: mode_info_map
98 | .get("cell_percentage")
99 | .and_then(|cp| cp.as_u64()),
100 | blinkwait: mode_info_map
101 | .get("blinkwait")
102 | .and_then(|cp| cp.as_u64())
103 | .map(|v| v as u32),
104 | })
105 | }
106 |
107 | pub fn cursor_shape(&self) -> Option<&CursorShape> {
108 | self.cursor_shape.as_ref()
109 | }
110 |
111 | pub fn cell_percentage(&self) -> u64 {
112 | self.cell_percentage.unwrap_or(0)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/nvim/client.rs:
--------------------------------------------------------------------------------
1 | use std::{cell::RefCell, rc::Rc, sync::RwLock};
2 |
3 | use crate::nvim::*;
4 |
5 | #[derive(Default)]
6 | pub struct NeovimApiInfo {
7 | pub channel: i64,
8 |
9 | pub ext_cmdline: bool,
10 | pub ext_wildmenu: bool,
11 | pub ext_hlstate: bool,
12 | pub ext_linegrid: bool,
13 | pub ext_popupmenu: bool,
14 | pub ext_tabline: bool,
15 | pub ext_termcolors: bool,
16 |
17 | pub ui_pum_set_height: bool,
18 | pub ui_pum_set_bounds: bool,
19 | }
20 |
21 | impl NeovimApiInfo {
22 | pub fn new(api_info: Vec) -> Result {
23 | let mut self_ = Self::default();
24 | let mut api_info = api_info.into_iter();
25 |
26 | self_.channel = api_info
27 | .next()
28 | .ok_or("Channel is missing")?
29 | .as_i64()
30 | .ok_or("Channel is not i64")?;
31 |
32 | let metadata = match api_info.next().ok_or("Metadata is missing")? {
33 | Value::Map(pairs) => Ok(pairs),
34 | v => Err(format!("Metadata is wrong type, got {v:?}")),
35 | }?;
36 |
37 | for (key, value) in metadata.into_iter() {
38 | match key
39 | .as_str()
40 | .ok_or(format!("Metadata key {key:?} isn't string"))?
41 | {
42 | "ui_options" => self_.parse_ui_options(value)?,
43 | "functions" => self_.parse_functions(value)?,
44 | _ => (),
45 | }
46 | }
47 | Ok(self_)
48 | }
49 |
50 | #[inline]
51 | fn parse_ui_options(&mut self, extensions: Value) -> Result<(), String> {
52 | for extension in extensions
53 | .as_array()
54 | .ok_or(format!("UI option list is invalid: {extensions:?}"))?
55 | {
56 | match extension
57 | .as_str()
58 | .ok_or(format!("UI option isn't string: {extensions:?}"))?
59 | {
60 | "ext_cmdline" => self.ext_cmdline = true,
61 | "ext_wildmenu" => self.ext_wildmenu = true,
62 | "ext_hlstate" => self.ext_hlstate = true,
63 | "ext_linegrid" => self.ext_linegrid = true,
64 | "ext_popupmenu" => self.ext_popupmenu = true,
65 | "ext_tabline" => self.ext_tabline = true,
66 | "ext_termcolors" => self.ext_termcolors = true,
67 | _ => (),
68 | };
69 | }
70 | Ok(())
71 | }
72 |
73 | #[inline]
74 | fn parse_functions(&mut self, functions: Value) -> Result<(), String> {
75 | for function in functions
76 | .as_array()
77 | .ok_or_else(|| format!("Function list is not a list: {functions:?}"))?
78 | {
79 | match function
80 | .as_map()
81 | .ok_or_else(|| format!("Function info is not a map: {function:?}"))?
82 | .iter()
83 | .find_map(|(key, value)| {
84 | key.as_str()
85 | .filter(|k| *k == "name")
86 | .and_then(|_| value.as_str())
87 | })
88 | .ok_or_else(|| format!("Function info is missing name: {functions:?}"))?
89 | {
90 | "nvim_ui_pum_set_height" => self.ui_pum_set_height = true,
91 | "nvim_ui_pum_set_bounds" => self.ui_pum_set_bounds = true,
92 | _ => (),
93 | }
94 | }
95 | Ok(())
96 | }
97 | }
98 |
99 | #[derive(Clone, Copy, PartialEq)]
100 | enum NeovimClientStatus {
101 | Uninitialized,
102 | InitInProgress,
103 | Initialized,
104 | Error,
105 | }
106 |
107 | struct NeovimClientState {
108 | status: NeovimClientStatus,
109 | api_info: Option>,
110 | }
111 |
112 | pub struct NeovimClient {
113 | state: RefCell,
114 | nvim: RwLock