diff options
| author | denis <denismaximov98@gmail.com> | 2021-04-09 14:14:50 +0300 |
|---|---|---|
| committer | denis <denismaximov98@gmail.com> | 2021-04-09 14:14:50 +0300 |
| commit | 748a7ffa6ea4cf3b2093adb30e27bea9117d40d6 (patch) | |
| tree | 2e906db54b01ec1ce47f77ad2406690242d63c45 | |
| parent | wip: need to figure out how to clear lines (diff) | |
| parent | feat(controls): add quick navigation (#260) (diff) | |
| download | sl0ppij-748a7ffa6ea4cf3b2093adb30e27bea9117d40d6.tar.gz sl0ppij-748a7ffa6ea4cf3b2093adb30e27bea9117d40d6.tar.xz sl0ppij-748a7ffa6ea4cf3b2093adb30e27bea9117d40d6.zip | |
wip: cleaning up a bit, need to map the colors to the right places in the palette
27 files changed, 1518 insertions, 513 deletions
@@ -36,6 +36,44 @@ Zellij was initially called "Mosaic". The status bar on the bottom should guide you through the possible keyboard shortcuts in the app. +# Configuration +It is possible to configure keyboard shortcuts and their actions in a yaml file. +An example file can be found under `example/config.yaml`. + +Zellij will look for a file `/zellij/config.yaml` in the default configuration location of your os. + +To pass a config file directly to zellij run it either with: +`cargo run -- config [FILE]` or `zellij config [FILE]`. + +The structure is as follows: +``` +keybinds: + normal: + - action: [] + key: [] +``` +`normal` is one of the `modes` zellij can be in. +It is possible to bind a sequence of actions to numerous keys at the same time. +Here a reference to the [Key](https://docs.rs/termion/1.5.6/termion/event/enum.Key.html) format that is used. + +For example: +``` +keybinds: + normal: + - action: [ NewTab, GoToTab: 1,] + key: [ Char: 'c',] +``` +Will create a new tab and then switch to tab number 1 on pressing the +`c` key. +Whereas: +``` +keybinds: + normal: + - action: [ NewTab,] + key: [ Char: 'c', Char: 'd',] +``` +Will create a new tab on pressing either the `c` or the `d` key. + # What is the current status of the project? Zellij is in the last stages of being VT compatible. As much as modern terminals are. diff --git a/assets/completions/_zellij b/assets/completions/_zellij index 64bd538c..fc1cd00f 100644 --- a/assets/completions/_zellij +++ b/assets/completions/_zellij @@ -30,16 +30,93 @@ _zellij() { '--help[Prints help information]' \ '-V[Prints version information]' \ '--version[Prints version information]' \ +":: :_zellij_commands" \ +"*::: :->zellij" \ && ret=0 - + case $state in + (zellij) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:zellij-command-$line[1]:" + case $line[1] in + (c) +_arguments "${_arguments_options[@]}" \ +'--clean[Disables loading of configuration file at default location]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::path:_files' \ +&& ret=0 +;; +(c) +_arguments "${_arguments_options[@]}" \ +'--clean[Disables loading of configuration file at default location]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::path:_files' \ +&& ret=0 +;; +(config) +_arguments "${_arguments_options[@]}" \ +'--clean[Disables loading of configuration file at default location]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::path:_files' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; + esac + ;; +esac } (( $+functions[_zellij_commands] )) || _zellij_commands() { local commands; commands=( - + "config:Path to the configuration yaml file" \ +"help:Prints this message or the help of the given subcommand(s)" \ ) _describe -t commands 'zellij commands' commands "$@" } +(( $+functions[_c_commands] )) || +_c_commands() { + local commands; commands=( + + ) + _describe -t commands 'c commands' commands "$@" +} +(( $+functions[_zellij__c_commands] )) || +_zellij__c_commands() { + local commands; commands=( + + ) + _describe -t commands 'zellij c commands' commands "$@" +} +(( $+functions[_zellij__config_commands] )) || +_zellij__config_commands() { + local commands; commands=( + + ) + _describe -t commands 'zellij config commands' commands "$@" +} +(( $+functions[_zellij__help_commands] )) || +_zellij__help_commands() { + local commands; commands=( + + ) + _describe -t commands 'zellij help commands' commands "$@" +} _zellij "$@"
\ No newline at end of file diff --git a/assets/completions/zellij.bash b/assets/completions/zellij.bash index a921e7c9..10c18d53 100644 --- a/assets/completions/zellij.bash +++ b/assets/completions/zellij.bash @@ -13,6 +13,15 @@ _zellij() { cmd="zellij" ;; + c) + cmd+="__c" + ;; + config) + cmd+="__config" + ;; + help) + cmd+="__help" + ;; *) ;; esac @@ -20,7 +29,7 @@ _zellij() { case "${cmd}" in zellij) - opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout " + opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout config help c c" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -63,6 +72,51 @@ _zellij() { return 0 ;; + zellij__c) + opts=" -h -V --clean --help --version <path> " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zellij__config) + opts=" -h -V --clean --help --version <path> " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zellij__help) + opts=" -h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; esac } diff --git a/assets/completions/zellij.fish b/assets/completions/zellij.fish index e902823f..36bab192 100644 --- a/assets/completions/zellij.fish +++ b/assets/completions/zellij.fish @@ -6,3 +6,10 @@ complete -c zellij -n "__fish_use_subcommand" -s m -l move-focus -d 'Send "move complete -c zellij -n "__fish_use_subcommand" -s d -l debug complete -c zellij -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' complete -c zellij -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' +complete -c zellij -n "__fish_use_subcommand" -f -a "config" -d 'Path to the configuration yaml file' +complete -c zellij -n "__fish_use_subcommand" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)' +complete -c zellij -n "__fish_seen_subcommand_from config" -l clean -d 'Disables loading of configuration file at default location' +complete -c zellij -n "__fish_seen_subcommand_from config" -s h -l help -d 'Prints help information' +complete -c zellij -n "__fish_seen_subcommand_from config" -s V -l version -d 'Prints version information' +complete -c zellij -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information' +complete -c zellij -n "__fish_seen_subcommand_from help" -s V -l version -d 'Prints version information' diff --git a/default-tiles/status-bar/src/first_line.rs b/default-tiles/status-bar/src/first_line.rs index c30cd5f2..06914051 100644 --- a/default-tiles/status-bar/src/first_line.rs +++ b/default-tiles/status-bar/src/first_line.rs @@ -63,37 +63,51 @@ impl CtrlKeyShortcut { } } +fn get_bg(palette: Palette) -> (u8, u8, u8) { + match palette.theme { + Theme::Dark => palette.white, + Theme::Light => palette.fg, + } +} + +fn get_fg(palette: Palette) -> (u8, u8, u8) { + match palette.theme { + Theme::Dark => palette.black, + Theme::Light => palette.bg, + } +} + fn unselected_mode_shortcut(letter: char, text: &str, palette: Palette) -> LinePart { let prefix_separator = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); let char_left_separator = Style::new() .bold() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .bold() .paint(" <"); let char_shortcut = Style::new() .bold() .fg(RGB(palette.red.0, palette.red.1, palette.red.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .bold() .paint(letter.to_string()); let char_right_separator = Style::new() .bold() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .bold() .paint(">"); let styled_text = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .bold() .paint(format!("{} ", text)); let suffix_separator = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); LinePart { part: ANSIStrings(&[ @@ -111,8 +125,8 @@ fn unselected_mode_shortcut(letter: char, text: &str, palette: Palette) -> LineP fn selected_mode_shortcut(letter: char, text: &str, palette: Palette) -> LinePart { let prefix_separator = Style::new() - .fg(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .on(RGB(palette.green.0, palette.green.1, palette.green.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); let char_left_separator = Style::new() .bold() @@ -133,12 +147,12 @@ fn selected_mode_shortcut(letter: char, text: &str, palette: Palette) -> LinePar .bold() .paint(format!(">")); let styled_text = Style::new() - .fg(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) .on(RGB(palette.green.0, palette.green.1, palette.green.2)) .bold() .paint(format!("{} ", text)); let suffix_separator = Style::new() - .fg(RGB(palette.green.0, palette.green.1, palette.green.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) .on(RGB(palette.green.0, palette.green.1, palette.green.2)) .paint(ARROW_SEPARATOR); LinePart { @@ -157,17 +171,17 @@ fn selected_mode_shortcut(letter: char, text: &str, palette: Palette) -> LinePar fn disabled_mode_shortcut(text: &str, palette: Palette) -> LinePart { let prefix_separator = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); let styled_text = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .dimmed() .paint(format!("{} ", text)); let suffix_separator = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); LinePart { part: format!("{}{}{}", prefix_separator, styled_text, suffix_separator), @@ -179,7 +193,7 @@ fn selected_mode_shortcut_single_letter(letter: char, palette: Palette) -> LineP let char_shortcut_text = format!(" {} ", letter); let len = char_shortcut_text.chars().count() + 4; // 2 for the arrows, 2 for the padding let prefix_separator = Style::new() - .fg(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) .on(RGB(palette.green.0, palette.green.1, palette.green.2)) .paint(ARROW_SEPARATOR); let char_shortcut = Style::new() @@ -189,8 +203,8 @@ fn selected_mode_shortcut_single_letter(letter: char, palette: Palette) -> LineP .bold() .paint(char_shortcut_text); let suffix_separator = Style::new() - .fg(RGB(palette.green.0, palette.green.1, palette.green.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); LinePart { part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), @@ -202,18 +216,18 @@ fn unselected_mode_shortcut_single_letter(letter: char, palette: Palette) -> Lin let char_shortcut_text = format!(" {} ", letter); let len = char_shortcut_text.chars().count() + 4; // 2 for the arrows, 2 for the padding let prefix_separator = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); let char_shortcut = Style::new() .bold() .fg(RGB(palette.red.0, palette.red.1, palette.red.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .bold() .paint(char_shortcut_text); let suffix_separator = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .paint(ARROW_SEPARATOR); LinePart { part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), @@ -304,8 +318,8 @@ fn key_indicators(max_len: usize, keys: &[CtrlKeyShortcut], palette: Palette) -> pub fn superkey(palette: Palette) -> LinePart { let prefix_text = " Ctrl + "; let prefix = Style::new() - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) + .fg(RGB(get_fg(palette).0, get_fg(palette).1, get_fg(palette).2)) + .on(RGB(get_bg(palette).0, get_bg(palette).1, get_bg(palette).2)) .bold() .paint(prefix_text); LinePart { diff --git a/default-tiles/status-bar/src/main.rs b/default-tiles/status-bar/src/main.rs index 7e1cf2fb..b73de84d 100644 --- a/default-tiles/status-bar/src/main.rs +++ b/default-tiles/status-bar/src/main.rs @@ -2,7 +2,7 @@ mod first_line; mod second_line; use std::fmt::{Display, Error, Formatter}; -use zellij_tile::{prelude::*, data::Theme}; +use zellij_tile::{data::Theme, prelude::*}; use first_line::{ctrl_keys, superkey}; use second_line::keybinds; @@ -62,30 +62,15 @@ impl ZellijTile for State { let first_line = format!("{}{}", superkey, ctrl_keys); let second_line = keybinds(&self.mode_info, cols); - let first_line_color = match self.mode_info.palette.theme { - Theme::Light => self.mode_info.palette.black, - Theme::Dark => self.mode_info.palette.white, - }; - let second_line_color = match self.mode_info.palette.theme { - Theme::Light => self.mode_info.palette.bg, - Theme::Dark => self.mode_info.palette.bg, - }; - // [48;5;238m is gray background, [0K is so that it fills the rest of the line - // [48;5;16m is black background, [0K is so that it fills the rest of the line + // [m is background reset, [0K is so that it clears the rest of the line println!( - "{}\x1B[38;2;{};{};{}m\u{1b}[0K", + "{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", first_line, - first_line_color.0, - first_line_color.1, - first_line_color.2 - ); - println!( - "{}\u{1b}[{};{};{}m\u{1b}[0K", - second_line, - second_line_color.0, - second_line_color.1, - second_line_color.2 + self.mode_info.palette.fg.0, + self.mode_info.palette.fg.1, + self.mode_info.palette.fg.2 ); + println!("\u{1b}[m{}\u{1b}[0K", second_line); } } diff --git a/default-tiles/status-bar/src/second_line.rs b/default-tiles/status-bar/src/second_line.rs index 42d05c8b..bc050b5a 100644 --- a/default-tiles/status-bar/src/second_line.rs +++ b/default-tiles/status-bar/src/second_line.rs @@ -1,6 +1,6 @@ // use colored::*; use ansi_term::{ANSIStrings, Color::RGB, Style}; -use zellij_tile::prelude::*; +use zellij_tile::{data::Theme, prelude::*}; use crate::colors::{BLACK, GREEN, ORANGE, WHITE}; use crate::{LinePart, MORE_MSG}; @@ -13,13 +13,13 @@ fn full_length_shortcut( ) -> LinePart { let separator = if is_first_shortcut { " " } else { " / " }; let separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint(separator); let shortcut_len = letter.chars().count() + 3; // 2 for <>'s around shortcut, 1 for the space let shortcut_left_separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint("<"); let shortcut = Style::new() .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) @@ -27,13 +27,13 @@ fn full_length_shortcut( .bold() .paint(letter); let shortcut_right_separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint("> "); let description_len = description.chars().count(); let description = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .bold() .paint(description); let len = shortcut_len + description_len + separator.chars().count(); @@ -60,13 +60,13 @@ fn first_word_shortcut( ) -> LinePart { let separator = if is_first_shortcut { " " } else { " / " }; let separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint(separator); let shortcut_len = letter.chars().count() + 3; // 2 for <>'s around shortcut, 1 for the space let shortcut_left_separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint("<"); let shortcut = Style::new() .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) @@ -74,14 +74,14 @@ fn first_word_shortcut( .bold() .paint(letter); let shortcut_right_separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint("> "); let description_first_word = description.split(' ').next().unwrap_or(""); let description_first_word_length = description_first_word.chars().count(); let description_first_word = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .bold() .paint(description_first_word); let len = shortcut_len + description_first_word_length + separator.chars().count(); @@ -99,13 +99,129 @@ fn first_word_shortcut( len, } } +fn quicknav_full() -> LinePart { + let text_first_part = " Tip: "; + let alt = "Alt"; + let text_second_part = " + "; + let new_pane_shortcut = "n"; + let text_third_part = " => open new pane. "; + let second_alt = "Alt"; + let text_fourth_part = " + "; + let brackets_navigation = "[]"; + let text_fifth_part = " or "; + let hjkl_navigation = "hjkl"; + let text_sixths_part = " => navigate between panes."; + let len = text_first_part.chars().count() + + alt.chars().count() + + text_second_part.chars().count() + + new_pane_shortcut.chars().count() + + text_third_part.chars().count() + + second_alt.chars().count() + + text_fourth_part.chars().count() + + brackets_navigation.chars().count() + + text_fifth_part.chars().count() + + hjkl_navigation.chars().count() + + text_sixths_part.chars().count(); + LinePart { + part: format!( + "{}{}{}{}{}{}{}{}{}{}{}", + text_first_part, + Style::new().fg(ORANGE).bold().paint(alt), + text_second_part, + Style::new().fg(GREEN).bold().paint(new_pane_shortcut), + text_third_part, + Style::new().fg(ORANGE).bold().paint(second_alt), + text_fourth_part, + Style::new().fg(GREEN).bold().paint(brackets_navigation), + text_fifth_part, + Style::new().fg(GREEN).bold().paint(hjkl_navigation), + text_sixths_part, + ), + len, + } +} + +fn quicknav_medium() -> LinePart { + let text_first_part = " Tip: "; + let alt = "Alt"; + let text_second_part = " + "; + let new_pane_shortcut = "n"; + let text_third_part = " => new pane. "; + let second_alt = "Alt"; + let text_fourth_part = " + "; + let brackets_navigation = "[]"; + let text_fifth_part = " or "; + let hjkl_navigation = "hjkl"; + let text_sixths_part = " => navigate."; + let len = text_first_part.chars().count() + + alt.chars().count() + + text_second_part.chars().count() + + new_pane_shortcut.chars().count() + + text_third_part.chars().count() + + second_alt.chars().count() + + text_fourth_part.chars().count() + + brackets_navigation.chars().count() + + text_fifth_part.chars().count() + + hjkl_navigation.chars().count() + + text_sixths_part.chars().count(); + LinePart { + part: format!( + "{}{}{}{}{}{}{}{}{}{}{}", + text_first_part, + Style::new().fg(ORANGE).bold().paint(alt), + text_second_part, + Style::new().fg(GREEN).bold().paint(new_pane_shortcut), + text_third_part, + Style::new().fg(ORANGE).bold().paint(second_alt), + text_fourth_part, + Style::new().fg(GREEN).bold().paint(brackets_navigation), + text_fifth_part, + Style::new().fg(GREEN).bold().paint(hjkl_navigation), + text_sixths_part, + ), + len, + } +} + +fn quicknav_short() -> LinePart { + let text_first_part = " QuickNav: "; + let alt = "Alt"; + let text_second_part = " + "; + let new_pane_shortcut = "n"; + let text_third_part = "/"; + let brackets_navigation = "[]"; + let text_fifth_part = "/"; + let hjkl_navigation = "hjkl"; + let len = text_first_part.chars().count() + + alt.chars().count() + + text_second_part.chars().count() + + new_pane_shortcut.chars().count() + + text_third_part.chars().count() + + brackets_navigation.chars().count() + + text_fifth_part.chars().count() + + hjkl_navigation.chars().count(); + LinePart { + part: format!( + "{}{}{}{}{}{}{}{}", + text_first_part, + Style::new().fg(ORANGE).bold().paint(alt), + text_second_part, + Style::new().fg(GREEN).bold().paint(new_pane_shortcut), + text_third_part, + Style::new().fg(GREEN).bold().paint(brackets_navigation), + text_fifth_part, + Style::new().fg(GREEN).bold().paint(hjkl_navigation), + ), + len, + } +} fn locked_interface_indication(palette: Palette) -> LinePart { let locked_text = " -- INTERFACE LOCKED -- "; let locked_text_len = locked_text.chars().count(); let locked_styled_text = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .bold() .paint(locked_text); LinePart { @@ -119,13 +235,13 @@ fn select_pane_shortcut(is_first_shortcut: bool, palette: Palette) -> LinePart { let description = "Select pane"; let separator = if is_first_shortcut { " " } else { " / " }; let separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint(separator); let shortcut_len = shortcut.chars().count() + 3; // 2 for <>'s around shortcut, 1 for the space let shortcut_left_separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint("<"); let shortcut = Style::new() .on(RGB(palette.black.0, palette.black.1, palette.black.2)) @@ -133,13 +249,13 @@ fn select_pane_shortcut(is_first_shortcut: bool, palette: Palette) -> LinePart { .bold() .paint(shortcut); let shortcut_right_separator = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .paint("> "); let description_len = description.chars().count(); let description = Style::new() - .on(RGB(palette.bg.0, palette.bg.1, palette.bg.2)) - .fg(RGB(palette.fg.0, palette.fg.1, palette.fg.2)) + .on(RGB(palette.black.0, palette.black.1, palette.black.2)) + .fg(RGB(palette.white.0, palette.white.1, palette.white.2)) .bold() .paint(description); let len = shortcut_len + description_len + separator.chars().count(); @@ -160,7 +276,7 @@ fn select_pane_shortcut(is_first_shortcut: bool, palette: Palette) -> LinePart { fn full_shortcut_list(help: &ModeInfo) -> LinePart { match help.mode { - InputMode::Normal => LinePart::default(), + InputMode::Normal => quicknav_full(), InputMode::Locked => locked_interface_indication(help.palette), _ => { let mut line_part = LinePart::default(); @@ -179,7 +295,7 @@ fn full_shortcut_list(help: &ModeInfo) -> LinePart { fn shortened_shortcut_list(help: &ModeInfo) -> LinePart { match help.mode { - InputMode::Normal => LinePart::default(), + InputMode::Normal => quicknav_medium(), InputMode::Locked => locked_interface_indication(help.palette), _ => { let mut line_part = LinePart::default(); @@ -198,7 +314,14 @@ fn shortened_shortcut_list(help: &ModeInfo) -> LinePart { fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { match help.mode { - InputMode::Normal => LinePart::default(), + InputMode::Normal => { + let line_part = quicknav_short(); + if line_part.len <= max_len { + line_part + } else { + LinePart::default() + } + } InputMode::Locked => { let line_part = locked_interface_indication(help.palette); if line_part.len <= max_len { @@ -218,7 +341,7 @@ fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { break; } line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut,); + line_part.part = format!("{}{}", line_part.part, shortcut); } let select_pane_shortcut = select_pane_shortcut(help.keybinds.is_empty(), help.palette); if line_part.len + select_pane_shortcut.len <= max_len { diff --git a/default-tiles/strider/src/main.rs b/default-tiles/strider/src/main.rs index c2867fe4..71e2e5b7 100644 --- a/default-tiles/strider/src/main.rs +++ b/default-tiles/strider/src/main.rs @@ -23,7 +23,7 @@ impl ZellijTile for State { let next = self.selected().saturating_add(1); *self.selected_mut() = min(self.files.len() - 1, next); } - Key::Right | Key::Char('\n') | Key::Char('l') => { + Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => { match self.files[self.selected()].clone() { FsEntry::Dir(p, _) => { self.path = p; diff --git a/example/config.yaml b/example/config.yaml new file mode 100644 index 00000000..3bb4fbfa --- /dev/null +++ b/example/config.yaml @@ -0,0 +1,26 @@ +--- +keybinds: + normal: + - action: [GoToTab: 1,] + key: [F: 1,] + - action: [GoToTab: 2,] + key: [F: 2,] + - action: [GoToTab: 3,] + key: [F: 3,] + - action: [GoToTab: 4,] + key: [F: 4,] + - action: [NewTab,] + key: [F: 5,] + - action: [MoveFocus: Left,] + key: [ Alt: h,] + - action: [MoveFocus: Right,] + key: [ Alt: l,] + - action: [MoveFocus: Down,] + key: [ Alt: j,] + - action: [MoveFocus: Up,] + key: [ Alt: k,] + pane: + - action: [ NewPane:, SwitchToMode: Normal,] + key: [Char: 'n',] + - action: [ NewPane: , ] + key: [Char: 'N',] @@ -1,7 +1,7 @@ use std::path::PathBuf; use structopt::StructOpt; -#[derive(StructOpt, Debug, Default)] +#[derive(StructOpt, Default, Debug)] #[structopt(name = "zellij")] pub struct CliArgs { /// Send "split (direction h == horizontal / v == vertical)" to active zellij session @@ -24,6 +24,21 @@ pub struct CliArgs { #[structopt(short, long)] pub layout: Option<PathBuf>, + #[structopt(subcommand)] + pub config: Option<ConfigCli>, + #[structopt(short, long)] pub debug: bool, } + +#[derive(Debug, StructOpt)] +pub enum ConfigCli { + /// Path to the configuration yaml file + #[structopt(alias = "c")] + Config { + path: Option<PathBuf>, + #[structopt(long)] + /// Disables loading of configuration file at default location + clean: bool, + }, +} diff --git a/src/client/boundaries.rs b/src/client/boundaries.rs index cd489be0..7a6a2b2b 100644 --- a/src/client/boundaries.rs +++ b/src/client/boundaries.rs @@ -1,5 +1,5 @@ -use crate::common::colors; use crate::tab::Pane; +use crate::utils::shared::colors; use ansi_term::Colour::RGB; use std::collections::HashMap; use zellij_tile::data::{InputMode, Palette}; @@ -19,6 +19,13 @@ pub mod boundary_type { pub const CROSS: &str = "┼"; } +// pub mod colors { +// use ansi_term::Colour::{self, Fixed}; +// pub const GREEN: Colour = Fixed(154); +// pub const GRAY: Colour = Fixed(238); +// pub const ORANGE: Colour = Fixed(166); +// } + pub type BoundaryType = &'static str; // easy way to refer to boundary_type above #[derive(Clone, Copy, Debug)] diff --git a/src/client/panes/terminal_character.rs b/src/client/panes/terminal_character.rs index 053ebd3f..ce967ef8 100644 --- a/src/client/panes/terminal_character.rs +++ b/src/client/panes/terminal_character.rs @@ -37,6 +37,14 @@ pub enum NamedColor { Magenta, Cyan, White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, } impl NamedColor { @@ -50,6 +58,14 @@ impl NamedColor { NamedColor::Magenta => format!("{}", 35), NamedColor::Cyan => format!("{}", 36), NamedColor::White => format!("{}", 37), + NamedColor::BrightBlack => format!("{}", 90), + NamedColor::BrightRed => format!("{}", 91), + NamedColor::BrightGreen => format!("{}", 92), + NamedColor::BrightYellow => format!("{}", 93), + NamedColor::BrightBlue => format!("{}", 94), + NamedColor::BrightMagenta => format!("{}", 95), + NamedColor::BrightCyan => format!("{}", 96), + NamedColor::BrightWhite => format!("{}", 97), } } fn to_background_ansi_code(&self) -> String { @@ -62,6 +78,14 @@ impl NamedColor { NamedColor::Magenta => format!("{}", 45), NamedColor::Cyan => format!("{}", 46), NamedColor::White => format!("{}", 47), + NamedColor::BrightBlack => format!("{}", 100), + NamedColor::BrightRed => format!("{}", 101), + NamedColor::BrightGreen => format!("{}", 102), + NamedColor::BrightYellow => format!("{}", 103), + NamedColor::BrightBlue => format!("{}", 104), + NamedColor::BrightMagenta => format!("{}", 105), + NamedColor::BrightCyan => format!("{}", 106), + NamedColor::BrightWhite => format!("{}", 107), } } } @@ -383,6 +407,46 @@ impl CharacterStyles { params_used += 1; // even if it's a bug, let's not create an endless loop, eh? } [49, ..] => *self = self.background(Some(AnsiCode::Reset)), + [90, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightBlack))) + } + [91, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightRed))), + [92, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightGreen))) + } + [93, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightYellow))) + } + [94, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightBlue))), + [95, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightMagenta))) + } + [96, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightCyan))), + [97, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightWhite))) + } + [100, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightBlack))) + } + [101, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightRed))), + [102, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightGreen))) + } + [103, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightYellow))) + } + [104, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightBlue))) + } + [105, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightMagenta))) + } + [106, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightCyan))) + } + [107, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightWhite))) + } _ => { // if this happens, it's a bug let _ = debug_log_to_file(format!("unhandled csi m code {:?}", ansi_params)); diff --git a/src/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs index 463a33e3..13575475 100644 --- a/src/client/panes/terminal_pane.rs +++ b/src/client/panes/terminal_pane.rs @@ -380,8 +380,7 @@ impl TerminalPane { self.mark_for_rerender(); } fn add_newline(&mut self) { - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid.add_canonical_line(pad_character); self.mark_for_rerender(); } @@ -497,8 +496,7 @@ impl vte::Perform for TerminalPane { } else { (params[0] as usize - 1, params[1] as usize - 1) }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid.move_cursor_to(col, row, pad_character); } else if c == 'A' { // move cursor up until edge of screen @@ -507,8 +505,7 @@ impl vte::Perform for TerminalPane { } else if c == 'B' { // move cursor down until edge of screen let move_down_count = if params[0] == 0 { 1 } else { params[0] }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .move_cursor_down(move_down_count as usize, pad_character); } else if c == 'D' { @@ -600,8 +597,7 @@ impl vte::Perform for TerminalPane { } else { params[0] as usize }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .delete_lines_in_scroll_region(line_count_to_delete, pad_character); } else if c == 'L' { @@ -611,8 +607,7 @@ impl vte::Perform for TerminalPane { } else { params[0] as usize }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .add_empty_lines_in_scroll_region(line_count_to_add, pad_character); } else if c == 'q' { @@ -632,8 +627,7 @@ impl vte::Perform for TerminalPane { // minus 1 because this is 1 indexed params[0] as usize - 1 }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid.move_cursor_to_line(line, pad_character); } else if c == 'P' { // erase characters @@ -672,8 +666,7 @@ impl vte::Perform for TerminalPane { } else { params[0] as usize }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .delete_lines_in_scroll_region(count, pad_character); // TODO: since delete_lines_in_scroll_region also adds lines, is the below redundant? diff --git a/src/client/tab.rs b/src/client/tab.rs index 9f769252..08135499 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -17,7 +17,7 @@ use std::{ collections::{BTreeMap, HashSet}, }; use std::{io::Write, sync::mpsc::channel}; -use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, colors}; +use zellij_tile::data::{colors, Event, InputMode, ModeInfo, Palette}; const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this const MIN_TERMINAL_HEIGHT: usize = 2; @@ -71,7 +71,7 @@ pub struct Tab { should_clear_display_before_rendering: bool, pub mode_info: ModeInfo, pub input_mode: InputMode, - pub colors: Palette + pub colors: Palette, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -568,6 +568,9 @@ impl Tab { None } } + pub fn has_terminal_pid(&self, pid: RawFd) -> bool { + self.panes.contains_key(&PaneId::Terminal(pid)) + } pub fn handle_pty_event(&mut self, pid: RawFd, event: VteEvent) { // if we don't have the terminal in self.terminals it's probably because // of a race condition where the terminal was created in pty_bus but has not @@ -638,8 +641,17 @@ impl Tab { } }); self.panes_to_hide = pane_ids_to_hide.collect(); - let active_terminal = self.panes.get_mut(&active_pane_id).unwrap(); - active_terminal.override_size_and_position(expand_to.x, expand_to.y, &expand_to); + if self.panes_to_hide.is_empty() { + // nothing to do, pane is already as fullscreen as it can be, let's bail + return; + } else { + let active_terminal = self.panes.get_mut(&active_pane_id).unwrap(); + active_terminal.override_size_and_position( + expand_to.x, + expand_to.y, + &expand_to, + ); + } } let active_terminal = self.panes.get(&active_pane_id).unwrap(); if let PaneId::Terminal(active_pid) = active_pane_id { @@ -1804,6 +1816,62 @@ impl Tab { } self.render(); } + pub fn focus_next_pane(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_pane_id = self.get_active_pane_id().unwrap(); + let mut panes: Vec<(&PaneId, &Box<dyn Pane>)> = self.get_selectable_panes().collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let first_pane = panes.get(0).unwrap(); + let active_pane_position = panes + .iter() + .position(|(id, _)| *id == &active_pane_id) // TODO: better + .unwrap(); + if let Some(next_pane) = panes.get(active_pane_position + 1) { + self.active_terminal = Some(*next_pane.0); + } else { + self.active_terminal = Some(*first_pane.0); + } + self.render(); + } + pub fn focus_previous_pane(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_pane_id = self.get_active_pane_id().unwrap(); + let mut panes: Vec<(&PaneId, &Box<dyn Pane>)> = self.get_selectable_panes().collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let last_pane = panes.last().unwrap(); + let active_pane_position = panes + .iter() + .position(|(id, _)| *id == &active_pane_id) // TODO: better + .unwrap(); + if active_pane_position == 0 { + self.active_terminal = Some(*last_pane.0); + } else { + self.active_terminal = Some(*panes.get(active_pane_position - 1).unwrap().0); + } + self.render(); + } pub fn move_focus_left(&mut self) { if !self.has_selectable_panes() { return; @@ -2071,47 +2139,79 @@ impl Tab { } } pub fn close_pane_without_rerender(&mut self, id: PaneId) { - if let Some(terminal_to_close) = self.panes.get(&id) { - let terminal_to_close_width = terminal_to_close.columns(); - let terminal_to_close_height = terminal_to_close.rows(); - if let Some(terminals) = self.panes_to_the_left_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_width_right(&terminal_id, terminal_to_close_width + 1); - // 1 for the border - } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); - } - } else if let Some(terminals) = self.panes_to_the_right_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_width_left(&terminal_id, terminal_to_close_width + 1); - // 1 for the border - } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); - } - } else if let Some(terminals) = self.panes_above_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_height_down(&terminal_id, terminal_to_close_height + 1); - // 1 for the border + if self.fullscreen_is_active { + self.toggle_active_pane_fullscreen(); + } + if let Some(pane_to_close) = self.panes.get(&id) { + let pane_to_close_width = pane_to_close.columns(); + let pane_to_close_height = pane_to_close.rows(); + if let Some(panes) = self.panes_to_the_left_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_width_by(pane_to_close_width + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_width_right(&pane_id, pane_to_close_width + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); + } + if let Some(panes) = self.panes_to_the_right_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_width_by(pane_to_close_width + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_width_left(&pane_id, pane_to_close_width + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; } - } else if let Some(terminals) = self.panes_below_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_height_up(&terminal_id, terminal_to_close_height + 1); - // 1 for the border + } + if let Some(panes) = self.panes_above_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_height_by(pane_to_close_height + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_height_down(&pane_id, pane_to_close_height + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); + } + if let Some(panes) = self.panes_below_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_height_by(pane_to_close_height + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_height_up(&pane_id, pane_to_close_height + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; } - } else { } + // if we reached here, this is either the last pane or there's some sort of + // configuration error (eg. we're trying to close a pane surrounded by fixed panes) self.panes.remove(&id); - if self.active_terminal.is_none() { - self.active_terminal = self.next_active_pane(self.get_pane_ids()); - } } } pub fn close_focused_pane(&mut self) { diff --git a/src/common/command_is_executing.rs b/src/common/command_is_executing.rs index 93c44eb6..775a7bfc 100644 --- a/src/common/command_is_executing.rs +++ b/src/common/command_is_executing.rs @@ -23,7 +23,7 @@ impl CommandIsExecuting { let (lock, cvar) = &*self.closing_pane; let mut closing_pane = lock.lock().unwrap(); *closing_pane = false; - cvar.notify_one(); + cvar.notify_all(); } pub fn opening_new_pane(&mut self) { let (lock, _cvar) = &*self.opening_new_pane; @@ -34,7 +34,7 @@ impl CommandIsExecuting { let (lock, cvar) = &*self.opening_new_pane; let mut opening_new_pane = lock.lock().unwrap(); *opening_new_pane = false; - cvar.notify_one(); + cvar.notify_all(); } pub fn wait_until_pane_is_closed(&self) { let (lock, cvar) = &*self.closing_pane; diff --git a/src/common/errors.rs b/src/common/errors.rs index 00284cd4..1548aec0 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -178,7 +178,9 @@ pub enum ScreenContext { ResizeRight, ResizeDown, ResizeUp, - MoveFocus, + SwitchFocus, + FocusNextPane, + FocusPreviousPane, MoveFocusLeft, MoveFocusDown, MoveFocusUp, @@ -218,7 +220,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ResizeRight => ScreenContext::ResizeRight, ScreenInstruction::ResizeDown => ScreenContext::ResizeDown, ScreenInstruction::ResizeUp => ScreenContext::ResizeUp, - ScreenInstruction::MoveFocus => ScreenContext::MoveFocus, + ScreenInstruction::SwitchFocus => ScreenContext::SwitchFocus, + ScreenInstruction::FocusNextPane => ScreenContext::FocusNextPane, + ScreenInstruction::FocusPreviousPane => ScreenContext::FocusPreviousPane, ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft, ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown, ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp, diff --git a/src/common/input/actions.rs b/src/common/input/actions.rs index f4226864..47a5f763 100644 --- a/src/common/input/actions.rs +++ b/src/common/input/actions.rs @@ -1,9 +1,10 @@ //! Definition of the actions that can be bound to keys. +use serde::{Deserialize, Serialize}; use zellij_tile::data::InputMode; /// The four directions (left, right, up, down). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum Direction { Left, Right, @@ -12,7 +13,7 @@ pub enum Direction { } /// Actions that can be bound to keys. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum Action { /// Quit Zellij. Quit, @@ -23,8 +24,10 @@ pub enum Action { /// Resize focus pane in specified direction. Resize(Direction), /// Switch focus to next pane in specified direction. - SwitchFocus(Direction), + FocusNextPane, + FocusPreviousPane, /// Move the focus pane in specified direction. + SwitchFocus, MoveFocus(Direction), /// Scroll up in focus pane. ScrollUp, diff --git a/src/common/input/config.rs b/src/common/input/config.rs new file mode 100644 index 00000000..4bb9e10f --- /dev/null +++ b/src/common/input/config.rs @@ -0,0 +1,158 @@ +//! Deserializes configuration options. +use std::error; +use std::fmt::{self, Display}; +use std::fs::File; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use super::keybinds::{Keybinds, KeybindsFromYaml}; +use crate::cli::ConfigCli; + +use directories_next::ProjectDirs; +use serde::Deserialize; + +type ConfigResult = Result<Config, ConfigError>; + +/// Intermediate deserialisation config struct +#[derive(Debug, Deserialize)] +pub struct ConfigFromYaml { + pub keybinds: Option<KeybindsFromYaml>, +} + +/// Main configuration. +#[derive(Debug, Clone, PartialEq)] +pub struct Config { + pub keybinds: Keybinds, +} + +#[derive(Debug)] +pub enum ConfigError { + // Deserialisation error + Serde(serde_yaml::Error), + // Io error + Io(io::Error), + // Io error with path context + IoPath(io::Error, PathBuf), +} + +impl Default for Config { + fn default() -> Self { + let keybinds = Keybinds::default(); + Config { keybinds } + } +} + +impl Config { + /// Uses defaults, but lets config override them. + pub fn from_yaml(yaml_config: &str) -> ConfigResult { + let config_from_yaml: ConfigFromYaml = serde_yaml::from_str(&yaml_config)?; + let keybinds = Keybinds::get_default_keybinds_with_config(config_from_yaml.keybinds); + Ok(Config { keybinds }) + } + + /// Deserializes from given path. + #[allow(unused_must_use)] + pub fn new(path: &Path) -> ConfigResult { + match File::open(path) { + Ok(mut file) => { + let mut yaml_config = String::new(); + file.read_to_string(&mut yaml_config) + .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?; + Ok(Config::from_yaml(&yaml_config)?) + } + Err(e) => Err(ConfigError::IoPath(e, path.into())), + } + } + + /// Deserializes the config from a default platform specific path, + /// merges the default configuration - options take precedence. + fn from_default_path() -> ConfigResult { + let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); + let mut config_path: PathBuf = project_dirs.config_dir().to_owned(); + config_path.push("config.yaml"); + + match Config::new(&config_path) { + Ok(config) => Ok(config), + Err(ConfigError::IoPath(_, _)) => Ok(Config::default()), + Err(e) => Err(e), + } + } + + /// Entry point of the configuration + #[cfg(not(test))] + pub fn from_cli_config(cli_config: Option<ConfigCli>) -> ConfigResult { + match cli_config { + Some(ConfigCli::Config { clean, .. }) if clean => Ok(Config::default()), + Some(ConfigCli::Config { path, .. }) if path.is_some() => { + Ok(Config::new(&path.unwrap())?) + } + Some(_) | None => Ok(Config::from_default_path()?), + } + } + + //#[allow(unused_must_use)] + /// In order not to mess up tests from changing configurations + #[cfg(test)] + pub fn from_cli_config(_: Option<ConfigCli>) -> ConfigResult { + Ok(Config::default()) + } +} + +impl Display for ConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + ConfigError::Io(ref err) => write!(formatter, "IoError: {}", err), + ConfigError::IoPath(ref err, ref path) => { + write!(formatter, "IoError: {}, File: {}", err, path.display(),) + } + ConfigError::Serde(ref err) => write!(formatter, "Deserialisation error: {}", err), + } + } +} + +impl std::error::Error for ConfigError { + fn cause(&self) -> Option<&dyn error::Error> { + match *self { + ConfigError::Io(ref err) => Some(err), + ConfigError::IoPath(ref err, _) => Some(err), + ConfigError::Serde(ref err) => Some(err), + } + } +} + +impl From<io::Error> for ConfigError { + fn from(err: io::Error) -> ConfigError { + ConfigError::Io(err) + } +} + +impl From<serde_yaml::Error> for ConfigError { + fn from(err: serde_yaml::Error) -> ConfigError { + ConfigError::Serde(err) + } +} + +// The unit test location. +#[cfg(test)] +mod config_test { + use super::*; + + #[test] + fn clean_option_equals_default_config() { + let no_file = PathBuf::from(r"../fixtures/config/config.yamlll"); + let cli_config = ConfigCli::Config { + path: Some(no_file), + clean: true, + }; + let config = Config::from_cli_config(Some(cli_config)).unwrap(); + let default = Config::default(); + assert_eq!(config, default); + } + + #[test] + fn no_config_option_file_equals_default_config() { + let config = Config::from_cli_config(None).unwrap(); + let default = Config::default(); + assert_eq!(config, default); + } +} diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 1ed9169e..2484f08c 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -1,8 +1,9 @@ //! Main input logic. use super::actions::Action; -use super::keybinds::get_default_keybinds; -use crate::common::{load_palette, AppInstruction, SenderWithContext, OPENCALLS}; +use super::keybinds::Keybinds; +use crate::common::input::config::Config; +use crate::common::{AppInstruction, SenderWithContext, OPENCALLS}; use crate::errors::ContextType; use crate::os_input_output::OsApi; use crate::pty_bus::PtyInstruction; @@ -10,22 +11,23 @@ use crate::screen::ScreenInstruction; use crate::wasm_vm::PluginInstruction; use crate::CommandIsExecuting; +use crate::common::utils::shared::load_palette; use termion::input::{TermRead, TermReadEventsAndRaw}; use zellij_tile::data::{Event, InputMode, Key, ModeInfo}; -use super::keybinds::key_to_actions; - /// Handles the dispatching of [`Action`]s according to the current /// [`InputMode`], and keep tracks of the current [`InputMode`]. struct InputHandler { /// The current input mode mode: InputMode, os_input: Box<dyn OsApi>, + config: Config, command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext<ScreenInstruction>, send_pty_instructions: SenderWithContext<PtyInstruction>, send_plugin_instructions: SenderWithContext<PluginInstruction>, send_app_instructions: SenderWithContext<AppInstruction>, + should_exit: bool, } impl InputHandler { @@ -33,6 +35,7 @@ impl InputHandler { fn new( os_input: Box<dyn OsApi>, command_is_executing: CommandIsExecuting, + config: Config, send_screen_instructions: SenderWithContext<ScreenInstruction>, send_pty_instructions: SenderWithContext<PtyInstruction>, send_plugin_instructions: SenderWithContext<PluginInstruction>, @@ -41,11 +44,13 @@ impl InputHandler { InputHandler { mode: InputMode::Normal, os_input, + config, command_is_executing, send_screen_instructions, send_pty_instructions, send_plugin_instructions, send_app_instructions, + should_exit: false, } } @@ -57,41 +62,44 @@ impl InputHandler { self.send_pty_instructions.update(err_ctx); self.send_app_instructions.update(err_ctx); self.send_screen_instructions.update(err_ctx); - if let Ok(keybinds) = get_default_keybinds() { - 'input_loop: loop { - //@@@ I think this should actually just iterate over stdin directly - let stdin_buffer = self.os_input.read_from_stdin(); - for key_result in stdin_buffer.events_and_raw() { - match key_result { - Ok((event, raw_bytes)) => match event { - termion::event::Event::Key(key) => { - let key = cast_termion_key(key); - // FIXME this explicit break is needed because the current test - // framework relies on it to not create dead threads that loop - // and eat up CPUs. Do not remove until the test framework has - // been revised. Sorry about this (@categorille) - let mut should_break = false; - for action in key_to_actions(&key, raw_bytes, &self.mode, &keybinds) - { - should_break |= self.dispatch_action(action); - } - if should_break { - break 'input_loop; - } - } - termion::event::Event::Mouse(_) - | termion::event::Event::Unsupported(_) => { - // Mouse and unsupported events aren't implemented yet, - // use a NoOp untill then. + let keybinds = self.config.keybinds.clone(); + let alt_left_bracket = vec![27, 91]; + loop { + if self.should_exit { + break; + } + let stdin_buffer = self.os_input.read_from_stdin(); + for key_result in stdin_buffer.events_and_raw() { + match key_result { + Ok((event, raw_bytes)) => match event { + termion::event::Event::Key(key) => { + let key = cast_termion_key(key); + self.handle_key(&key, raw_bytes, &keybinds); + } + termion::event::Event::Unsupported(unsupported_key) => { + // we have to do this because of a bug in termion + // this should be a key event and not an unsupported event + if unsupported_key == alt_left_bracket { + let key = Key::Alt('['); + self.handle_key(&key, raw_bytes, &keybinds); } - }, - Err(err) => panic!("Encountered read error: {:?}", err), - } + } + termion::event::Event::Mouse(_) => { + // Mouse events aren't implemented yet, + // use a NoOp untill then. + } + }, + Err(err) => panic!("Encountered read error: {:?}", err), } } - } else { - //@@@ Error handling? - self.exit(); + } + } + fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>, keybinds: &Keybinds) { + for action in Keybinds::key_to_actions(&key, raw_bytes, &self.mode, &keybinds) { + let should_exit = self.dispatch_action(action); + if should_exit { + self.should_exit = true; + } } } @@ -146,9 +154,19 @@ impl InputHandler { }; self.send_screen_instructions.send(screen_instr).unwrap(); } - Action::SwitchFocus(_) => { + Action::SwitchFocus => { + self.send_screen_instructions + .send(ScreenInstruction::SwitchFocus) + .unwrap(); + } + Action::FocusNextPane => { + self.send_screen_instructions + .send(ScreenInstruction::FocusNextPane) + .unwrap(); + } + Action::FocusPreviousPane => { self.send_screen_instructions - .send(ScreenInstruction::MoveFocus) + .send(ScreenInstruction::FocusPreviousPane) .unwrap(); } Action::MoveFocus(direction) => { @@ -296,6 +314,7 @@ pub fn get_mode_info(mode: InputMode) -> ModeInfo { /// its [`InputHandler::handle_input()`] loop. pub fn input_loop( os_input: Box<dyn OsApi>, + config: Config, command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext<ScreenInstruction>, send_pty_instructions: SenderWithContext<PtyInstruction>, @@ -305,6 +324,7 @@ pub fn input_loop( let _handler = InputHandler::new( os_input, command_is_executing, + config, send_screen_instructions, send_pty_instructions, send_plugin_instructions, diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 7b1e8779..8a064970 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -1,268 +1,415 @@ //! Mapping of inputs to sequences of actions. +use std::collections::HashMap; use super::actions::{Action, Direction}; -use std::collections::HashMap; - +use serde::Deserialize; use strum::IntoEnumIterator; use zellij_tile::data::*; -type Keybinds = HashMap<InputMode, ModeKeybinds>; -type ModeKeybinds = HashMap<Key, Vec<Action>>; +#[derive(Clone, Debug, PartialEq)] +pub struct Keybinds(HashMap<InputMode, ModeKeybinds>); +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ModeKeybinds(HashMap<Key, Vec<Action>>); -/// Populates the default hashmap of keybinds. -/// @@@khs26 What about an input config file? -pub fn get_default_keybinds() -> Result<Keybinds, String> { - let mut defaults = Keybinds::new(); +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct KeybindsFromYaml(HashMap<InputMode, Vec<KeyActionFromYaml>>); - for mode in InputMode::iter() { - defaults.insert(mode, get_defaults_for_mode(&mode)); - } - - Ok(defaults) +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct KeyActionFromYaml { + action: Vec<Action>, + key: Vec<Key>, } -/// Returns the default keybinds for a givent [`InputMode`]. -fn get_defaults_for_mode(mode: &InputMode) -> ModeKeybinds { - let mut defaults = ModeKeybinds::new(); +impl Default for Keybinds { + fn default() -> Keybinds { + let mut defaults = Keybinds::new(); - match *mode { - InputMode::Normal => { - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Locked)], - ); - defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); - defaults.insert( - Key::Ctrl('r'), - vec![Action::SwitchToMode(InputMode::Resize)], - ); - defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); - defaults.insert( - Key::Ctrl('s'), - vec![Action::SwitchToMode(InputMode::Scroll)], - ); - defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); + for mode in InputMode::iter() { + defaults + .0 + .insert(mode, Keybinds::get_defaults_for_mode(&mode)); } - InputMode::Locked => { - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); + defaults + } +} + +impl Keybinds { + pub fn new() -> Keybinds { + Keybinds(HashMap::<InputMode, ModeKeybinds>::new()) + } + + pub fn get_default_keybinds_with_config(keybinds: Option<KeybindsFromYaml>) -> Keybinds { + let default_keybinds = Keybinds::default(); + if let Some(keybinds) = keybinds { + default_keybinds.merge_keybinds(Keybinds::from(keybinds)) + } else { + default_keybinds } - InputMode::Resize => { - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Locked)], - ); - defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); - defaults.insert( - Key::Ctrl('r'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); - defaults.insert( - Key::Ctrl('s'), - vec![Action::SwitchToMode(InputMode::Scroll)], - ); - defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); - defaults.insert( - Key::Char('\n'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert( - Key::Char(' '), - vec![Action::SwitchToMode(InputMode::Normal)], - ); + } - defaults.insert(Key::Char('h'), vec![Action::Resize(Direction::Left)]); - defaults.insert(Key::Char('j'), vec![Action::Resize(Direction::Down)]); - defaults.insert(Key::Char('k'), vec![Action::Resize(Direction::Up)]); - defaults.insert(Key::Char('l'), vec![Action::Resize(Direction::Right)]); + /// Merges two Keybinds structs into one Keybinds struct + /// `other` overrides the ModeKeybinds of `self`. + fn merge_keybinds(&self, other: Keybinds) -> Keybinds { + let mut keybinds = Keybinds::new(); - defaults.insert(Key::Left, vec![Action::Resize(Direction::Left)]); - defaults.insert(Key::Down, vec![Action::Resize(Direction::Down)]); - defaults.insert(Key::Up, vec![Action::Resize(Direction::Up)]); - defaults.insert(Key::Right, vec![Action::Resize(Direction::Right)]); + for mode in InputMode::iter() { + let mut mode_keybinds = ModeKeybinds::new(); + if let Some(keybind) = self.0.get(&mode) { + mode_keybinds.0.extend(keybind.0.clone()); + }; + if let Some(keybind) = other.0.get(&mode) { + mode_keybinds.0.extend(keybind.0.clone()); + } + if !mode_keybinds.0.is_empty() { + keybinds.0.insert(mode, mode_keybinds); + } } - InputMode::Pane => { - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Locked)], - ); - defaults.insert( - Key::Ctrl('p'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert( - Key::Ctrl('r'), - vec![Action::SwitchToMode(InputMode::Resize)], - ); - defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); - defaults.insert( - Key::Ctrl('s'), - vec![Action::SwitchToMode(InputMode::Scroll)], - ); - defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); - defaults.insert( - Key::Char('\n'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert( - Key::Char(' '), - vec![Action::SwitchToMode(InputMode::Normal)], - ); + keybinds + } - defaults.insert(Key::Char('h'), vec![Action::MoveFocus(Direction::Left)]); - defaults.insert(Key::Char('j'), vec![Action::MoveFocus(Direction::Down)]); - defaults.insert(Key::Char('k'), vec![Action::MoveFocus(Direction::Up)]); - defaults.insert(Key::Char('l'), vec![Action::MoveFocus(Direction::Right)]); + /// Returns the default keybinds for a given [`InputMode`]. + fn get_defaults_for_mode(mode: &InputMode) -> ModeKeybinds { + let mut defaults = HashMap::new(); - defaults.insert(Key::Left, vec![Action::MoveFocus(Direction::Left)]); - defaults.insert(Key::Down, vec![Action::MoveFocus(Direction::Down)]); - defaults.insert(Key::Up, vec![Action::MoveFocus(Direction::Up)]); - defaults.insert(Key::Right, vec![Action::MoveFocus(Direction::Right)]); + match *mode { + InputMode::Normal => { + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Locked)], + ); + defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); + defaults.insert( + Key::Ctrl('r'), + vec![Action::SwitchToMode(InputMode::Resize)], + ); + defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); + defaults.insert( + Key::Ctrl('s'), + vec![Action::SwitchToMode(InputMode::Scroll)], + ); + defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); - defaults.insert(Key::Char('p'), vec![Action::SwitchFocus(Direction::Right)]); - defaults.insert(Key::Char('n'), vec![Action::NewPane(None)]); - defaults.insert(Key::Char('d'), vec![Action::NewPane(Some(Direction::Down))]); - defaults.insert( - Key::Char('r'), - vec![Action::NewPane(Some(Direction::Right))], - ); - defaults.insert(Key::Char('x'), vec![Action::CloseFocus]); - defaults.insert(Key::Char('f'), vec![Action::ToggleFocusFullscreen]); - } - InputMode::Tab => { - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Locked)], - ); - defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); - defaults.insert( - Key::Ctrl('r'), - vec![Action::SwitchToMode(InputMode::Resize)], - ); - defaults.insert( - Key::Ctrl('t'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert( - Key::Ctrl('s'), - vec![Action::SwitchToMode(InputMode::Scroll)], - ); - defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); - defaults.insert( - Key::Char('\n'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert( - Key::Char(' '), - vec![Action::SwitchToMode(InputMode::Normal)], - ); + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); + } + InputMode::Locked => { + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + } + InputMode::Resize => { + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Locked)], + ); + defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); + defaults.insert( + Key::Ctrl('r'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); + defaults.insert( + Key::Ctrl('s'), + vec![Action::SwitchToMode(InputMode::Scroll)], + ); + defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); + defaults.insert( + Key::Char('\n'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert( + Key::Char(' '), + vec![Action::SwitchToMode(InputMode::Normal)], + ); - defaults.insert(Key::Char('h'), vec![Action::GoToPreviousTab]); - defaults.insert(Key::Char('j'), vec![Action::GoToNextTab]); - defaults.insert(Key::Char('k'), vec![Action::GoToPreviousTab]); - defaults.insert(Key::Char('l'), vec![Action::GoToNextTab]); + defaults.insert(Key::Char('h'), vec![Action::Resize(Direction::Left)]); + defaults.insert(Key::Char('j'), vec![Action::Resize(Direction::Down)]); + defaults.insert(Key::Char('k'), vec![Action::Resize(Direction::Up)]); + defaults.insert(Key::Char('l'), vec![Action::Resize(Direction::Right)]); - defaults.insert(Key::Left, vec![Action::GoToPreviousTab]); - defaults.insert(Key::Down, vec![Action::GoToNextTab]); - defaults.insert(Key::Up, vec![Action::GoToPreviousTab]); - defaults.insert(Key::Right, vec![Action::GoToNextTab]); + defaults.insert(Key::Left, vec![Action::Resize(Direction::Left)]); + defaults.insert(Key::Down, vec![Action::Resize(Direction::Down)]); + defaults.insert(Key::Up, vec![Action::Resize(Direction::Up)]); + defaults.insert(Key::Right, vec![Action::Resize(Direction::Right)]); - defaults.insert(Key::Char('n'), vec![Action::NewTab]); - defaults.insert(Key::Char('x'), vec![Action::CloseTab]); + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); + } + InputMode::Pane => { + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Locked)], + ); + defaults.insert( + Key::Ctrl('p'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert( + Key::Ctrl('r'), + vec![Action::SwitchToMode(InputMode::Resize)], + ); + defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); + defaults.insert( + Key::Ctrl('s'), + vec![Action::SwitchToMode(InputMode::Scroll)], + ); + defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); + defaults.insert( + Key::Char('\n'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert( + Key::Char(' '), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + + defaults.insert(Key::Char('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Char('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Char('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Char('l'), vec![Action::MoveFocus(Direction::Right)]); + + defaults.insert(Key::Left, vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Down, vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Up, vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Right, vec![Action::MoveFocus(Direction::Right)]); - defaults.insert( - Key::Char('r'), - vec![ - Action::SwitchToMode(InputMode::RenameTab), - Action::TabNameInput(vec![0]), - ], - ); - defaults.insert(Key::Char('q'), vec![Action::Quit]); - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - for i in '1'..='9' { - defaults.insert(Key::Char(i), vec![Action::GoToTab(i.to_digit(10).unwrap())]); + defaults.insert(Key::Char('p'), vec![Action::SwitchFocus]); + defaults.insert(Key::Char('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Char('d'), vec![Action::NewPane(Some(Direction::Down))]); + defaults.insert( + Key::Char('r'), + vec![Action::NewPane(Some(Direction::Right))], + ); + defaults.insert(Key::Char('x'), vec![Action::CloseFocus]); + defaults.insert(Key::Char('f'), vec![Action::ToggleFocusFullscreen]); + + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); } - } - InputMode::Scroll => { - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Locked)], - ); - defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); - defaults.insert( - Key::Ctrl('r'), - vec![Action::SwitchToMode(InputMode::Resize)], - ); - defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); - defaults.insert( - Key::Ctrl('s'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); - defaults.insert( - Key::Char('\n'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert( - Key::Char(' '), - vec![Action::SwitchToMode(InputMode::Normal)], - ); + InputMode::Tab => { + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Locked)], + ); + defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); + defaults.insert( + Key::Ctrl('r'), + vec![Action::SwitchToMode(InputMode::Resize)], + ); + defaults.insert( + Key::Ctrl('t'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert( + Key::Ctrl('s'), + vec![Action::SwitchToMode(InputMode::Scroll)], + ); + defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); + defaults.insert( + Key::Char('\n'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert( + Key::Char(' '), + vec![Action::SwitchToMode(InputMode::Normal)], + ); - defaults.insert(Key::Char('j'), vec![Action::ScrollDown]); - defaults.insert(Key::Char('k'), vec![Action::ScrollUp]); + defaults.insert(Key::Char('h'), vec![Action::GoToPreviousTab]); + defaults.insert(Key::Char('j'), vec![Action::GoToNextTab]); + defaults.insert(Key::Char('k'), vec![Action::GoToPreviousTab]); + defaults.insert(Key::Char('l'), vec![Action::GoToNextTab]); - defaults.insert(Key::Down, vec![Action::ScrollDown]); - defaults.insert(Key::Up, vec![Action::ScrollUp]); + defaults.insert(Key::Left, vec![Action::GoToPreviousTab]); + defaults.insert(Key::Down, vec![Action::GoToNextTab]); + defaults.insert(Key::Up, vec![Action::GoToPreviousTab]); + defaults.insert(Key::Right, vec![Action::GoToNextTab]); + + defaults.insert(Key::Char('n'), vec![Action::NewTab]); + defaults.insert(Key::Char('x'), vec![Action::CloseTab]); + + defaults.insert( + Key::Char('r'), + vec![ + Action::SwitchToMode(InputMode::RenameTab), + Action::TabNameInput(vec![0]), + ], + ); + defaults.insert(Key::Char('q'), vec![Action::Quit]); + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + for i in '1'..='9' { + defaults.insert(Key::Char(i), vec![Action::GoToTab(i.to_digit(10).unwrap())]); + } + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); + } + InputMode::Scroll => { + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Locked)], + ); + defaults.insert(Key::Ctrl('p'), vec![Action::SwitchToMode(InputMode::Pane)]); + defaults.insert( + Key::Ctrl('r'), + vec![Action::SwitchToMode(InputMode::Resize)], + ); + defaults.insert(Key::Ctrl('t'), vec![Action::SwitchToMode(InputMode::Tab)]); + defaults.insert( + Key::Ctrl('s'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); + defaults.insert( + Key::Char('\n'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert( + Key::Char(' '), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + + defaults.insert(Key::Char('j'), vec![Action::ScrollDown]); + defaults.insert(Key::Char('k'), vec![Action::ScrollUp]); + + defaults.insert(Key::Down, vec![Action::ScrollDown]); + defaults.insert(Key::Up, vec![Action::ScrollUp]); + + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); + } + InputMode::RenameTab => { + defaults.insert(Key::Char('\n'), vec![Action::SwitchToMode(InputMode::Tab)]); + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert( + Key::Esc, + vec![ + Action::TabNameInput(vec![0x1b]), + Action::SwitchToMode(InputMode::Tab), + ], + ); + + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); + } } - InputMode::RenameTab => { - defaults.insert(Key::Char('\n'), vec![Action::SwitchToMode(InputMode::Tab)]); - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert( - Key::Esc, - vec![ - Action::TabNameInput(vec![0x1b]), - Action::SwitchToMode(InputMode::Tab), - ], - ); + ModeKeybinds(defaults) + } + + /// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current + /// [`InputMode`] and [`Keybinds`]. + pub fn key_to_actions( + key: &Key, + input: Vec<u8>, + mode: &InputMode, + keybinds: &Keybinds, + ) -> Vec<Action> { + let mode_keybind_or_action = |action: Action| { + keybinds + .0 + .get(mode) + .unwrap_or_else(|| unreachable!("Unrecognized mode: {:?}", mode)) + .0 + .get(key) + .cloned() + .unwrap_or_else(|| vec![action]) + }; + match *mode { + InputMode::Normal | InputMode::Locked => mode_keybind_or_action(Action::Write(input)), + InputMode::RenameTab => mode_keybind_or_action(Action::TabNameInput(input)), + _ => mode_keybind_or_action(Action::NoOp), } } +} - defaults +impl ModeKeybinds { + fn new() -> ModeKeybinds { + ModeKeybinds(HashMap::<Key, Vec<Action>>::new()) + } + + /// Merges `self` with `other`, if keys are the same, `other` overwrites. + fn merge(self, other: ModeKeybinds) -> ModeKeybinds { + let mut merged = self; + merged.0.extend(other.0); + merged + } } -/// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current -/// [`InputMode`] and [`Keybinds`]. -pub fn key_to_actions( - key: &Key, - input: Vec<u8>, - mode: &InputMode, - keybinds: &Keybinds, -) -> Vec<Action> { - let mode_keybind_or_action = |action: Action| { +impl From<KeybindsFromYaml> for Keybinds { + fn from(keybinds_from_yaml: KeybindsFromYaml) -> Keybinds { + let mut keybinds = Keybinds::new(); + + for mode in InputMode::iter() { + let mut mode_keybinds = ModeKeybinds::new(); + for key_action in keybinds_from_yaml.0.get(&mode).iter() { + for keybind in key_action.iter() { + mode_keybinds = mode_keybinds.merge(ModeKeybinds::from(keybind.clone())); + } + } + keybinds.0.insert(mode, mode_keybinds); + } keybinds - .get(mode) - .unwrap_or_else(|| unreachable!("Unrecognized mode: {:?}", mode)) - .get(key) - .cloned() - .unwrap_or_else(|| vec![action]) - }; - match *mode { - InputMode::Normal | InputMode::Locked => mode_keybind_or_action(Action::Write(input)), - InputMode::RenameTab => mode_keybind_or_action(Action::TabNameInput(input)), - _ => mode_keybind_or_action(Action::NoOp), } } + +/// For each `Key` assigned to `Action`s, +/// map the `Action`s to the key +impl From<KeyActionFromYaml> for ModeKeybinds { + fn from(key_action: KeyActionFromYaml) -> ModeKeybinds { + let keys = key_action.key; + let actions = key_action.action; + + ModeKeybinds( + keys.into_iter() + .map(|k| (k, actions.clone())) + .collect::<HashMap<Key, Vec<Action>>>(), + ) + } +} + +// The unit test location. +#[cfg(test)] +#[path = "./unit/keybinds_test.rs"] +mod keybinds_test; diff --git a/src/common/input/mod.rs b/src/common/input/mod.rs index 20c30dae..6589cac2 100644 --- a/src/common/input/mod.rs +++ b/src/common/input/mod.rs @@ -1,5 +1,6 @@ -//! The way terminal iput is handled. +//! The way terminal input is handled. pub mod actions; +pub mod config; pub mod handler; pub mod keybinds; diff --git a/src/common/input/unit/keybinds_test.rs b/src/common/input/unit/keybinds_test.rs new file mode 100644 index 00000000..6411d017 --- /dev/null +++ b/src/common/input/unit/keybinds_test.rs @@ -0,0 +1,135 @@ +use super::super::actions::*; +use super::super::keybinds::*; +use zellij_tile::data::Key; + +#[test] +fn merge_keybinds_merges_different_keys() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::Backspace, vec![Action::NoOp]); + + let mut mode_keybinds_expected = ModeKeybinds::new(); + mode_keybinds_expected + .0 + .insert(Key::F(1), vec![Action::NoOp]); + mode_keybinds_expected + .0 + .insert(Key::Backspace, vec![Action::NoOp]); + + let mode_keybinds_merged = mode_keybinds_self.merge(mode_keybinds_other); + + assert_eq!(mode_keybinds_expected, mode_keybinds_merged); +} + +#[test] +fn merge_mode_keybinds_overwrites_same_keys() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::F(1), vec![Action::GoToTab(1)]); + + let mut mode_keybinds_expected = ModeKeybinds::new(); + mode_keybinds_expected + .0 + .insert(Key::F(1), vec![Action::GoToTab(1)]); + + let mode_keybinds_merged = mode_keybinds_self.merge(mode_keybinds_other); + + assert_eq!(mode_keybinds_expected, mode_keybinds_merged); +} + +#[test] +fn merge_keybinds_merges() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::Backspace, vec![Action::NoOp]); + let mut keybinds_self = Keybinds::new(); + keybinds_self + .0 + .insert(InputMode::Normal, mode_keybinds_self.clone()); + let mut keybinds_other = Keybinds::new(); + keybinds_other + .0 + .insert(InputMode::Resize, mode_keybinds_other.clone()); + let mut keybinds_expected = Keybinds::new(); + keybinds_expected + .0 + .insert(InputMode::Normal, mode_keybinds_self); + keybinds_expected + .0 + .insert(InputMode::Resize, mode_keybinds_other); + + assert_eq!( + keybinds_expected, + keybinds_self.merge_keybinds(keybinds_other) + ) +} + +#[test] +fn merge_keybinds_overwrites_same_keys() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + mode_keybinds_self.0.insert(Key::F(2), vec![Action::NoOp]); + mode_keybinds_self.0.insert(Key::F(3), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::F(1), vec![Action::GoToTab(1)]); + mode_keybinds_other + .0 + .insert(Key::F(2), vec![Action::GoToTab(2)]); + mode_keybinds_other + .0 + .insert(Key::F(3), vec![Action::GoToTab(3)]); + let mut keybinds_self = Keybinds::new(); + keybinds_self + .0 + .insert(InputMode::Normal, mode_keybinds_self.clone()); + let mut keybinds_other = Keybinds::new(); + keybinds_other + .0 + .insert(InputMode::Normal, mode_keybinds_other.clone()); + let mut keybinds_expected = Keybinds::new(); + keybinds_expected + .0 + .insert(InputMode::Normal, mode_keybinds_other); + + assert_eq!( + keybinds_expected, + keybinds_self.merge_keybinds(keybinds_other) + ) +} + +#[test] +fn from_keyaction_from_yaml_to_mode_keybindings() { + let actions = vec![Action::NoOp, Action::GoToTab(1)]; + let keyaction = KeyActionFromYaml { + action: actions.clone(), + key: vec![Key::F(1), Key::Backspace, Key::Char('t')], + }; + + let mut expected = ModeKeybinds::new(); + expected.0.insert(Key::F(1), actions.clone()); + expected.0.insert(Key::Backspace, actions.clone()); + expected.0.insert(Key::Char('t'), actions); + + assert_eq!(expected, ModeKeybinds::from(keyaction)); +} + +//#[test] +//fn from_keybinds_from_yaml_to_keybinds(){ +//let mut keybinds_from_yaml = KeybindsFromYaml(HashMap<InputMode, Vec<KeyActionFromYaml>>); +//let actions = vec![Action::NoOp, Action::GoToTab(1), ]; +//let keyaction = KeyActionFromYaml { +//action : actions.clone(), +//key : vec![ Key::F(1), Key::Backspace , Key::Char('t'), ], +//}; +//} diff --git a/src/common/mod.rs b/src/common/mod.rs index afd1f78f..0e3e733b 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -22,6 +22,7 @@ use std::{ }; use crate::cli::CliArgs; +use crate::common::input::config::Config; use crate::layout::Layout; use crate::panes::PaneId; use colors_transform::{Color, Rgb}; @@ -33,7 +34,7 @@ use os_input_output::OsApi; use pty_bus::{PtyBus, PtyInstruction}; use screen::{Screen, ScreenInstruction}; use serde::{Deserialize, Serialize}; -use utils::consts::ZELLIJ_IPC_PIPE; +use utils::{consts::ZELLIJ_IPC_PIPE, shared::load_palette}; use wasm_vm::PluginEnv; use wasm_vm::{wasi_stdout, wasi_write_string, zellij_imports, PluginInstruction}; use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; @@ -119,88 +120,6 @@ pub enum AppInstruction { Error(String), } -pub mod colors { - pub const WHITE: (u8, u8, u8) = (238, 238, 238); - pub const GREEN: (u8, u8, u8) = (175, 255, 0); - pub const GRAY: (u8, u8, u8) = (68, 68, 68); - pub const BRIGHT_GRAY: (u8, u8, u8) = (138, 138, 138); - pub const RED: (u8, u8, u8) = (135, 0, 0); - pub const BLACK: (u8, u8, u8) = (0, 0, 0); -} - -pub fn detect_theme(bg: (u8, u8, u8)) -> Theme { - let (r, g, b) = bg; - // HSP, P stands for perceived brightness - let hsp: f64 = (0.299 * (r as f64 * r as f64) - + 0.587 * (g as f64 * g as f64) - + 0.114 * (b as f64 * b as f64)) - .sqrt(); - match hsp > 127.5 { - true => Theme::Light, - false => Theme::Dark, - } -} - -pub fn load_palette() -> Palette { - let palette = match Colors::new("xresources") { - Some(colors) => { - let fg = colors.fg.unwrap(); - let fg_imm = &fg; - let fg_hex: &str = &fg_imm; - let fg = Rgb::from_hex_str(fg_hex).unwrap().as_tuple(); - let fg = (fg.0 as u8, fg.1 as u8, fg.2 as u8); - let bg = colors.bg.unwrap(); - let bg_imm = &bg; - let bg_hex: &str = &bg_imm; - let bg = Rgb::from_hex_str(bg_hex).unwrap().as_tuple(); - let bg = (bg.0 as u8, bg.1 as u8, bg.2 as u8); - let colors: Vec<(u8, u8, u8)> = colors - .colors - .iter() - .map(|c| { - let c = c.clone(); - let imm_str = &c.unwrap(); - let hex_str: &str = &imm_str; - let rgb = Rgb::from_hex_str(hex_str).unwrap().as_tuple(); - (rgb.0 as u8, rgb.1 as u8, rgb.2 as u8) - }) - .collect(); - let theme = detect_theme(bg); - debug_log_to_file(format!( - "{:?} {:?}, white: {:?}, black: {:?}, fg: {:?}", - theme, bg, colors[7], colors[0], fg - )); - Palette { - theme, - fg, - bg, - black: colors[0], - red: colors[1], - green: colors[2], - yellow: colors[3], - blue: colors[4], - magenta: colors[5], - cyan: colors[6], - white: colors[7], - } - } - None => Palette { - theme: Theme::Dark, - fg: colors::BRIGHT_GRAY, - bg: colors::BLACK, - black: colors::BLACK, - red: colors::RED, - green: colors::GREEN, - yellow: colors::GRAY, - blue: colors::GRAY, - magenta: colors::GRAY, - cyan: colors::GRAY, - white: colors::WHITE, - }, - }; - palette -} - /// Start Zellij with the specified [`OsApi`] and command-line arguments. // FIXME this should definitely be modularized and split into different functions. pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { @@ -211,6 +130,13 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { .write(take_snapshot.as_bytes()) .unwrap(); + let config = Config::from_cli_config(opts.config) + .map_err(|e| { + eprintln!("There was an error in the config file:\n{}", e); + std::process::exit(1); + }) + .unwrap(); + let command_is_executing = CommandIsExecuting::new(); let full_screen_ws = os_input.get_terminal_size_using_fd(0); @@ -360,10 +286,22 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { screen.send_pty_instructions.update(err_ctx); match event { ScreenInstruction::Pty(pid, vte_event) => { - screen - .get_active_tab_mut() - .unwrap() - .handle_pty_event(pid, vte_event); + let active_tab = screen.get_active_tab_mut().unwrap(); + if active_tab.has_terminal_pid(pid) { + // it's most likely that this event is directed at the active tab + // look there first + active_tab.handle_pty_event(pid, vte_event); + } else { + // if this event wasn't directed at the active tab, start looking + // in other tabs + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_terminal_pid(pid) { + tab.handle_pty_event(pid, vte_event); + break; + } + } + } } ScreenInstruction::Render => { screen.render(); @@ -398,9 +336,15 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { ScreenInstruction::ResizeUp => { screen.get_active_tab_mut().unwrap().resize_up(); } - ScreenInstruction::MoveFocus => { + ScreenInstruction::SwitchFocus => { screen.get_active_tab_mut().unwrap().move_focus(); } + ScreenInstruction::FocusNextPane => { + screen.get_active_tab_mut().unwrap().focus_next_pane(); + } + ScreenInstruction::FocusPreviousPane => { + screen.get_active_tab_mut().unwrap().focus_previous_pane(); + } ScreenInstruction::MoveFocusLeft => { screen.get_active_tab_mut().unwrap().move_focus_left(); } @@ -661,7 +605,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { } ApiCommand::MoveFocus => { send_screen_instructions - .send(ScreenInstruction::MoveFocus) + .send(ScreenInstruction::FocusNextPane) .unwrap(); } } @@ -682,9 +626,11 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { let send_pty_instructions = send_pty_instructions.clone(); let send_plugin_instructions = send_plugin_instructions.clone(); let os_input = os_input.clone(); + let config = config; move || { input_loop( os_input, + config, command_is_executing, send_screen_instructions, send_pty_instructions, diff --git a/src/common/os_input_output.rs b/src/common/os_input_output.rs index de0c6a3b..013b9f12 100644 --- a/src/common/os_input_output.rs +++ b/src/common/os_input_output.rs @@ -242,7 +242,13 @@ impl OsApi for OsInputOutput { Box::new(stdout) } fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error> { - kill(Pid::from_raw(pid), Some(Signal::SIGINT)).unwrap(); + // TODO: + // Ideally, we should be using SIGINT rather than SIGKILL here, but there are cases in which + // the terminal we're trying to kill hangs on SIGINT and so all the app gets stuck + // that's why we're sending SIGKILL here + // A better solution would be to send SIGINT here and not wait for it, and then have + // a background thread do the waitpid stuff and send SIGKILL if the process is stuck + kill(Pid::from_raw(pid), Some(Signal::SIGKILL)).unwrap(); waitpid(Pid::from_raw(pid), None).unwrap(); Ok(()) } diff --git a/src/common/screen.rs b/src/common/screen.rs index 7546bece..6ef7e9e0 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -28,7 +28,9 @@ pub enum ScreenInstruction { ResizeRight, ResizeDown, ResizeUp, - MoveFocus, + SwitchFocus, + FocusNextPane, + FocusPreviousPane, MoveFocusLeft, MoveFocusDown, MoveFocusUp, diff --git a/src/common/utils/shared.rs b/src/common/utils/shared.rs index 93892d4f..5d01e78e 100644 --- a/src/common/utils/shared.rs +++ b/src/common/utils/shared.rs @@ -4,6 +4,10 @@ use std::{iter, str::from_utf8}; use strip_ansi_escapes::strip; +use colors_transform::{Color, Rgb}; +use xrdb::Colors; +use zellij_tile::data::{Palette, Theme}; + fn ansi_len(s: &str) -> usize { from_utf8(&strip(s.as_bytes()).unwrap()) .unwrap() @@ -28,3 +32,72 @@ pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String { .collect::<Vec<_>>() .join("\n\r") } + +// Colors +pub mod colors { + pub const WHITE: (u8, u8, u8) = (238, 238, 238); + pub const GREEN: (u8, u8, u8) = (175, 255, 0); + pub const GRAY: (u8, u8, u8) = (68, 68, 68); + pub const BRIGHT_GRAY: (u8, u8, u8) = (138, 138, 138); + pub const RED: (u8, u8, u8) = (135, 0, 0); + pub const BLACK: (u8, u8, u8) = (0, 0, 0); +} + +pub fn hex_to_rgb(hex: &Option<String>) -> (u8, u8, u8) { + let c = hex.clone(); + let imm_str = &c.unwrap(); + let hex_str: &str = &imm_str; + let rgb = Rgb::from_hex_str(hex_str).unwrap().as_tuple(); + (rgb.0 as u8, rgb.1 as u8, rgb.2 as u8) +} + +pub fn detect_theme(bg: (u8, u8, u8)) -> Theme { + let (r, g, b) = bg; + // HSP, P stands for perceived brightness + let hsp: f64 = (0.299 * (r as f64 * r as f64) + + 0.587 * (g as f64 * g as f64) + + 0.114 * (b as f64 * b as f64)) + .sqrt(); + match hsp > 127.5 { + true => Theme::Light, + false => Theme::Dark, + } +} + +pub fn load_palette() -> Palette { + let palette = match Colors::new("xresources") { + Some(palette) => { + let fg = hex_to_rgb(&palette.fg); + let bg = hex_to_rgb(&palette.bg); + let colors: Vec<(u8, u8, u8)> = palette.colors.iter().map(|c| hex_to_rgb(c)).collect(); + let theme = detect_theme(bg); + Palette { + theme, + fg, + bg, + black: colors[0], + red: colors[1], + green: colors[2], + yellow: colors[3], + blue: colors[4], + magenta: colors[5], + cyan: colors[6], + white: colors[7], + } + } + None => Palette { + theme: Theme::Dark, + fg: colors::BRIGHT_GRAY, + bg: colors::BLACK, + black: colors::BLACK, + red: colors::RED, + green: colors::GREEN, + yellow: colors::GRAY, + blue: colors::GRAY, + magenta: colors::GRAY, + cyan: colors::GRAY, + white: colors::WHITE, + }, + }; + palette +} diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 8a44dcdf..be25f3c5 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -37,18 +37,25 @@ pub enum Event { pub enum InputMode { /// In `Normal` mode, input is always written to the terminal, except for the shortcuts leading /// to other modes + #[serde(alias = "normal")] Normal, /// In `Locked` mode, input is always written to the terminal and all shortcuts are disabled /// except the one leading back to normal mode + #[serde(alias = "locked")] Locked, /// `Resize` mode allows resizing the different existing panes. + #[serde(alias = "resize")] Resize, /// `Pane` mode allows creating and closing panes, as well as moving between them. + #[serde(alias = "pane")] Pane, /// `Tab` mode allows creating and closing tabs, as well as moving between them. + #[serde(alias = "tab")] Tab, /// `Scroll` mode allows scrolling up and down within a pane. + #[serde(alias = "scroll")] Scroll, + #[serde(alias = "renametab")] RenameTab, } @@ -61,7 +68,7 @@ impl Default for InputMode { #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum Theme { Light, - Dark + Dark, } pub mod colors { pub const WHITE: (u8, u8, u8) = (238, 238, 238); |
