diff --git a/tui/Cargo.toml b/tui/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..28195a29c3ac1c54e714069b3115c123eb525704
--- /dev/null
+++ b/tui/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "duniter-tui"
+version = "0.1.0"
+authors = ["librelois <elois@ifee.fr>"]
+description = "Terminal user interface for Duniter-Rs."
+license = "AGPL-3.0"
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+chrono = "0.4.2"
+duniter-conf = { path = "../conf" }
+duniter-crypto = { path = "../crypto" }
+duniter-dal = { path = "../dal" }
+duniter-documents = { path = "../documents" }
+duniter-message =  { path = "../message" }
+duniter-module = { path = "../module" }
+duniter-network = { path = "../network" }
+log = "0.4.1"
+serde_json = "1.0.9"
+termion = "1.5.1"
\ No newline at end of file
diff --git a/tui/lib.rs b/tui/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..55625209f54634d0efeba11bab539af3b06fbeb3
--- /dev/null
+++ b/tui/lib.rs
@@ -0,0 +1,655 @@
+//  Copyright (C) 2018  The Duniter Project Developers.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//! Defined the few global types used by all modules,
+//! as well as the DuniterModule trait that all modules must implement.
+
+#![cfg_attr(feature = "strict", deny(warnings))]
+#![deny(
+    missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
+    trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
+    unused_qualifications
+)]
+
+#[macro_use]
+extern crate log;
+
+extern crate chrono;
+extern crate duniter_conf;
+extern crate duniter_crypto;
+extern crate duniter_dal;
+extern crate duniter_documents;
+extern crate duniter_message;
+extern crate duniter_module;
+extern crate duniter_network;
+extern crate serde_json;
+extern crate termion;
+
+use chrono::prelude::*;
+use duniter_crypto::keys::ed25519;
+use duniter_dal::dal_event::DALEvent;
+use duniter_message::DuniterMessage;
+use duniter_module::*;
+use duniter_network::network_head::NetworkHead;
+use duniter_network::{NetworkEvent, NodeFullId};
+use std::collections::HashMap;
+use std::io::{stdout, Write};
+use std::sync::mpsc;
+use std::thread;
+use std::time::{Duration, SystemTime};
+use termion::event::*;
+use termion::input::{MouseTerminal, TermRead};
+use termion::raw::{IntoRawMode, RawTerminal};
+use termion::{clear, color, cursor, style};
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+/// Tui Module Configuration (For future use)
+pub struct TuiConf {}
+
+#[derive(Debug, Clone)]
+/// Format of messages received by the tui module
+pub enum TuiMess {
+    /// Message from another module
+    DuniterMessage(DuniterMessage),
+    /// Message from stdin (user event)
+    TermionEvent(Event),
+}
+
+#[derive(Debug, Copy, Clone)]
+/// Tui module
+pub struct TuiModule {}
+
+#[derive(Debug, Clone)]
+/// Network connexion (data to display)
+pub struct Connection {
+    /// connexion status
+    status: u32,
+    /// Node uid at the other end of the connection (member nodes only)
+    uid: Option<String>,
+}
+
+#[derive(Debug, Clone)]
+/// Data that the Tui module needs to cache
+pub struct TuiModuleDatas {
+    /// Sender of all other modules
+    pub followers: Vec<mpsc::Sender<DuniterMessage>>,
+    /// HEADs cache content
+    pub heads_cache: HashMap<NodeFullId, NetworkHead>,
+    /// Position of the 1st head displayed on the screen
+    pub heads_index: usize,
+    /// Connections cache content
+    pub connections_status: HashMap<NodeFullId, Connection>,
+    /// Number of connections in `Established` status
+    pub established_conns_count: usize,
+    /// Position of the 1st connection displayed on the screen
+    pub conns_index: usize,
+}
+
+impl TuiModuleDatas {
+    /// Parse tui configuration
+    fn parse_tui_conf(_json_conf: &serde_json::Value) -> TuiConf {
+        TuiConf {}
+    }
+    /// Draw terminal
+    fn draw_term<W: Write>(
+        &self,
+        stdout: &mut RawTerminal<W>,
+        start_time: &DateTime<Utc>,
+        heads_cache: &HashMap<NodeFullId, NetworkHead>,
+        heads_index: usize,
+        out_connections_status: &HashMap<NodeFullId, Connection>,
+        _in_connections_status: &HashMap<NodeFullId, Connection>,
+        conns_index: usize,
+    ) {
+        // Get Terminal size
+        let (w, h) = termion::terminal_size().expect("Fail to get terminal size !");
+
+        // Prepare connections screen
+        let mut out_never_try_conns_count = 0;
+        let mut out_unreachable_conns_count = 0;
+        let mut out_trying_conns_count = 0;
+        let mut out_denial_conns_count = 0;
+        let mut out_disconnected_conns_count = 0;
+        let mut out_established_conns = Vec::new();
+        for (node_full_id, connection) in out_connections_status {
+            match connection.status {
+                0 => out_never_try_conns_count += 1,
+                2 | 4 => out_unreachable_conns_count += 1,
+                1 | 3 | 5 | 7 | 8 | 9 => out_trying_conns_count += 1,
+                10 => out_denial_conns_count += 1,
+                11 => out_disconnected_conns_count += 1,
+                12 => out_established_conns.push((node_full_id, connection.uid.clone())),
+                _ => {}
+            }
+        }
+
+        // Prepare HEADs screen
+        let mut heads = heads_cache.values().collect::<Vec<&NetworkHead>>();
+        heads.sort_unstable_by(|a, b| b.cmp(a));
+        let heads_index_max = if heads.len() > (h - 14) as usize {
+            heads.len() - (h - 14) as usize
+        } else {
+            0
+        };
+
+        // Clear term and reset background color
+        write!(
+            stdout,
+            "{}{}{}",
+            color::Bg(color::Black),
+            clear::All,
+            cursor::Goto(1, 1)
+        ).unwrap();
+
+        // Draw headers
+        let mut line = 1;
+        write!(
+            stdout,
+            "{}{}{} established connections : ",
+            cursor::Goto(1, line),
+            color::Fg(color::White),
+            out_established_conns.len()
+        ).unwrap();
+        line += 1;
+        write!(
+            stdout,
+            "{}{}{} NodeId-PubKey",
+            cursor::Goto(1, line),
+            color::Fg(color::White),
+            style::Italic,
+        ).unwrap();
+        line += 1;
+        write!(
+            stdout,
+            "{}{}/\\",
+            cursor::Goto(29, line),
+            color::Fg(color::Black),
+        ).unwrap();
+
+        // Draw inter-nodes established connections
+        if out_established_conns.is_empty() {
+            line += 1;
+            write!(
+                stdout,
+                "{}{}{}No established connections !",
+                cursor::Goto(2, line),
+                color::Fg(color::Red),
+                style::Bold,
+            ).unwrap();
+        } else {
+            let mut count_conns = 0;
+            let conns_index_use = if conns_index > (out_established_conns.len() - 5) {
+                out_established_conns.len() - 5
+            } else {
+                conns_index
+            };
+            for &(node_full_id, ref uid) in &out_established_conns[conns_index_use..] {
+                line += 1;
+                count_conns += 1;
+                write!(
+                    stdout,
+                    "{}{} {} {}",
+                    cursor::Goto(2, line),
+                    color::Fg(color::Green),
+                    node_full_id,
+                    uid.clone().unwrap_or_else(String::new),
+                ).unwrap();
+                if count_conns == 5 {
+                    line += 1;
+                    write!(
+                        stdout,
+                        "{}{}\\/",
+                        cursor::Goto(29, line),
+                        color::Fg(color::Black)
+                    ).unwrap();
+                    break;
+                }
+            }
+        }
+
+        // Draw number of conns per state
+        line += 1;
+        write!(
+            stdout,
+            "{}{}{} know endpoints : {} Never try, {} Unreach, {} on trial, {} Denial, {} Close.",
+            cursor::Goto(2, line),
+            color::Fg(color::Rgb(128, 128, 128)),
+            out_connections_status.len(),
+            out_never_try_conns_count,
+            out_unreachable_conns_count,
+            out_trying_conns_count,
+            out_denial_conns_count,
+            out_disconnected_conns_count,
+        ).unwrap();
+
+        // Draw separated line
+        line += 1;
+        let mut separated_line = String::with_capacity(w as usize);
+        for _ in 0..w as usize {
+            separated_line.push('-');
+        }
+        write!(
+            stdout,
+            "{}{}{}",
+            cursor::Goto(1, line),
+            color::Fg(color::White),
+            separated_line,
+        ).unwrap();
+
+        // Draw HEADs
+        line += 1;
+        write!(
+            stdout,
+            "{}{}{} HEADs :",
+            cursor::Goto(1, line),
+            color::Fg(color::White),
+            heads.len()
+        ).unwrap();
+        line += 1;
+        if heads_index > 0 {
+            write!(
+                stdout,
+                "{}{}/\\",
+                cursor::Goto(35, line),
+                color::Fg(color::Green),
+            ).unwrap();
+        } else {
+            write!(
+                stdout,
+                "{}{}/\\",
+                cursor::Goto(35, line),
+                color::Fg(color::Black),
+            ).unwrap();
+        }
+        line += 1;
+        write!(
+            stdout,
+            "{}{}Step NodeId-Pubkey BlockId-BlockHash    Soft:Ver     Pre [ Api ] MeR:MiR uid",
+            cursor::Goto(1, line),
+            color::Fg(color::White)
+        ).unwrap();
+        for head in &heads[heads_index..] {
+            if line < (h - 2) {
+                line += 1;
+                if head.step() == 0 {
+                    write!(
+                        stdout,
+                        "{}{}{}",
+                        cursor::Goto(1, line),
+                        color::Fg(color::Blue),
+                        head.to_human_string(w as usize),
+                    ).unwrap();
+                } else {
+                    write!(
+                        stdout,
+                        "{}{}{}",
+                        cursor::Goto(1, line),
+                        color::Fg(color::Green),
+                        head.to_human_string(w as usize),
+                    ).unwrap();
+                }
+            } else {
+                break;
+            }
+        }
+        line += 1;
+        if heads_index < heads_index_max {
+            write!(
+                stdout,
+                "{}{}\\/",
+                cursor::Goto(35, line),
+                color::Fg(color::Green),
+            ).unwrap();
+        } else {
+            write!(
+                stdout,
+                "{}{}\\/",
+                cursor::Goto(35, line),
+                color::Fg(color::Black),
+            ).unwrap();
+        }
+
+        // Draw footer
+        let runtime_in_secs = Utc::now().timestamp() - (*start_time).timestamp();
+        let runtime_str =
+            DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(runtime_in_secs, 0), Utc)
+                .format("%H:%M:%S")
+                .to_string();
+        write!(
+            stdout,
+            "{}{}{}runtime : {}",
+            cursor::Goto(1, h),
+            color::Bg(color::Blue),
+            color::Fg(color::White),
+            runtime_str,
+        ).unwrap();
+        write!(
+            stdout,
+            "{}{}{}q : quit{}",
+            cursor::Goto(w - 7, h),
+            color::Bg(color::Blue),
+            color::Fg(color::White),
+            cursor::Hide,
+        ).unwrap();
+
+        // Flush stdout (i.e. make the output appear).
+        stdout.flush().unwrap();
+    }
+}
+
+impl Default for TuiModule {
+    fn default() -> TuiModule {
+        TuiModule {}
+    }
+}
+
+impl DuniterModule<ed25519::PublicKey, ed25519::KeyPair, DuniterMessage> for TuiModule {
+    fn id() -> ModuleId {
+        ModuleId::Str("tui")
+    }
+    fn priority() -> ModulePriority {
+        ModulePriority::Recommended()
+    }
+    fn ask_required_keys() -> RequiredKeys {
+        RequiredKeys::None()
+    }
+    fn default_conf() -> serde_json::Value {
+        serde_json::Value::default()
+    }
+    fn start(
+        _soft_name: &str,
+        _soft_version: &str,
+        _keys: RequiredKeysContent<ed25519::PublicKey, ed25519::KeyPair>,
+        _conf: &DuniterConf,
+        module_conf: &serde_json::Value,
+        main_sender: mpsc::Sender<RooterThreadMessage<DuniterMessage>>,
+        load_conf_only: bool,
+    ) -> Result<(), ModuleInitError> {
+        let start_time: DateTime<Utc> = Utc::now();
+
+        // load conf
+        let _conf = TuiModuleDatas::parse_tui_conf(module_conf);
+        if load_conf_only {
+            return Ok(());
+        }
+
+        // Instanciate Tui module datas
+        let mut tui = TuiModuleDatas {
+            followers: Vec::new(),
+            heads_cache: HashMap::new(),
+            heads_index: 0,
+            connections_status: HashMap::new(),
+            established_conns_count: 0,
+            conns_index: 0,
+        };
+
+        // Create tui main thread channel
+        let (tui_sender, tui_receiver): (mpsc::Sender<TuiMess>, mpsc::Receiver<TuiMess>) =
+            mpsc::channel();
+
+        // Create proxy channel
+        let (proxy_sender, proxy_receiver): (
+            mpsc::Sender<DuniterMessage>,
+            mpsc::Receiver<DuniterMessage>,
+        ) = mpsc::channel();
+
+        // Launch a proxy thread that transform DuniterMessage() to TuiMess::DuniterMessage(DuniterMessage())
+        let tui_sender_clone = tui_sender.clone();
+        thread::spawn(move || {
+            // Send proxy sender to main
+            match main_sender.send(RooterThreadMessage::ModuleSender(proxy_sender)) {
+                Ok(_) => {
+                    debug!("Send tui sender to main thread.");
+                }
+                Err(_) => panic!("Fatal error : tui module fail to send is sender channel !"),
+            }
+            loop {
+                match proxy_receiver.recv() {
+                    Ok(message) => {
+                        match tui_sender_clone.send(TuiMess::DuniterMessage(message.clone())) {
+                            Ok(_) => {
+                                if let DuniterMessage::Stop() = message {
+                                    break;
+                                };
+                            }
+                            Err(_) => debug!(
+                                "tui proxy : fail to relay DuniterMessage to tui main thread !"
+                            ),
+                        }
+                    }
+                    Err(e) => {
+                        warn!("{}", e);
+                        break;
+                    }
+                }
+            }
+        });
+
+        // Enter raw mode.
+        //let mut stdout = stdout().into_raw_mode().unwrap();
+        let mut stdout = MouseTerminal::from(stdout().into_raw_mode().unwrap());
+
+        // Initial draw
+        let mut last_draw = SystemTime::now();
+        tui.draw_term(
+            &mut stdout,
+            &start_time,
+            &tui.heads_cache,
+            tui.heads_index,
+            &tui.connections_status,
+            &HashMap::with_capacity(0),
+            tui.conns_index,
+        );
+
+        // Launch stdin thread
+        let _stdin_thread = thread::spawn(move || {
+            // Get the standard input stream.
+            let stdin = std::io::stdin();
+            // Get stdin events
+            for c in stdin.events() {
+                match tui_sender.send(TuiMess::TermionEvent(
+                    c.expect("error to read stdin event !"),
+                )) {
+                    Ok(_) => {
+                        trace!("Send stdin event to tui main thread.");
+                    }
+                    Err(_) => {
+                        panic!("Fatal error : tui stdin thread module fail to send message !")
+                    }
+                }
+            }
+        });
+
+        // ui main loop
+        loop {
+            let mut user_event = false;
+            // Get messages
+            match tui_receiver.recv_timeout(Duration::from_millis(250)) {
+                Ok(ref message) => match message {
+                    &TuiMess::DuniterMessage(ref duniter_message) => match duniter_message {
+                        &DuniterMessage::Stop() => {
+                            writeln!(
+                                stdout,
+                                "{}{}{}{}{}",
+                                color::Fg(color::Reset),
+                                cursor::Goto(1, 1),
+                                color::Bg(color::Reset),
+                                cursor::Show,
+                                clear::All,
+                            ).unwrap();
+                            let _result_stop_propagation: Result<
+                                (),
+                                mpsc::SendError<DuniterMessage>,
+                            > = tui
+                                .followers
+                                .iter()
+                                .map(|f| f.send(DuniterMessage::Stop()))
+                                .collect();
+                            break;
+                        }
+                        &DuniterMessage::Followers(ref new_followers) => {
+                            info!("Tui module receive followers !");
+                            for new_follower in new_followers {
+                                debug!("TuiModule : push one follower.");
+                                tui.followers.push(new_follower.clone());
+                            }
+                        }
+                        &DuniterMessage::DALEvent(ref dal_event) => match dal_event {
+                            &DALEvent::StackUpValidBlock(ref _block) => {}
+                            &DALEvent::RevertBlocks(ref _blocks) => {}
+                            _ => {}
+                        },
+                        &DuniterMessage::NetworkEvent(ref network_event) => match network_event {
+                            &NetworkEvent::ConnectionStateChange(
+                                ref node_full_id,
+                                ref status,
+                                ref uid,
+                            ) => {
+                                if let Some(conn) = tui.connections_status.get(node_full_id) {
+                                    if *status == 12 && (*conn).status != 12 {
+                                        tui.established_conns_count += 1;
+                                    } else if *status != 12 && (*conn).status == 12 {
+                                        tui.established_conns_count -= 1;
+                                    }
+                                };
+                                tui.connections_status.insert(
+                                    *node_full_id,
+                                    Connection {
+                                        status: *status,
+                                        uid: uid.clone(),
+                                    },
+                                );
+                            }
+                            &NetworkEvent::ReceiveHeads(ref heads) => {
+                                heads
+                                    .iter()
+                                    .map(|h| tui.heads_cache.insert(h.node_full_id(), h.clone()))
+                                    .collect::<Vec<Option<NetworkHead>>>();
+                            }
+                            _ => {}
+                        },
+                        _ => {}
+                    },
+                    &TuiMess::TermionEvent(ref term_event) => match term_event {
+                        &Event::Key(Key::Char('q')) => {
+                            // Exit
+                            writeln!(
+                                stdout,
+                                "{}{}{}{}{}",
+                                color::Fg(color::Reset),
+                                cursor::Goto(1, 1),
+                                color::Bg(color::Reset),
+                                cursor::Show,
+                                clear::All,
+                            ).unwrap();
+                            let _result_stop_propagation: Result<
+                                (),
+                                mpsc::SendError<DuniterMessage>,
+                            > = tui
+                                .followers
+                                .iter()
+                                .map(|f| f.send(DuniterMessage::Stop()))
+                                .collect();
+                            break;
+                        }
+                        &Event::Mouse(ref me) => match me {
+                            &MouseEvent::Press(ref button, ref _a, ref b) => match button {
+                                &MouseButton::WheelDown => {
+                                    // Get Terminal size
+                                    let (_w, h) = termion::terminal_size()
+                                        .expect("Fail to get terminal size !");
+                                    if *b < 11 {
+                                        // conns_index
+                                        let conns_index_max = if tui.established_conns_count > 5 {
+                                            tui.established_conns_count - 5
+                                        } else {
+                                            0
+                                        };
+                                        if tui.heads_index < conns_index_max {
+                                            tui.conns_index += 1;
+                                            user_event = true;
+                                        } else {
+                                            tui.conns_index = conns_index_max;
+                                        }
+                                    } else {
+                                        // heads_index
+                                        if h > 16 {
+                                            let heads_index_max =
+                                                if tui.heads_cache.len() > (h - 16) as usize {
+                                                    tui.heads_cache.len() - (h - 16) as usize
+                                                } else {
+                                                    0
+                                                };
+                                            if tui.heads_index < heads_index_max {
+                                                tui.heads_index += 1;
+                                                user_event = true;
+                                            } else {
+                                                tui.heads_index = heads_index_max;
+                                            }
+                                        }
+                                    }
+                                }
+                                &MouseButton::WheelUp => {
+                                    if *b < 11 {
+                                        // conns_index
+                                        if tui.conns_index > 0 {
+                                            tui.conns_index -= 1;
+                                            user_event = true;
+                                        }
+                                    } else {
+                                        // heads_index
+                                        if tui.heads_index > 0 {
+                                            tui.heads_index -= 1;
+                                            user_event = true;
+                                        }
+                                    }
+                                }
+                                _ => {}
+                            },
+                            &MouseEvent::Release(ref _a, ref _b)
+                            | &MouseEvent::Hold(ref _a, ref _b) => {}
+                        },
+                        _ => {}
+                    },
+                },
+                Err(e) => match e {
+                    mpsc::RecvTimeoutError::Disconnected => {
+                        panic!("Disconnected tui module !");
+                    }
+                    mpsc::RecvTimeoutError::Timeout => {}
+                },
+            }
+            let now = SystemTime::now();
+            if user_event
+                || now
+                    .duration_since(last_draw)
+                    .expect("Tui : Fatal error : fail to get duration since last draw !")
+                    .subsec_nanos() > 250_000_000
+            {
+                last_draw = now;
+                tui.draw_term(
+                    &mut stdout,
+                    &start_time,
+                    &tui.heads_cache,
+                    tui.heads_index,
+                    &tui.connections_status,
+                    &HashMap::with_capacity(0),
+                    tui.conns_index,
+                );
+            }
+        }
+        Ok(())
+    }
+}