Merge pull request #63 from christopher-l/feature/headerbar-title

Show filename and dir in header bar
This commit is contained in:
daa84 2018-03-05 14:52:45 +03:00 committed by GitHub
commit 14bd3d3515
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 315 additions and 52 deletions

View File

@ -48,6 +48,7 @@ mod popup_menu;
mod project;
mod tabline;
mod error;
mod subscriptions;
use std::env;
use std::time::Duration;

View File

@ -83,6 +83,12 @@ impl NvimHandler {
error!("Unsupported event {:?}", params);
}
}
"subscription" => {
self.safe_call(move |ui| {
let ui = &ui.borrow();
ui.notify(params)
});
}
_ => {
error!("Notification {}({:?})", method, params);
}
@ -130,7 +136,7 @@ impl NvimHandler {
}
}
}
fn safe_call<F>(&self, cb: F)
where
F: FnOnce(&Arc<UiMutex<shell::State>>) -> result::Result<(), String> + 'static + Send,

View File

@ -59,7 +59,7 @@ pub struct Projects {
}
impl Projects {
pub fn new(ref_widget: &gtk::ToolButton, shell: Rc<RefCell<Shell>>) -> Rc<RefCell<Projects>> {
pub fn new(ref_widget: &gtk::Button, shell: Rc<RefCell<Shell>>) -> Rc<RefCell<Projects>> {
let projects = Projects {
shell,
popup: Popover::new(Some(ref_widget)),

View File

@ -33,6 +33,7 @@ use error;
use mode;
use render;
use render::CellMetrics;
use subscriptions::{SubscriptionHandle, Subscriptions};
const DEFAULT_FONT_NAME: &str = "DejaVu Sans Mono 12";
pub const MINIMUM_SUPPORTED_NVIM_VERSION: &str = "0.2.2";
@ -78,6 +79,8 @@ pub struct State {
detach_cb: Option<Box<RefCell<FnMut() + Send + 'static>>>,
nvim_started_cb: Option<Box<RefCell<FnMut() + Send + 'static>>>,
subscriptions: RefCell<Subscriptions>,
}
impl State {
@ -116,6 +119,8 @@ impl State {
detach_cb: None,
nvim_started_cb: None,
subscriptions: RefCell::new(Subscriptions::new()),
}
}
@ -328,6 +333,31 @@ impl State {
};
}
}
pub fn subscribe<F>(&self, event_name: &str, args: &[&str], cb: F) -> SubscriptionHandle
where
F: Fn(Vec<String>) + 'static,
{
self.subscriptions
.borrow_mut()
.subscribe(event_name, args, cb)
}
pub fn set_autocmds(&self) {
self.subscriptions
.borrow()
.set_autocmds(&mut self.nvim().unwrap());
}
pub fn notify(&self, params: Vec<Value>) -> Result<(), String> {
self.subscriptions.borrow().notify(params)
}
pub fn run_now(&self, handle: &SubscriptionHandle) {
self.subscriptions
.borrow()
.run_now(handle, &mut self.nvim().unwrap());
}
}
pub struct UiState {
@ -574,6 +604,15 @@ impl Shell {
}
}
pub fn new_tab(&self) {
let state = self.state.borrow();
let nvim = state.nvim();
if let Some(mut nvim) = nvim {
nvim.command(":tabe").report_err();
}
}
pub fn set_detach_cb<F>(&self, cb: Option<F>)
where
F: FnMut() + Send + 'static,

157
src/subscriptions.rs Normal file
View File

@ -0,0 +1,157 @@
use std::collections::HashMap;
use neovim_lib::{NeovimApi, Value};
use nvim::NeovimRef;
/// A subscription to a Neovim autocmd event.
struct Subscription {
/// A callback to be executed each time the event triggers.
cb: Box<Fn(Vec<String>) + 'static>,
/// A list of expressions which will be evaluated when the event triggers. The result is passed
/// to the callback.
args: Vec<String>,
}
/// A map of all registered subscriptions.
pub struct Subscriptions(HashMap<String, Vec<Subscription>>);
/// A handle to identify a `Subscription` within the `Subscriptions` map.
///
/// Can be used to trigger the subscription manually even when the event was not triggered.
///
/// Could be used in the future to suspend individual subscriptions.
#[derive(Debug)]
pub struct SubscriptionHandle {
event_name: String,
index: usize,
}
impl Subscriptions {
pub fn new() -> Self {
Subscriptions(HashMap::new())
}
/// Subscribe to a Neovim autocmd event.
///
/// Subscriptions are not active immediately but only after `set_autocmds` is called. At the
/// moment, all calls to `subscribe` must be made before calling `set_autocmds`.
///
/// This function is wrapped by `shell::State`.
///
/// # Arguments:
///
/// - `event_name`: The event to register.
/// See `:help autocmd-events` for a list of supported event names. Event names can be
/// comma-separated.
///
/// - `args`: A list of expressions to be evaluated when the event triggers.
/// Expressions are evaluated using Vimscript. The results are passed to the callback as a
/// list of Strings.
/// This is especially useful as `Neovim::eval` is synchronous and might block if called from
/// the callback function; so always use the `args` mechanism instead.
///
/// - `cb`: The callback function.
/// This will be called each time the event triggers or when `run_now` is called.
/// It is passed a vector with the results of the evaluated expressions given with `args`.
///
/// # Example
///
/// Call a function each time a buffer is entered or the current working directory is changed.
/// Pass the current buffer name and directory to the callback.
/// ```
/// let my_subscription = shell.state.borrow()
/// .subscribe("BufEnter,DirChanged", &["expand(@%)", "getcwd()"], move |args| {
/// let filename = &args[0];
/// let dir = &args[1];
/// // do stuff
/// });
/// ```
pub fn subscribe<F>(&mut self, event_name: &str, args: &[&str], cb: F) -> SubscriptionHandle
where
F: Fn(Vec<String>) + 'static,
{
let entry = self.0.entry(event_name.to_owned()).or_insert(Vec::new());
let index = entry.len();
entry.push(Subscription {
cb: Box::new(cb),
args: args.into_iter().map(|&s| s.to_owned()).collect(),
});
SubscriptionHandle {
event_name: event_name.to_owned(),
index,
}
}
/// Register all subscriptions with Neovim.
///
/// This function is wrapped by `shell::State`.
pub fn set_autocmds(&self, nvim: &mut NeovimRef) {
for (event_name, subscriptions) in &self.0 {
for (i, subscription) in subscriptions.iter().enumerate() {
let args = subscription
.args
.iter()
.fold("".to_owned(), |acc, arg| acc + ", " + &arg);
nvim.command(&format!(
"au {} * call rpcnotify(1, 'subscription', '{}', {} {})",
event_name, event_name, i, args,
)).unwrap_or_else(|err| {
error!("Could not set autocmd: {}", err);
});
}
}
}
/// Trigger given event.
fn on_notify(&self, event_name: &str, index: usize, args: Vec<String>) {
if let Some(subscription) = self.0.get(event_name).and_then(|v| v.get(index)) {
(*subscription.cb)(args);
}
}
/// Wrapper around `on_notify` for easy calling with a `neovim_lib::Handler` implementation.
///
/// This function is wrapped by `shell::State`.
pub fn notify(&self, params: Vec<Value>) -> Result<(), String> {
let mut params_iter = params.into_iter();
let ev_name = params_iter.next();
let ev_name = ev_name
.as_ref()
.and_then(Value::as_str)
.ok_or("Error reading event name")?;
let index = params_iter
.next()
.and_then(|i| i.as_u64())
.ok_or("Error reading index")? as usize;
let args = params_iter
.map(|arg| arg.as_str().map(|s| s.to_owned()))
.collect::<Option<Vec<String>>>()
.ok_or("Error reading args")?;
self.on_notify(ev_name, index, args);
Ok(())
}
/// Manually trigger the given subscription.
///
/// The `nvim` instance is needed to evaluate the `args` expressions.
///
/// This function is wrapped by `shell::State`.
pub fn run_now(&self, handle: &SubscriptionHandle, nvim: &mut NeovimRef) {
let subscription = &self.0.get(&handle.event_name).unwrap()[handle.index];
let args = subscription
.args
.iter()
.map(|arg| nvim.eval(arg))
.map(|res| {
res.ok()
.and_then(|val| val.as_str().map(|s: &str| s.to_owned()))
})
.collect::<Option<Vec<String>>>();
if let Some(args) = args {
self.on_notify(&handle.event_name, handle.index, args);
} else {
error!("Error manually running {:?}", handle);
}
}
}

160
src/ui.rs
View File

@ -1,13 +1,13 @@
use std::cell::{Ref, RefCell, RefMut};
use std::{env, thread};
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use gdk;
use gtk;
use gtk_sys;
use gtk::prelude::*;
use gtk::{AboutDialog, ApplicationWindow, HeaderBar, Image, SettingsExt, ToolButton};
use gtk::{AboutDialog, ApplicationWindow, Button, HeaderBar, SettingsExt};
use gio::prelude::*;
use gio::{Menu, MenuExt, MenuItem, SimpleAction};
use toml;
@ -17,6 +17,7 @@ use shell::{self, Shell, ShellOptions};
use shell_dlg;
use project::Projects;
use plug_manager;
use subscriptions::SubscriptionHandle;
macro_rules! clone {
(@param _) => ( _ );
@ -50,16 +51,24 @@ pub struct Ui {
pub struct Components {
window: Option<ApplicationWindow>,
window_state: WindowState,
open_btn: ToolButton,
open_btn: Button,
}
impl Components {
fn new() -> Components {
let save_image =
Image::new_from_icon_name("document-open", gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32);
let open_btn = Button::new();
let open_btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 3);
open_btn_box.pack_start(&gtk::Label::new("Open"), false, false, 3);
open_btn_box.pack_start(
&gtk::Image::new_from_icon_name("pan-down-symbolic", gtk::IconSize::Menu.into()),
false,
false,
3,
);
open_btn.add(&open_btn_box);
open_btn.set_can_focus(false);
Components {
open_btn: ToolButton::new(Some(&save_image), "Open"),
open_btn,
window: None,
window_state: WindowState::load(),
}
@ -126,48 +135,6 @@ impl Ui {
}
}
// Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1
let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR")
.map(|opt| opt.trim() != "1")
.unwrap_or(true);
if app.prefers_app_menu() || use_header_bar {
self.create_main_menu(app, &window);
}
if use_header_bar {
let header_bar = HeaderBar::new();
let projects = self.projects.clone();
header_bar.pack_start(&comps.open_btn);
comps
.open_btn
.connect_clicked(move |_| projects.borrow_mut().show());
let save_image = Image::new_from_icon_name(
"document-save",
gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32,
);
let save_btn = ToolButton::new(Some(&save_image), "Save");
let shell = self.shell.clone();
save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all());
header_bar.pack_start(&save_btn);
let paste_image = Image::new_from_icon_name(
"edit-paste",
gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32,
);
let paste_btn = ToolButton::new(Some(&paste_image), "Paste");
let shell = self.shell.clone();
paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste());
header_bar.pack_start(&paste_btn);
header_bar.set_show_close_button(true);
window.set_titlebar(Some(&header_bar));
}
if restore_win_state {
if comps.window_state.is_maximized {
window.maximize();
@ -182,6 +149,21 @@ impl Ui {
}
}
// Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1
let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR")
.map(|opt| opt.trim() != "1")
.unwrap_or(true);
if app.prefers_app_menu() || use_header_bar {
self.create_main_menu(app, &window);
}
let update_subtitle = if use_header_bar {
Some(self.create_header_bar())
} else {
None
};
let comps_ref = self.comps.clone();
window.connect_size_allocate(move |window, _| {
gtk_window_size_allocate(window, &mut *comps_ref.borrow_mut())
@ -202,7 +184,30 @@ impl Ui {
window.add(&**shell);
window.show_all();
window.set_title("NeovimGtk");
let comps_ref = self.comps.clone();
let update_title = shell.state.borrow().subscribe(
"BufEnter,DirChanged",
&["expand('%:p')", "getcwd()"],
move |args| {
let comps = comps_ref.borrow();
let window = comps.window.as_ref().unwrap();
let file_path = &args[0];
let dir = Path::new(&args[1]);
let filename = if file_path.is_empty() {
"[No Name]"
} else if let Some(rel_path) = Path::new(&file_path)
.strip_prefix(&dir)
.ok()
.and_then(|p| p.to_str())
{
rel_path
} else {
&file_path
};
window.set_title(filename);
},
);
let comps_ref = self.comps.clone();
let shell_ref = self.shell.clone();
@ -222,12 +227,67 @@ impl Ui {
let state_ref = self.shell.borrow().state.clone();
let plug_manager_ref = self.plug_manager.clone();
shell.set_nvim_started_cb(Some(move || {
let state = state_ref.borrow();
plug_manager_ref
.borrow_mut()
.init_nvim_client(state_ref.borrow().nvim_clone());
state.set_autocmds();
state.run_now(&update_title);
if let Some(ref update_subtitle) = update_subtitle {
state.run_now(&update_subtitle);
}
}));
}
fn create_header_bar(&self) -> SubscriptionHandle {
let header_bar = HeaderBar::new();
let comps = self.comps.borrow();
let window = comps.window.as_ref().unwrap();
let projects = self.projects.clone();
header_bar.pack_start(&comps.open_btn);
comps
.open_btn
.connect_clicked(move |_| projects.borrow_mut().show());
let new_tab_btn =
Button::new_from_icon_name("tab-new-symbolic", gtk::IconSize::SmallToolbar.into());
let shell_ref = Rc::clone(&self.shell);
new_tab_btn.connect_clicked(move |_| shell_ref.borrow_mut().new_tab());
new_tab_btn.set_can_focus(false);
new_tab_btn.set_tooltip_text("Open a new tab");
header_bar.pack_start(&new_tab_btn);
let paste_btn =
Button::new_from_icon_name("edit-paste-symbolic", gtk::IconSize::SmallToolbar.into());
let shell = self.shell.clone();
paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste());
paste_btn.set_can_focus(false);
paste_btn.set_tooltip_text("Paste from clipboard");
header_bar.pack_end(&paste_btn);
let save_btn = Button::new_with_label("Save All");
let shell = self.shell.clone();
save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all());
save_btn.set_can_focus(false);
header_bar.pack_end(&save_btn);
header_bar.set_show_close_button(true);
window.set_titlebar(Some(&header_bar));
let shell = self.shell.borrow();
let update_subtitle = shell.state.borrow().subscribe(
"DirChanged",
&["getcwd()"],
move |args| {
header_bar.set_subtitle(&*args[0]);
},
);
update_subtitle
}
fn create_main_menu(&self, app: &gtk::Application, window: &gtk::ApplicationWindow) {
let plug_manager = self.plug_manager.clone();