Repository: osa1/tiny (language: rust)

Issue:
how to setting /ignore and /notify by default?
subject


Discussion / hints:
This was mentioned a few times in some other tickets -- we currently doesn't support this.

Ideally I think we need a few things:

- A way to set default values of these. E.g. in the config file:

  ```
  defaults:
    ignore: true # enable ignore by default
    notify: off # disable notifications my default
  ```

- A way to set these parameters when joining, e.g.
  ```
  /join #foo -ignore -notify all
  ```

  This should join `#foo` with "ignore" on and "notify" set to "all".

- The parameters above should work in config file too, e.g.
  ```
  join:
    - '#tiny -ignore -notify all'
  ```

Not to hard to implement but I just need to allocate a few hours.

If anyone wants to contribute let me know and I can help.
Unless someone else is working on this, I think I'll take a crack at it, starting tonight, along with #217.
@nerosnm go for it! Let me know if you have any questions.
@nerosnm anything on this? 😬 
@trevarj it's been more than 2 months without any updates, I think this is up for grabs.
Hi, really sorry about that! This kind of slipped off my radar, I ended up not really having the bandwidth to work on it. Anyone else please feel free to take it over.
No problem @nerosnm , thanks for the update.

Fix patch:
diff --git a/crates/libtiny_common/src/lib.rs b/crates/libtiny_common/src/lib.rs
index 0b75190a..ac9d2f08 100644
--- a/crates/libtiny_common/src/lib.rs
+++ b/crates/libtiny_common/src/lib.rs
@@ -164,6 +164,26 @@ pub enum MsgTarget<'a> {
     CurrentTab,
 }
 
+impl<'a> MsgTarget<'a> {
+    pub fn serv_name(&self) -> Option<&str> {
+        match self {
+            MsgTarget::Server { serv }
+            | MsgTarget::Chan { serv, .. }
+            | MsgTarget::User { serv, .. }
+            | MsgTarget::AllServTabs { serv } => Some(serv),
+            _ => None,
+        }
+    }
+
+    pub fn chan_or_user_name(&self) -> Option<&ChanNameRef> {
+        match self {
+            MsgTarget::Chan { chan, .. } => Some(chan),
+            MsgTarget::User { nick, .. } => Some(ChanNameRef::new(nick)),
+            _ => None,
+        }
+    }
+}
+
 /// Source of a message from the user.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum MsgSource {
@@ -186,6 +206,13 @@ impl MsgSource {
         }
     }
 
+    pub fn chan_name(&self) -> Option<&ChanNameRef> {
+        match self {
+            MsgSource::Chan { chan, .. } => Some(chan),
+            _ => None,
+        }
+    }
+
     pub fn to_target(&self) -> MsgTarget {
         match self {
             MsgSource::Serv { serv } => MsgTarget::Server { serv },
diff --git a/crates/libtiny_tui/src/config.rs b/crates/libtiny_tui/src/config.rs
index 730a707c..e0889429 100644
--- a/crates/libtiny_tui/src/config.rs
+++ b/crates/libtiny_tui/src/config.rs
@@ -1,18 +1,26 @@
 // To see how color numbers map to actual colors in your terminal run
 // `cargo run --example colors`. Use tab to swap fg/bg colors.
 
+use libtiny_common::{ChanName, ChanNameRef};
 use serde::de::{self, Deserializer, MapAccess, Visitor};
 use serde::Deserialize;
+use std::collections::HashMap;
 use std::fs::File;
 use std::io::Read;
 use std::path::Path;
+use std::str::FromStr;
 
 pub use termbox_simple::*;
 
 use crate::key_map::KeyMap;
+use crate::notifier::Notifier;
 
-#[derive(Deserialize)]
+#[derive(Debug, Default, Deserialize)]
 pub(crate) struct Config {
+    pub(crate) servers: Vec<Server>,
+
+    pub(crate) defaults: Defaults,
+
     #[serde(default)]
     pub(crate) colors: Colors,
 
@@ -28,6 +36,222 @@ pub(crate) struct Config {
     pub(crate) key_map: Option<KeyMap>,
 }
 
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+pub(crate) struct Server {
+    pub(crate) addr: String,
+    pub(crate) join: Vec<Chan>,
+    #[serde(flatten)]
+    pub(crate) config: TabConfig,
+}
+
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+pub(crate) struct Defaults {
+    #[serde(default, flatten)]
+    pub(crate) tab_config: TabConfig,
+}
+
+impl Default for Defaults {
+    fn default() -> Self {
+        Defaults {
+            tab_config: TabConfig {
+                ignore: Some(false),
+                notify: Some(Notifier::default()),
+            },
+        }
+    }
+}
+
+#[derive(Clone, Deserialize, Debug, PartialEq, Eq)]
+#[serde(untagged)]
+pub enum Chan {
+    #[serde(deserialize_with = "deser_chan_name")]
+    Name(ChanName),
+    WithConfig {
+        #[serde(deserialize_with = "deser_chan_name")]
+        name: ChanName,
+        #[serde(flatten)]
+        config: TabConfig,
+    },
+}
+
+impl Chan {
+    pub fn name(&self) -> &ChanNameRef {
+        match self {
+            Chan::Name(name) => name,
+            Chan::WithConfig { name, .. } => name,
+        }
+        .as_ref()
+    }
+}
+
+fn deser_chan_name<'de, D>(d: D) -> Result<ChanName, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let name: String = serde::de::Deserialize::deserialize(d)?;
+    Ok(ChanName::new(name))
+}
+
+impl FromStr for Chan {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        // Make sure channel starts with '#'
+        let s = if !s.starts_with('#') {
+            format!("#{}", s)
+        } else {
+            s.to_string()
+        };
+        // Try to split chan name and args
+        match s.split_once(' ') {
+            // with args
+            Some((name, args)) => {
+                let config = TabConfig::from_str(args)?;
+                Ok(Chan::WithConfig {
+                    name: ChanName::new(name.to_string()),
+                    config,
+                })
+            }
+            // chan name only
+            None => Ok(Chan::Name(ChanName::new(s))),
+        }
+    }
+}
+
+/// Map of TabConfigs by tab names
+#[derive(Debug, Default)]
+pub(crate) struct TabConfigs(HashMap<String, TabConfig>);
+
+impl TabConfigs {
+    pub(crate) fn get(
+        &self,
+        serv_name: &str,
+        chan_name: Option<&ChanNameRef>,
+    ) -> Option<TabConfig> {
+        let key = if let Some(chan) = chan_name {
+            format!("{}_{}", serv_name, chan.display())
+        } else {
+            serv_name.to_string()
+        };
+        self.0.get(&key).cloned()
+    }
+
+    pub(crate) fn get_mut(
+        &mut self,
+        serv_name: &str,
+        chan_name: Option<&ChanNameRef>,
+    ) -> Option<&mut TabConfig> {
+        let key = if let Some(chan) = chan_name {
+            format!("{}_{}", serv_name, chan.display())
+        } else {
+            serv_name.to_string()
+        };
+        self.0.get_mut(&key)
+    }
+
+    pub(crate) fn set(
+        &mut self,
+        serv_name: &str,
+        chan_name: Option<&ChanNameRef>,
+        config: TabConfig,
+    ) {
+        let key = if let Some(chan) = chan_name {
+            format!("{}_{}", serv_name, chan.display())
+        } else {
+            serv_name.to_string()
+        };
+        self.0.insert(key, config);
+    }
+
+    pub(crate) fn set_by_server(&mut self, serv_name: &str, config: TabConfig) {
+        for c in self
+            .0
+            .iter_mut()
+            .filter(|entry| entry.0.starts_with(serv_name))
+        {
+            *c.1 = config;
+        }
+    }
+}
+
+impl From<&Config> for TabConfigs {
+    fn from(config: &Config) -> Self {
+        let mut tab_configs = HashMap::new();
+        for server in &config.servers {
+            let serv_tc = server.config.or_use(&config.defaults.tab_config);
+            tab_configs.insert(server.addr.clone(), serv_tc);
+            for chan in &server.join {
+                let (name, tc) = match chan {
+                    Chan::Name(name) => (name, serv_tc),
+                    Chan::WithConfig { name, config } => (name, config.or_use(&serv_tc)),
+                };
+                tab_configs.insert(format!("{}_{}", server.addr, name.display()), tc);
+            }
+        }
+        tab_configs.insert("_defaults".to_string(), config.defaults.tab_config);
+        debug!("new {tab_configs:?}");
+        Self(tab_configs)
+    }
+}
+
+#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq)]
+pub struct TabConfig {
+    /// `true` if tab is ignoring join/part messages
+    #[serde(default)]
+    pub ignore: Option<bool>,
+    /// Notification setting for tab
+    #[serde(default)]
+    pub notify: Option<Notifier>,
+}
+
+impl TabConfig {
+    pub fn user_tab_defaults() -> TabConfig {
+        TabConfig {
+            ignore: Some(false),
+            notify: Some(Notifier::Messages),
+        }
+    }
+
+    pub(crate) fn or_use(&self, config: &TabConfig) -> TabConfig {
+        TabConfig {
+            ignore: self.ignore.or(config.ignore),
+            notify: self.notify.or(config.notify),
+        }
+    }
+
+    pub(crate) fn toggle_ignore(&mut self) -> bool {
+        let ignore = self.ignore.get_or_insert(false);
+        *ignore = !&*ignore;
+        *ignore
+    }
+}
+
+impl FromStr for TabConfig {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let config = s
+            .split('-')
+            .filter_map(|arg| (!arg.is_empty()).then(|| arg.trim()))
+            .try_fold(TabConfig::default(), |mut tc, arg| match arg {
+                // flag
+                "ignore" => {
+                    tc.ignore = Some(true);
+                    Ok(tc)
+                }
+                arg => match arg.split_once(' ') {
+                    // arg with parameter
+                    Some(("notify", val)) => {
+                        tc.notify = Some(Notifier::from_str(val)?);
+                        Ok(tc)
+                    }
+                    _ => Err(format!("Unexpected argument: {:?}", arg)),
+                },
+            })?;
+        Ok(config)
+    }
+}
+
 fn default_max_nick_length() -> usize {
     12
 }
@@ -41,7 +265,7 @@ pub struct Style {
     pub bg: u16,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub(crate) enum Layout {
     Compact,
diff --git a/crates/libtiny_tui/src/lib.rs b/crates/libtiny_tui/src/lib.rs
index 53e6b161..efc2e985 100644
--- a/crates/libtiny_tui/src/lib.rs
+++ b/crates/libtiny_tui/src/lib.rs
@@ -1,7 +1,7 @@
 #![allow(clippy::too_many_arguments)]
 #![allow(clippy::cognitive_complexity)]
 
-mod config;
+pub mod config;
 mod editor;
 mod exit_dialogue;
 mod input_area;
@@ -24,7 +24,9 @@ mod widget;
 mod tests;
 
 use crate::tui::{CmdResult, TUIRet};
+use config::TabConfig;
 use libtiny_common::{ChanNameRef, Event, MsgSource, MsgTarget, TabStyle};
+pub use notifier::Notifier;
 use term_input::Input;
 
 use std::cell::RefCell;
@@ -256,6 +258,25 @@ impl TUI {
     ));
     delegate!(set_tab_style(style: TabStyle, target: &MsgTarget,));
 
+    pub fn get_tab_config(&self, serv_name: &str, chan_name: Option<&ChanNameRef>) -> TabConfig {
+        self.inner
+            .upgrade()
+            .map(|tui| tui.borrow().get_tab_config(serv_name, chan_name))
+            .unwrap_or_default()
+    }
+
+    pub fn set_tab_config(
+        &self,
+        serv_name: &str,
+        chan_name: Option<&ChanNameRef>,
+        config: TabConfig,
+    ) {
+        if let Some(tui) = self.inner.upgrade() {
+            tui.borrow_mut()
+                .set_tab_config(serv_name, chan_name, config);
+        }
+    }
+
     pub fn user_tab_exists(&self, serv_name: &str, nick: &str) -> bool {
         match self.inner.upgrade() {
             Some(tui) => tui.borrow().user_tab_exists(serv_name, nick),
diff --git a/crates/libtiny_tui/src/messaging.rs b/crates/libtiny_tui/src/messaging.rs
index 04bf5538..91a00006 100644
--- a/crates/libtiny_tui/src/messaging.rs
+++ b/crates/libtiny_tui/src/messaging.rs
@@ -27,9 +27,6 @@ pub(crate) struct MessagingUI {
     width: i32,
     height: i32,
 
-    // Option to disable status messages ( join/part )
-    show_status: bool,
-
     // All nicks in the channel. Need to keep this up-to-date to be able to
     // properly highlight mentions.
     nicks: Trie,
@@ -91,7 +88,6 @@ impl MessagingUI {
     pub(crate) fn new(
         width: i32,
         height: i32,
-        status: bool,
         scrollback: usize,
         msg_layout: Layout,
     ) -> MessagingUI {
@@ -101,7 +97,6 @@ impl MessagingUI {
             exit_dialogue: None,
             width,
             height,
-            show_status: status,
             nicks: Trie::new(),
             last_activity_line: None,
             last_activity_ts: None,
@@ -385,8 +380,8 @@ impl MessagingUI {
         self.nicks.clear();
     }
 
-    pub(crate) fn join(&mut self, nick: &str, ts: Option<Timestamp>) {
-        if self.show_status {
+    pub(crate) fn join(&mut self, nick: &str, ts: Option<Timestamp>, ignore: bool) {
+        if !ignore {
             if let Some(ts) = ts {
                 let line_idx = self.get_activity_line_idx(ts);
                 self.msg_area.modify_line(line_idx, |line| {
@@ -399,10 +394,10 @@ impl MessagingUI {
         self.nicks.insert(nick);
     }
 
-    pub(crate) fn part(&mut self, nick: &str, ts: Option<Timestamp>) {
+    pub(crate) fn part(&mut self, nick: &str, ts: Option<Timestamp>, ignore: bool) {
         self.nicks.remove(nick);
 
-        if self.show_status {
+        if !ignore {
             if let Some(ts) = ts {
                 let line_idx = self.get_activity_line_idx(ts);
                 self.msg_area.modify_line(line_idx, |line| {
@@ -413,21 +408,6 @@ impl MessagingUI {
         }
     }
 
-    /// `state` == `None` means toggle
-    /// `state` == `Some(state)` means set it to `state`
-    pub(crate) fn set_or_toggle_ignore(&mut self, state: Option<bool>) {
-        self.show_status = state.unwrap_or(!self.show_status);
-        if self.show_status {
-            self.add_client_notify_msg("Ignore disabled");
-        } else {
-            self.add_client_notify_msg("Ignore enabled");
-        }
-    }
-
-    pub(crate) fn is_showing_status(&self) -> bool {
-        self.show_status
-    }
-
     pub(crate) fn nick(&mut self, old_nick: &str, new_nick: &str, ts: Timestamp) {
         self.nicks.remove(old_nick);
         self.nicks.insert(new_nick);
diff --git a/crates/libtiny_tui/src/notifier.rs b/crates/libtiny_tui/src/notifier.rs
index f507186f..107ab4a7 100644
--- a/crates/libtiny_tui/src/notifier.rs
+++ b/crates/libtiny_tui/src/notifier.rs
@@ -1,21 +1,48 @@
+use std::str::FromStr;
+
 use crate::MsgTarget;
 
 use libtiny_wire::formatting::remove_irc_control_chars;
 
 #[cfg(feature = "desktop-notifications")]
 use notify_rust::Notification;
+use serde::Deserialize;
 
 /// Destktop notification handler
-#[derive(Debug, PartialEq, Eq, Copy, Clone)]
-pub(crate) enum Notifier {
+#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, PartialOrd, Ord)]
+#[serde(rename_all = "lowercase")]
+pub enum Notifier {
     /// Notifications are disabled.
     Off,
     /// Generate notifications only for mentions.
     Mentions,
-    /// Generate notificastions for all messages.
+    /// Generate notifications for all messages.
     Messages,
 }
 
+impl Default for Notifier {
+    fn default() -> Self {
+        if cfg!(feature = "desktop-notifications") {
+            Notifier::Mentions
+        } else {
+            Notifier::Off
+        }
+    }
+}
+
+impl FromStr for Notifier {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.to_lowercase().as_str() {
+            "off" => Ok(Notifier::Off),
+            "mentions" => Ok(Notifier::Mentions),
+            "messages" => Ok(Notifier::Messages),
+            _ => Err(format!("Unknown Notifier variant: {}", s)),
+        }
+    }
+}
+
 #[cfg(feature = "desktop-notifications")]
 fn notify(summary: &str, body: &str) {
     // TODO: Report errors somehow
diff --git a/crates/libtiny_tui/src/tab.rs b/crates/libtiny_tui/src/tab.rs
index e60b9b30..e30b4e85 100644
--- a/crates/libtiny_tui/src/tab.rs
+++ b/crates/libtiny_tui/src/tab.rs
@@ -6,7 +6,6 @@ use unicode_width::UnicodeWidthStr;
 use crate::{
     config::{Colors, Style},
     messaging::MessagingUI,
-    notifier::Notifier,
 };
 
 pub(crate) struct Tab {
@@ -16,7 +15,6 @@ pub(crate) struct Tab {
     pub(crate) style: TabStyle,
     /// Alt-character to use to switch to this tab.
     pub(crate) switch: Option<char>,
-    pub(crate) notifier: Notifier,
 }
 
 fn tab_style(style: TabStyle, colors: &Colors) -> Style {
diff --git a/crates/libtiny_tui/src/tui.rs b/crates/libtiny_tui/src/tui.rs
index ec34817f..81c8227a 100644
--- a/crates/libtiny_tui/src/tui.rs
+++ b/crates/libtiny_tui/src/tui.rs
@@ -13,7 +13,7 @@ use std::path::PathBuf;
 use std::str::{self, SplitWhitespace};
 use time::Tm;
 
-use crate::config::{parse_config, Colors, Config, Style};
+use crate::config::{parse_config, Colors, Config, Style, TabConfig, TabConfigs};
 use crate::editor;
 use crate::key_map::{KeyAction, KeyMap};
 use crate::messaging::{MessagingUI, Timestamp};
@@ -100,6 +100,9 @@ pub struct TUI {
 
     /// Config file path
     config_path: Option<PathBuf>,
+
+    /// TabConfig settings loaded from config file
+    tab_configs: TabConfigs,
 }
 
 pub(crate) enum CmdResult {
@@ -166,6 +169,7 @@ impl TUI {
             h_scroll: 0,
             key_map: KeyMap::default(),
             config_path,
+            tab_configs: TabConfigs::default(),
         };
 
         // Init "mentions" tab. This needs to happen right after creating the TUI to be able to
@@ -181,17 +185,33 @@ impl TUI {
     }
 
     fn ignore(&mut self, src: &MsgSource) {
-        match src {
+        let (target, ignore) = match src {
             MsgSource::Serv { serv } => {
-                self.toggle_ignore(&MsgTarget::AllServTabs { serv });
+                let mut config = self.get_tab_config(serv, None);
+                let new_ignore = config.toggle_ignore();
+                self.tab_configs.set_by_server(serv, config);
+                (MsgTarget::AllServTabs { serv }, new_ignore)
             }
             MsgSource::Chan { serv, chan } => {
-                self.toggle_ignore(&MsgTarget::Chan { serv, chan });
+                let mut config = self.get_tab_config(serv, Some(chan));
+                let new_ignore = config.toggle_ignore();
+                self.tab_configs.set(serv, Some(chan), config);
+                (MsgTarget::Chan { serv, chan }, new_ignore)
             }
             MsgSource::User { serv, nick } => {
-                self.toggle_ignore(&MsgTarget::User { serv, nick });
+                let nick_chan = ChanNameRef::new(nick);
+                let mut config = self.get_tab_config(serv, Some(nick_chan));
+                let new_ignore = config.toggle_ignore();
+                self.tab_configs.set(serv, Some(nick_chan), config);
+                (MsgTarget::User { serv, nick }, new_ignore)
             }
-        }
+        };
+        let msg = if ignore {
+            "Ignore enabled"
+        } else {
+            "Ignore disabled"
+        };
+        self.add_client_notify_msg(msg, &target);
     }
 
     fn notify(&mut self, words: &mut SplitWhitespace, src: &MsgSource) {
@@ -216,7 +236,7 @@ impl TUI {
         };
 
         if words.is_empty() {
-            self.show_notify_mode(&MsgTarget::CurrentTab);
+            self.show_notify_mode(src);
         } else if words.len() != 1 {
             show_usage();
         } else {
@@ -283,6 +303,7 @@ impl TUI {
             }
             Some("reload") => {
                 self.reload_config();
+                self.add_client_notify_msg("Reloaded config file.", &MsgTarget::CurrentTab);
                 CmdResult::Ok
             }
             Some("help") => {
@@ -313,7 +334,7 @@ impl TUI {
         }
     }
 
-    pub(crate) fn reload_config(&mut self) {
+    pub(crate) fn load_config(&mut self) -> Option<Config> {
         if let Some(ref config_path) = self.config_path {
             match parse_config(config_path) {
                 Err(err) => {
@@ -321,25 +342,35 @@ impl TUI {
                         &format!("Can't parse TUI config: {:?}", err),
                         &MsgTarget::CurrentTab,
                     );
+                    None
                 }
-                Ok(Config {
-                    colors,
-                    scrollback,
-                    layout,
-                    max_nick_length,
-                    key_map,
-                }) => {
-                    self.set_colors(colors);
-                    self.scrollback = scrollback.max(1);
-                    self.key_map.load(&key_map.unwrap_or_default());
-                    if let Some(layout) = layout {
-                        match layout {
-                            crate::config::Layout::Compact => self.msg_layout = Layout::Compact,
-                            crate::config::Layout::Aligned => {
-                                self.msg_layout = Layout::Aligned {
-                                    max_nick_len: max_nick_length,
-                                }
-                            }
+                Ok(config) => Some(config),
+            }
+        } else {
+            None
+        }
+    }
+
+    fn apply_config(&mut self, config: Option<Config>) {
+        if let Some(config) = config {
+            self.tab_configs = config.borrow().into();
+            let Config {
+                colors,
+                scrollback,
+                max_nick_length,
+                key_map,
+                layout,
+                ..
+            } = config;
+            self.set_colors(colors);
+            self.scrollback = scrollback.max(1);
+            self.key_map.load(&key_map.unwrap_or_default());
+            if let Some(layout) = layout {
+                match layout {
+                    crate::config::Layout::Compact => self.msg_layout = Layout::Compact,
+                    crate::config::Layout::Aligned => {
+                        self.msg_layout = Layout::Aligned {
+                            max_nick_len: max_nick_length,
                         }
                     }
                 }
@@ -347,20 +378,18 @@ impl TUI {
         }
     }
 
+    fn reload_config(&mut self) {
+        let config = self.load_config();
+        self.apply_config(config);
+    }
+
     fn set_colors(&mut self, colors: Colors) {
         self.tb
             .set_clear_attributes(colors.clear.fg as u8, colors.clear.bg as u8);
         self.colors = colors;
     }
 
-    fn new_tab(
-        &mut self,
-        idx: usize,
-        src: MsgSource,
-        status: bool,
-        notifier: Notifier,
-        alias: Option<String>,
-    ) {
+    fn new_tab(&mut self, idx: usize, src: MsgSource, alias: Option<String>) {
         let visible_name = alias.unwrap_or_else(|| match &src {
             MsgSource::Serv { serv } => serv.to_owned(),
             MsgSource::Chan { chan, .. } => chan.display().to_owned(),
@@ -410,14 +439,12 @@ impl TUI {
                 widget: MessagingUI::new(
                     self.width,
                     self.height - 1,
-                    status,
                     self.scrollback,
                     self.msg_layout,
                 ),
                 src,
                 style: TabStyle::Normal,
                 switch,
-                notifier,
             },
         );
     }
@@ -432,12 +459,6 @@ impl TUI {
                     MsgSource::Serv {
                         serv: serv.to_owned(),
                     },
-                    true,
-                    if cfg!(feature = "desktop-notifications") {
-                        Notifier::Mentions
-                    } else {
-                        Notifier::Off
-                    },
                     alias,
                 );
                 Some(tab_idx)
@@ -466,17 +487,6 @@ impl TUI {
                     self.new_chan_tab(serv, chan)
                 }
                 Some(serv_tab_idx) => {
-                    let mut status_val: bool = true;
-                    let mut notifier: Option<Notifier> = None;
-                    for tab in &self.tabs {
-                        if let MsgSource::Serv { serv: ref serv_ } = tab.src {
-                            if serv == serv_ {
-                                status_val = tab.widget.is_showing_status();
-                                notifier = Some(tab.notifier);
-                                break;
-                            }
-                        }
-                    }
                     let tab_idx = serv_tab_idx + 1;
                     self.new_tab(
                         tab_idx,
@@ -484,8 +494,6 @@ impl TUI {
                             serv: serv.to_owned(),
                             chan: chan.to_owned(),
                         },
-                        status_val,
-                        notifier.expect("Creating a channel tab, but the server tab doesn't exist"),
                         None,
                     );
                     if self.active_idx >= tab_idx {
@@ -526,14 +534,12 @@ impl TUI {
                             serv: serv.to_owned(),
                             nick: nick.to_owned(),
                         },
-                        true,
-                        Notifier::Messages,
                         None,
                     );
                     if let Some(nick) = self.tabs[tab_idx].widget.get_nick() {
                         self.tabs[tab_idx + 1].widget.set_nick(nick);
                     }
-                    self.tabs[tab_idx + 1].widget.join(nick, None);
+                    self.tabs[tab_idx + 1].widget.join(nick, None, false);
                     Some(tab_idx + 1)
                 }
             },
@@ -784,6 +790,28 @@ impl TUI {
         // redraw after resize
         self.draw()
     }
+
+    pub(crate) fn get_tab_config(
+        &self,
+        serv_name: &str,
+        chan_name: Option<&ChanNameRef>,
+    ) -> TabConfig {
+        let configs = &self.tab_configs;
+        configs
+            .get(serv_name, chan_name) // tries to get
+            .or_else(|| configs.get(serv_name, None))
+            .or_else(|| configs.get("_defaults", None))
+            .unwrap_or_default()
+    }
+
+    pub(crate) fn set_tab_config(
+        &mut self,
+        serv_name: &str,
+        chan_name: Option<&ChanNameRef>,
+        config: TabConfig,
+    ) {
+        self.tab_configs.set(serv_name, chan_name, config)
+    }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1157,9 +1185,9 @@ impl TUI {
     ////////////////////////////////////////////////////////////////////////////
     // Interfacing with tabs
 
-    fn apply_to_target<F>(&mut self, target: &MsgTarget, can_create_tab: bool, f: &F)
+    fn apply_to_target<F>(&mut self, target: &MsgTarget, can_create_tab: bool, f: &mut F)
     where
-        F: Fn(&mut Tab, bool),
+        F: FnMut(&mut Tab, bool),
     {
         // Creating a vector just to make borrow checker happy (I can't have a Vec<&mut Tab>)
         // I need to collect tabs here because of the "create if not exists" logic.
@@ -1248,11 +1276,15 @@ impl TUI {
     }
 
     pub(crate) fn set_tab_style(&mut self, style: TabStyle, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, is_active: bool| {
-            if (tab.widget.is_showing_status() || style != TabStyle::JoinOrPart)
-                && tab.style < style
-                && !is_active
-            {
+        let ignore = self
+            .get_tab_config(
+                target.serv_name().unwrap_or_default(),
+                target.chan_or_user_name(),
+            )
+            .ignore
+            .unwrap_or_default();
+        self.apply_to_target(target, false, &mut |tab: &mut Tab, is_active: bool| {
+            if (!ignore || style != TabStyle::JoinOrPart) && tab.style < style && !is_active {
                 tab.set_style(style);
             }
         });
@@ -1261,7 +1293,7 @@ impl TUI {
     /// An error message coming from Tiny, probably because of a command error
     /// etc. Those are not timestamped and not logged.
     pub(crate) fn add_client_err_msg(&mut self, msg: &str, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
+        self.apply_to_target(target, true, &mut |tab: &mut Tab, _| {
             tab.widget.add_client_err_msg(msg);
         });
     }
@@ -1269,7 +1301,7 @@ impl TUI {
     /// A notify message coming from tiny, usually shows a response of a command
     /// e.g. "Notifications enabled".
     pub(crate) fn add_client_notify_msg(&mut self, msg: &str, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
+        self.apply_to_target(target, false, &mut |tab: &mut Tab, _| {
             tab.widget.add_client_notify_msg(msg);
         });
     }
@@ -1277,7 +1309,7 @@ impl TUI {
     /// A message from client, usually just to indidate progress, e.g.
     /// "Connecting...". Not timestamed and not logged.
     pub(crate) fn add_client_msg(&mut self, msg: &str, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
+        self.apply_to_target(target, false, &mut |tab: &mut Tab, _| {
             tab.widget.add_client_msg(msg);
         });
     }
@@ -1293,13 +1325,19 @@ impl TUI {
         highlight: bool,
         is_action: bool,
     ) {
-        self.apply_to_target(target, true, &|tab: &mut Tab, _| {
+        let mut notifier = if let Some(serv) = target.serv_name() {
+            self.get_tab_config(serv, target.chan_or_user_name())
+                .notify
+                .unwrap_or_default()
+        } else {
+            Notifier::default()
+        };
+        self.apply_to_target(target, true, &mut |tab: &mut Tab, _| {
             tab.widget
                 .add_privmsg(sender, msg, Timestamp::from(ts), highlight, is_action);
             let nick = tab.widget.get_nick();
             if let Some(nick_) = nick {
-                tab.notifier
-                    .notify_privmsg(sender, msg, target, &nick_, highlight);
+                notifier.notify_privmsg(sender, msg, target, &nick_, highlight);
             }
         });
     }
@@ -1307,7 +1345,7 @@ impl TUI {
     /// A message without any explicit sender info. Useful for e.g. in server
     /// and debug log tabs. Timestamped and logged.
     pub fn add_msg(&mut self, msg: &str, ts: Tm, target: &MsgTarget) {
-        self.apply_to_target(target, true, &|tab: &mut Tab, _| {
+        self.apply_to_target(target, true, &mut |tab: &mut Tab, _| {
             tab.widget.add_msg(msg, Timestamp::from(ts));
         });
     }
@@ -1315,34 +1353,50 @@ impl TUI {
     /// Error messages related with the protocol - e.g. can't join a channel,
     /// nickname is in use etc. Timestamped and logged.
     pub(crate) fn add_err_msg(&mut self, msg: &str, ts: Tm, target: &MsgTarget) {
-        self.apply_to_target(target, true, &|tab: &mut Tab, _| {
+        self.apply_to_target(target, true, &mut |tab: &mut Tab, _| {
             tab.widget.add_err_msg(msg, Timestamp::from(ts));
         });
     }
 
     pub(crate) fn set_topic(&mut self, title: &str, ts: Tm, serv: &str, chan: &ChanNameRef) {
         let target = MsgTarget::Chan { serv, chan };
-        self.apply_to_target(&target, false, &|tab: &mut Tab, _| {
+        self.apply_to_target(&target, false, &mut |tab: &mut Tab, _| {
             tab.widget.show_topic(title, Timestamp::from(ts));
         });
     }
 
     pub(crate) fn clear_nicks(&mut self, serv: &str) {
         let target = MsgTarget::AllServTabs { serv };
-        self.apply_to_target(&target, false, &|tab: &mut Tab, _| {
+        self.apply_to_target(&target, false, &mut |tab: &mut Tab, _| {
             tab.widget.clear_nicks();
         });
     }
 
     pub(crate) fn add_nick(&mut self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
-            tab.widget.join(nick, ts.map(Timestamp::from));
+        let ignore = self
+            .get_tab_config(
+                target.serv_name().unwrap_or_default(),
+                target.chan_or_user_name(),
+            )
+            .ignore
+            .unwrap_or_default();
+
+        self.apply_to_target(target, false, &mut |tab: &mut Tab, _| {
+            tab.widget.join(nick, ts.map(Timestamp::from), ignore);
         });
     }
 
     pub(crate) fn remove_nick(&mut self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
-            tab.widget.part(nick, ts.map(Timestamp::from));
+        let ignore = self
+            .get_tab_config(
+                target.serv_name().unwrap_or_default(),
+                target.chan_or_user_name(),
+            )
+            .ignore
+            .unwrap_or_default();
+
+        self.apply_to_target(target, false, &mut |tab: &mut Tab, _| {
+            tab.widget.part(nick, ts.map(Timestamp::from), ignore);
         });
     }
 
@@ -1353,7 +1407,7 @@ impl TUI {
         ts: Tm,
         target: &MsgTarget,
     ) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
+        self.apply_to_target(target, false, &mut |tab: &mut Tab, _| {
             tab.widget.nick(old_nick, new_nick, Timestamp::from(ts));
             // TODO: Does this actually rename the tab?
             tab.update_source(&|src: &mut MsgSource| {
@@ -1367,34 +1421,13 @@ impl TUI {
 
     pub(crate) fn set_nick(&mut self, serv: &str, new_nick: &str) {
         let target = MsgTarget::AllServTabs { serv };
-        self.apply_to_target(&target, false, &|tab: &mut Tab, _| {
+        self.apply_to_target(&target, false, &mut |tab: &mut Tab, _| {
             tab.widget.set_nick(new_nick.to_owned())
         });
     }
 
     pub(crate) fn clear(&mut self, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| tab.widget.clear());
-    }
-
-    pub(crate) fn toggle_ignore(&mut self, target: &MsgTarget) {
-        if let MsgTarget::AllServTabs { serv } = *target {
-            let mut status_val: bool = false;
-            for tab in &self.tabs {
-                if let MsgSource::Serv { serv: ref serv_ } = tab.src {
-                    if serv == serv_ {
-                        status_val = tab.widget.is_showing_status();
-                        break;
-                    }
-                }
-            }
-            self.apply_to_target(target, false, &|tab: &mut Tab, _| {
-                tab.widget.set_or_toggle_ignore(Some(!status_val));
-            });
-        } else {
-            self.apply_to_target(target, false, &|tab: &mut Tab, _| {
-                tab.widget.set_or_toggle_ignore(None);
-            });
-        }
+        self.apply_to_target(target, false, &mut |tab: &mut Tab, _| tab.widget.clear());
     }
 
     // TODO: Maybe remove this and add a `create: bool` field to MsgTarget::User
@@ -1410,14 +1443,20 @@ impl TUI {
     }
 
     pub(crate) fn set_notifier(&mut self, notifier: Notifier, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
-            tab.notifier = notifier;
-        });
+        if let Some(serv) = target.serv_name() {
+            if let Some(config) = self.tab_configs.get_mut(serv, target.chan_or_user_name()) {
+                config.notify = Some(notifier);
+            }
+        }
     }
 
-    pub(crate) fn show_notify_mode(&mut self, target: &MsgTarget) {
-        self.apply_to_target(target, false, &|tab: &mut Tab, _| {
-            let msg = match tab.notifier {
+    pub(crate) fn show_notify_mode(&mut self, src: &MsgSource) {
+        let notifier = self
+            .get_tab_config(src.serv_name(), src.chan_name())
+            .notify
+            .unwrap_or_default();
+        self.apply_to_target(&src.to_target(), false, &mut |tab: &mut Tab, _| {
+            let msg = match notifier {
                 Notifier::Off => "Notifications are off",
                 Notifier::Mentions => "Notifications enabled for mentions",
                 Notifier::Messages => "Notifications enabled for all messages",
diff --git a/crates/tiny/config.yml b/crates/tiny/config.yml
index 054cae28..50515f70 100644
--- a/crates/tiny/config.yml
+++ b/crates/tiny/config.yml
@@ -10,8 +10,15 @@ servers:
       # alias: OFTC
 
       # Channels to automatically join
+      # Can include tab config arguments
+      # -ignore: [true | false] -- ignores join/part messages
+      # -notify: [off | mentions | messages] -- sets notifications
       join:
           - "#tiny"
+      # join:
+      #   - name: "#tiny"
+      #     ignore: true
+      #     notify: "mentions"
 
       # Three authentication methods: pass, sasl, and nickserv_ident
       # These are optional and you probably only need one of these, delete
@@ -29,6 +36,12 @@ servers:
       # (useful when `pass` or `sasl` fields above are not used)
       # nickserv_ident: 'hunter2'
 
+      # Set /ignore for this server and all its tabs, default: false
+      # ignore: true  
+
+      # Set notifications for this server and all its tabs, default: mentions
+      # notify: [off | mentions | messages]
+
 # Defaults used when connecting to servers via the /connect command
 defaults:
     nicks: [tiny_user]
@@ -36,6 +49,12 @@ defaults:
     join: []
     tls: false
 
+    # Set /ignore as the default for all servers and all its tabs, default: false
+    # ignore: true 
+
+    # Set default notifications for all servers and all its tabs, default: mentions
+    # notify: [off | mentions | messages]
+
 # Where to put log files
 log_dir: "{}"
 
diff --git a/crates/tiny/src/cmd.rs b/crates/tiny/src/cmd.rs
index b050f7a1..62c51d31 100644
--- a/crates/tiny/src/cmd.rs
+++ b/crates/tiny/src/cmd.rs
@@ -1,14 +1,16 @@
-use crate::config;
+use crate::config::Defaults;
 use crate::ui::UI;
 use crate::utils;
 use libtiny_client::{Client, ServerInfo};
-use libtiny_common::{ChanName, ChanNameRef, MsgSource, MsgTarget};
+use libtiny_common::{ChanNameRef, MsgSource, MsgTarget};
+use libtiny_tui::config::Chan;
 
 use std::borrow::Borrow;
+use std::str::FromStr;
 
 pub(crate) struct CmdArgs<'a> {
     pub args: &'a str,
-    pub defaults: &'a config::Defaults,
+    pub defaults: &'a Defaults,
     pub ui: &'a UI,
     pub clients: &'a mut Vec<Client>,
     pub src: MsgSource,
@@ -220,7 +222,7 @@ fn reconnect(ui: &UI, clients: &mut [Client], src: MsgSource) {
 fn connect_(
     serv_addr: &str,
     pass: Option<&str>,
-    defaults: &config::Defaults,
+    defaults: &Defaults,
     ui: &UI,
     clients: &mut Vec<Client>,
 ) {
@@ -292,7 +294,7 @@ static JOIN_CMD: Cmd = Cmd {
     name: "join",
     cmd_fn: join,
     description: "Joins a channel",
-    usage: "`/join <chan1>,<chan2>,...` or `/join` in a channel tab to rejoin",
+    usage: "`/join <chan1> [-ignore] [-notify [off|mentions|messages]],<chan2>...` or `/join` in a channel tab to rejoin",
 };
 
 fn join(args: CmdArgs) {
@@ -313,16 +315,28 @@ fn join(args: CmdArgs) {
         }
     }
 
-    let words = args
-        .split_whitespace()
-        .map(|c| ChanName::new(c.to_owned()))
-        .collect::<Vec<_>>();
+    let chans = args
+        .split(',')
+        .map(str::trim)
+        .filter_map(|c| match Chan::from_str(c) {
+            Ok(c) => Some(c),
+            Err(err) => {
+                ui.add_client_err_msg(
+                    &format!("{}, Usage: {}", err, JOIN_CMD.usage),
+                    &MsgTarget::CurrentTab,
+                );
+                None
+            }
+        })
+        .collect::<Vec<Chan>>();
 
-    let chans: Vec<ChanName> = if words.is_empty() {
+    let chans = if chans.is_empty() {
         match ui.current_tab() {
             None => return,
-            Some(MsgSource::Chan { serv: _, chan }) => {
-                vec![chan]
+            // rejoin current tab's channel
+            Some(MsgSource::Chan { serv, chan }) => {
+                let config = ui.get_tab_config(&serv, Some(chan.as_ref()));
+                vec![Chan::WithConfig { name: chan, config }]
             }
             Some(_) => {
                 return ui.add_client_err_msg(
@@ -332,11 +346,27 @@ fn join(args: CmdArgs) {
             }
         }
     } else {
-        words
+        chans
     };
 
-    match find_client(clients, src.serv_name()) {
-        Some(client) => client.join(chans.iter().map(ChanName::as_ref)),
+    let serv = src.serv_name();
+    match find_client(clients, serv) {
+        Some(client) => {
+            let iter_ref = chans.iter().map(|c| c.name());
+            // set tab configs of new channel tabs (creates new tab)
+            for chan in &chans {
+                match chan {
+                    Chan::Name(name) => {
+                        let config = ui.get_tab_config(serv, Some(name.as_ref()));
+                        ui.set_tab_config(serv, Some(name), config)
+                    }
+                    Chan::WithConfig { name, config } => {
+                        ui.set_tab_config(serv, Some(name), config.to_owned())
+                    }
+                }
+            }
+            client.join(iter_ref);
+        }
         None => ui.add_client_err_msg(
             &format!("Can't join: Not connected to server {}", src.serv_name()),
             &MsgTarget::CurrentTab,
diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs
index 3e64f1b7..b140d2ae 100644
--- a/crates/tiny/src/config.rs
+++ b/crates/tiny/src/config.rs
@@ -1,11 +1,14 @@
 use libtiny_client::SASLAuth as ClientSASLAuth;
 use serde::{Deserialize, Deserializer};
+
 use std::fs;
 use std::fs::File;
 use std::io::{Read, Write};
 use std::path::{Path, PathBuf};
 use std::process::Command;
 
+use libtiny_tui::config::Chan;
+
 #[derive(Clone, Deserialize, Debug, PartialEq, Eq)]
 #[serde(untagged, rename_all = "snake_case")]
 pub(crate) enum SASLAuth<P> {
@@ -66,8 +69,7 @@ pub(crate) struct Server<P> {
     pub(crate) nicks: Vec<String>,
 
     /// Channels to automatically join.
-    #[serde(default)]
-    pub(crate) join: Vec<String>,
+    pub(crate) join: Vec<Chan>,
 
     /// NickServ identification password. Used on connecting to the server and nick change.
     pub(crate) nickserv_ident: Option<P>,
@@ -448,6 +450,9 @@ fn get_default_config_yaml() -> String {
 
 #[cfg(test)]
 mod tests {
+    use libtiny_common::ChanName;
+    use serde_yaml;
+
     use super::*;
 
     #[test]
@@ -458,8 +463,11 @@ mod tests {
                 panic!();
             }
             Ok(Config { servers, .. }) => {
-                assert_eq!(servers[0].join, vec!["#tiny".to_owned()]);
-                assert!(servers[0].tls);
+                assert_eq!(
+                    servers[0].join,
+                    vec![Chan::Name(ChanName::new("#tiny".to_string()))]
+                );
+                assert_eq!(servers[0].tls, true);
             }
         }
     }
diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs
index be8b3542..5e481f49 100644
--- a/crates/tiny/src/main.rs
+++ b/crates/tiny/src/main.rs
@@ -12,7 +12,7 @@ mod utils;
 mod tests;
 
 use libtiny_client::{Client, SASLAuth, ServerInfo};
-use libtiny_common::{ChanNameRef, MsgTarget};
+use libtiny_common::MsgTarget;
 use libtiny_logger::{Logger, LoggerInitError};
 use libtiny_tui::TUI;
 use ui::UI;
@@ -165,11 +165,7 @@ fn run(
                 pass: server.pass,
                 realname: server.realname,
                 nicks: server.nicks,
-                auto_join: server
-                    .join
-                    .iter()
-                    .map(|c| ChanNameRef::new(c).to_owned())
-                    .collect(),
+                auto_join: server.join.iter().map(|c| c.name().to_owned()).collect(),
                 nickserv_ident: server.nickserv_ident,
                 sasl_auth,
             };
diff --git a/crates/tiny/src/ui.rs b/crates/tiny/src/ui.rs
index 8404ae06..a3efd902 100644
--- a/crates/tiny/src/ui.rs
+++ b/crates/tiny/src/ui.rs
@@ -7,6 +7,7 @@ use libtiny_common::{ChanNameRef, MsgSource, MsgTarget, TabStyle};
 use libtiny_logger::Logger;
 use libtiny_tui::TUI;
 
+use libtiny_tui::config::TabConfig;
 use time::Tm;
 use tokio::sync::mpsc;
 use tokio_stream::wrappers::ReceiverStream;
@@ -85,6 +86,12 @@ impl UI {
     delegate_ui!(set_nick(serv: &str, nick: &str,));
     delegate_ui!(set_tab_style(style: TabStyle, target: &MsgTarget,));
     delegate_ui!(user_tab_exists(serv_name: &str, nick: &str,) -> bool);
+    delegate_ui!(get_tab_config(serv_name: &str, chan_name: Option<&ChanNameRef>,) -> TabConfig);
+    delegate_ui!(set_tab_config(
+        serv_name: &str,
+        chan_name: Option<&ChanNameRef>,
+        config: TabConfig,
+    ));
 
     pub(crate) fn current_tab(&self) -> Option<MsgSource> {
         self.ui.current_tab()


Test patch:
diff --git a/crates/libtiny_tui/src/tests/config.rs b/crates/libtiny_tui/src/tests/config.rs
new file mode 100644
index 00000000..a93c0065
--- /dev/null
+++ b/crates/libtiny_tui/src/tests/config.rs
@@ -0,0 +1,100 @@
+use libtiny_common::{ChanName, ChanNameRef};
+
+use crate::config::*;
+use crate::notifier::Notifier;
+
+#[test]
+fn parsing_tab_configs() {
+    let config_str = r##"
+        servers:
+          - addr: "server"
+            join: 
+              - name: "#tiny"
+                ignore: true
+                notify: "messages"
+            notify: "mentions"
+          - addr: "server2"
+            join:
+              - "#tiny2" 
+            ignore: true
+        defaults:
+            ignore: false
+            notify: off
+        "##;
+    let config: Config = serde_yaml::from_str(config_str).expect("parsed config");
+    let tab_configs: TabConfigs = (&config).into();
+    let expected = Config {
+        servers: vec![
+            Server {
+                addr: "server".to_string(),
+                join: vec![Chan::WithConfig {
+                    name: ChanName::new("#tiny".to_string()),
+                    config: TabConfig {
+                        ignore: Some(true),
+                        notify: Some(Notifier::Messages),
+                    },
+                }],
+                config: TabConfig {
+                    notify: Some(Notifier::Mentions),
+                    ..Default::default()
+                },
+            },
+            Server {
+                addr: "server2".to_string(),
+                join: vec![Chan::Name(ChanName::new("#tiny2".to_string()))],
+                config: TabConfig {
+                    ignore: Some(true),
+                    ..Default::default()
+                },
+            },
+        ],
+        defaults: Defaults {
+            tab_config: TabConfig {
+                ignore: Some(false),
+                notify: Some(Notifier::Off),
+            },
+        },
+        ..Default::default()
+    };
+    assert_eq!(config.servers, expected.servers);
+    assert_eq!(config.defaults, expected.defaults);
+
+    assert_eq!(
+        tab_configs.get("server", None),
+        Some(TabConfig {
+            ignore: Some(false),              // overwritten by defaults
+            notify: Some(Notifier::Mentions)  // configured
+        })
+    );
+
+    assert_eq!(
+        tab_configs.get("server2", None),
+        Some(TabConfig {
+            ignore: Some(true),                // configured
+            notify: Some(Notifier::default())  // overwritten by defaults
+        })
+    );
+
+    assert_eq!(tab_configs.get("randomserver", None), None);
+
+    assert_eq!(
+        tab_configs.get("server", Some(ChanNameRef::new("#tiny"))),
+        Some(TabConfig {
+            ignore: Some(true),               // configured
+            notify: Some(Notifier::Messages)  // configured
+        })
+    );
+
+    assert_eq!(
+        tab_configs.get("server", Some(ChanNameRef::new("##rust"))),
+        None
+    );
+
+    assert_eq!(
+        tab_configs.get("server2", Some(ChanNameRef::new("#tiny2"))),
+        Some(TabConfig {
+            ignore: Some(true),          // overwritten by server
+            notify: Some(Notifier::Off)  // overwritten by defaults
+        })
+    );
+}
diff --git a/crates/libtiny_tui/src/tests/mod.rs b/crates/libtiny_tui/src/tests/mod.rs
index a0133f9c..4fe40582 100644
--- a/crates/libtiny_tui/src/tests/mod.rs
+++ b/crates/libtiny_tui/src/tests/mod.rs
@@ -9,6 +9,8 @@ use crate::tui::TUI;
 mod layout;
 mod resize;
 
+mod config;
+
 fn enter_string(tui: &mut TUI, s: &str) {
     for c in s.chars() {
         tui.handle_input_event(Event::Key(Key::Char(c)), &mut None);