diff --git a/src/rust/amadeus-rs/.gitignore b/src/rust/amadeus-rs/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba
--- /dev/null
+++ b/src/rust/amadeus-rs/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/src/rust/amadeus-rs/.gitlab-ci.yml b/src/rust/amadeus-rs/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1d9669e1ad5d4dc7c17b7a827ec4dc0f04b0dd16
--- /dev/null
+++ b/src/rust/amadeus-rs/.gitlab-ci.yml
@@ -0,0 +1,10 @@
+image: "rust:latest"
+
+before_script:
+  - apt-get update -yqq
+  - apt-get install -yqq --no-install-recommends build-essential libxcb-render0-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-xfixes0-dev 
+
+test:cargo:
+  script:
+    - rustc --version && cargo --version
+    - cargo test --verbose
diff --git a/src/rust/amadeus-rs/Cargo.toml b/src/rust/amadeus-rs/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..e07989ee38057ea5d190a53a831bc7eaf76f1f76
--- /dev/null
+++ b/src/rust/amadeus-rs/Cargo.toml
@@ -0,0 +1,7 @@
+[workspace]
+members = [
+    "amadeus",
+    "amadeus-macro",
+    "lkt-rs",
+    "lkt-lib"
+]
\ No newline at end of file
diff --git a/src/rust/amadeus-rs/LICENSE b/src/rust/amadeus-rs/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..14d093867d2df165a8eb9060c47071f471d171e6
--- /dev/null
+++ b/src/rust/amadeus-rs/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Maël MARTIN
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/rust/amadeus-rs/README.md b/src/rust/amadeus-rs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8544a75dcf2ebfa29a438bc2a3475b68ee01e49a
--- /dev/null
+++ b/src/rust/amadeus-rs/README.md
@@ -0,0 +1,10 @@
+# Amadeus RS
+
+Amadeus, the rust version.
+
+## Build
+
+You need a rust toolchain. When cloned just enter `cargo build` to build the
+application. You will need some XCB libraries on Linux. On Ubuntu you will need
+the following packets : `libxcb-render0-dev`, `libxcb-render-util0-dev`,
+`libxcb-shape0-dev`, `libxcb-xfixes0-dev`
diff --git a/src/rust/amadeus-rs/amadeus-macro/Cargo.toml b/src/rust/amadeus-rs/amadeus-macro/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..7671b6f7e25463f99d7aea99467a1542185a926e
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus-macro/Cargo.toml
@@ -0,0 +1,5 @@
+[package]
+name    = "amadeus_macro"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
\ No newline at end of file
diff --git a/src/rust/amadeus-rs/amadeus-macro/src/lib.rs b/src/rust/amadeus-rs/amadeus-macro/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7e6a7e6107c33f1c24d0414d852f628a64e36443
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus-macro/src/lib.rs
@@ -0,0 +1,19 @@
+// https://doc.rust-lang.org/reference/macros-by-example.html
+
+#[macro_export]
+macro_rules! either {
+    ($test:expr => $true_expr:expr; $false_expr:expr) => {
+        if $test {
+            $true_expr
+        } else {
+            $false_expr
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! lkt_command_from_str {
+    ($lit:literal) => {
+        concat!($lit, '\n').to_owned()
+    };
+}
diff --git a/src/rust/amadeus-rs/amadeus/Cargo.toml b/src/rust/amadeus-rs/amadeus/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..cdea7bab694b64ad3539838cf85cf70e0b2e1eef
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name    = "amadeus"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+
+[dependencies]
+lkt_lib       = { path = "../lkt-lib" }
+amadeus_macro = { path = "../amadeus-macro" }
+serde         = { version = "1", default-features = false,     features = [ "derive", "std" ] }
+serde_json    = { version = "1", default-features = false,     features = [ "std" ] }
+eframe        = { version = "0.17.0",                          features = [ "persistence" ] }
+image         = { version = "^0.24", default-features = false, features = [ "jpeg", "ico", "png" ] }
+egui          = { version = "0.17.0",                          features = [ "extra_debug_asserts", "extra_asserts", "serde", "persistence" ] }
+epi           = { version = "0.17.0",                          features = [ "persistence" ] }
+log           = { version = "0.4" }
+lazy_static   = "1"
\ No newline at end of file
diff --git a/src/rust/amadeus-rs/amadeus/src/action/action.rs b/src/rust/amadeus-rs/amadeus/src/action/action.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2b9931314cc398672b9e53815dbf47e132a2909e
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/action/action.rs
@@ -0,0 +1,99 @@
+use std::fmt;
+
+#[derive(Clone)]
+pub enum Action {
+    /// Resume playback from that kara.
+    PlayFromKara,
+
+    /// Pop the kara out of the queue.
+    DeleteKaraFromQueue,
+
+    /// Insert the kara in the queue. The kara will be inserted at the top of
+    /// the queue, but after all the other inserted karas.
+    InsertKaraInQueue,
+
+    /// Add the kara at the end of the playlist.
+    AddKaraToQueue,
+
+    /// Add the kara to a playlist. The playlist will be selected by the user in
+    /// a pop-up or something like that.
+    AddKaraToPlaylist,
+
+    /// Action to delete a kara from a playlist. The passed `u64` is the unique
+    /// id of the playlist, the internal one.
+    DeleteKaraFromPlaylist(u64),
+
+    /// Open the playlist in a window for the user to browse.
+    OpenPlaylist,
+
+    /// Add all the content of a playlist to the queue.
+    AddPlaylistToQueue,
+
+    /// Insert all the content of a playlist in the queue.
+    InsertPlaylistToQueue,
+
+    /// Clear the content of the playlist, delete all the contained karas.
+    ClearPlaylistContent,
+
+    /// Connect to lektord.
+    ConnectToLektord,
+
+    /// Disconnect from lektord.
+    DisconnectFromLektord,
+
+    PlaybackPrevious,
+    PlaybackPlay,
+    PlaybackPause,
+    PlaybackNext,
+}
+
+impl fmt::Debug for Action {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::PlayFromKara => write!(f, "PlayFromKara"),
+            Self::DeleteKaraFromQueue => write!(f, "DeleteKaraFromQueue"),
+            Self::InsertKaraInQueue => write!(f, "InsertKaraInQueue"),
+            Self::AddKaraToQueue => write!(f, "AddKaraToQueue"),
+            Self::AddKaraToPlaylist => write!(f, "AddKaraToPlaylist"),
+            Self::DeleteKaraFromPlaylist(arg0) => {
+                f.debug_tuple("DeleteKaraFromPlaylist").field(arg0).finish()
+            }
+            Self::OpenPlaylist => write!(f, "OpenPlaylist"),
+            Self::AddPlaylistToQueue => write!(f, "AddPlaylistToQueue"),
+            Self::InsertPlaylistToQueue => write!(f, "InsertPlaylistToQueue"),
+            Self::ClearPlaylistContent => write!(f, "ClearPlaylistContent"),
+            Self::ConnectToLektord => write!(f, "ConnectToLektord"),
+            Self::DisconnectFromLektord => write!(f, "DisconnectFromLektord"),
+            Self::PlaybackPrevious => write!(f, "PlaybackPrevious"),
+            Self::PlaybackPlay => write!(f, "PlaybackPlay"),
+            Self::PlaybackPause => write!(f, "PlaybackPause"),
+            Self::PlaybackNext => write!(f, "PlaybackNext"),
+        }
+    }
+}
+
+pub fn get_card_action_name(act: &Action) -> &'static str {
+    use Action::*;
+    return match act {
+        PlayFromKara => "Play from that kara",
+        DeleteKaraFromQueue => "Delete from the queue",
+
+        InsertKaraInQueue => "Insert in the queue",
+        AddKaraToQueue => "Add to the queue",
+        AddKaraToPlaylist => "Add to playlist",
+
+        DeleteKaraFromPlaylist(_) => "Delete from playlist",
+        OpenPlaylist => "Open playlist",
+        AddPlaylistToQueue => "Add playlist to the queue",
+        InsertPlaylistToQueue => "Insert the playlist in the queue",
+        ClearPlaylistContent => "Clear the playlist content",
+
+        ConnectToLektord => "Connect to lektord",
+        DisconnectFromLektord => "Disconnect from lektord",
+
+        PlaybackPrevious => "Previous",
+        PlaybackPlay => "Play",
+        PlaybackPause => "Pause",
+        PlaybackNext => "Next",
+    };
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/action/menu.rs b/src/rust/amadeus-rs/amadeus/src/action/menu.rs
new file mode 100644
index 0000000000000000000000000000000000000000..828d4057e836ff20ee2420af295fb63112afddbb
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/action/menu.rs
@@ -0,0 +1,23 @@
+use super::action::get_card_action_name;
+use super::Action;
+use crate::utils;
+
+pub fn render_action_menu(
+    ui: &mut egui::Ui,
+    actions: &Vec<Action>,
+    menu_name: impl Into<egui::WidgetText>,
+    fullfilled: &mut Vec<Action>,
+) {
+    ui.menu_button(menu_name, |ui| {
+        ui.style_mut().override_text_style = Some(utils::font::body());
+        for act in actions {
+            let button = egui::Button::new(get_card_action_name(act))
+                .wrap(false)
+                .frame(false);
+            if ui.add(button).clicked() {
+                fullfilled.push(act.clone());
+                ui.close_menu();
+            }
+        }
+    });
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/action/mod.rs b/src/rust/amadeus-rs/amadeus/src/action/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c24c5a8375a960113676566586e00b3f8d05dc3e
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/action/mod.rs
@@ -0,0 +1,5 @@
+mod action;
+mod menu;
+
+pub use action::*;
+pub use menu::render_action_menu;
diff --git a/src/rust/amadeus-rs/amadeus/src/amadeus.rs b/src/rust/amadeus-rs/amadeus/src/amadeus.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f3c857bb7c9c2cfeff4efb062a1a9f7684358e30
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/amadeus.rs
@@ -0,0 +1,528 @@
+// https://docs.rs/epi/0.17.0/epi/trait.App.html
+// https://docs.rs/egui/0.17.0/egui/
+// https://docs.rs/egui/0.17.0/egui/struct.FontDefinitions.html
+
+use crate::{
+    action,
+    cards::*,
+    constants, playlists,
+    utils::{self, deamon::Deamon},
+    widgets,
+};
+use amadeus_macro::either;
+use eframe::{
+    egui,
+    epi::{self, App},
+};
+use lkt_lib::{
+    query::LektorQuery,
+    response::LektorFormatedResponse,
+    types::{LektorState, Playlist},
+};
+use log::debug;
+use std::{
+    sync::mpsc::{Receiver, Sender},
+    time,
+};
+
+pub struct Amadeus<'a> {
+    config: utils::AmadeusConfig,
+    has_config_changed: bool,
+    need_about_window: bool,
+    need_settings_window: bool,
+    actions: Vec<action::Action>,
+
+    last_render_instant: time::SystemTime,
+    begin_render_instant: time::SystemTime,
+
+    deamon: Option<(
+        (Sender<LektorQuery>, Receiver<LektorFormatedResponse>),
+        utils::deamon::CommandDeamon,
+    )>,
+    status_deamon: Option<(
+        (Receiver<utils::deamon::StatusDeamonMessageType>,),
+        utils::deamon::StatusDeamon,
+    )>,
+
+    lektord_current_kara: Option<KaraCard>,
+    lektord_queue: KaraCardCollection<'a>,
+    lektord_historic: KaraCardCollection<'a>,
+
+    lektord_search_results: KaraCardCollection<'a>,
+    lektord_search_query: String,
+    lektord_updated_query: bool,
+
+    playlist_store: playlists::PlaylistsStore,
+
+    lektord_state: LektorState,
+
+    amadeus_logo_texture: Option<egui::TextureHandle>,
+}
+
+impl Default for Amadeus<'_> {
+    fn default() -> Self {
+        Self {
+            last_render_instant: time::SystemTime::now(),
+            begin_render_instant: time::SystemTime::UNIX_EPOCH,
+
+            lektord_queue: KaraCardCollection::new("Queue".to_owned())
+                .add_action(action::Action::PlayFromKara)
+                .add_action(action::Action::DeleteKaraFromQueue)
+                .add_action(action::Action::AddKaraToPlaylist),
+            lektord_historic: KaraCardCollection::new("Historic".to_owned())
+                .add_action(action::Action::AddKaraToQueue)
+                .add_action(action::Action::InsertKaraInQueue)
+                .add_action(action::Action::DeleteKaraFromQueue)
+                .add_action(action::Action::AddKaraToPlaylist),
+            lektord_search_results: KaraCardCollection::new("Search results".to_owned())
+                .add_action(action::Action::AddKaraToQueue)
+                .add_action(action::Action::InsertKaraInQueue)
+                .add_action(action::Action::DeleteKaraFromQueue)
+                .add_action(action::Action::AddKaraToPlaylist),
+            lektord_search_query: String::new(),
+
+            actions: Vec::with_capacity(10),
+            playlist_store: Default::default(),
+            config: Default::default(),
+            has_config_changed: Default::default(),
+            need_about_window: Default::default(),
+            need_settings_window: Default::default(),
+            deamon: Default::default(),
+            status_deamon: Default::default(),
+            lektord_current_kara: Default::default(),
+            lektord_updated_query: Default::default(),
+            lektord_state: Default::default(),
+            amadeus_logo_texture: Default::default(),
+        }
+    }
+}
+
+impl Amadeus<'_> {
+    pub fn create() -> Box<Self> {
+        Box::new(Amadeus::default())
+    }
+
+    fn load_amadeus_logo(&mut self, ctx: &egui::Context) {
+        let (logo_buffer, [size_x, size_y]) = utils::get_icon_as_dynamic_image();
+        let pixels = logo_buffer.as_flat_samples();
+        let logo_texture = egui::ColorImage::from_rgba_unmultiplied(
+            [size_x as usize, size_y as usize],
+            pixels.as_slice(),
+        );
+        self.amadeus_logo_texture = Some(ctx.load_texture("amadeus-logo", logo_texture));
+    }
+
+    fn collect_fulfilled_actions(&mut self) -> Vec<(u64, action::Action)> {
+        let mut ret = self.lektord_queue.fulfilled_actions();
+        ret.extend(self.lektord_historic.fulfilled_actions());
+        ret.extend(self.lektord_search_results.fulfilled_actions());
+        ret.extend(self.playlist_store.fulfilled_actions());
+        return ret;
+    }
+
+    fn render_side_panel_main_view_button(
+        &mut self,
+        ui: &mut egui::Ui,
+        title: &str,
+        view: utils::AmadeusMainView,
+    ) {
+        ui.add_space(constants::PADDING);
+        let selected = view == self.config.main_panel_view;
+        if selected {
+            ui.colored_label(constants::get_text_color(self.config.dark_mode), title);
+        } else {
+            let the_btn = egui::Button::new(title).frame(false);
+            if ui.add(the_btn).clicked() {
+                self.config.main_panel_view = view;
+            }
+        }
+    }
+
+    fn render_side_panel(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
+        if self.config.side_panel_show {
+            egui::SidePanel::left("LEFT_PANEL")
+                .resizable(true)
+                .show(ctx, |ui| {
+                    ui.vertical(|ui| {
+                        use utils::AmadeusMainView::*;
+                        ui.style_mut().override_text_style = Some(utils::font::heading1());
+                        self.render_side_panel_main_view_button(ui, "🎵 Queue", Queue);
+                        self.render_side_panel_main_view_button(ui, "📚 Historic", Historic);
+                        self.render_side_panel_main_view_button(ui, "🔍 Search", SearchResults);
+                        ui.add_space(constants::PADDING);
+                        ui.style_mut().override_text_style = Some(utils::font::body());
+                        ui.separator();
+                    });
+                    self.playlist_store.render(ui, self.config.dark_mode);
+                });
+        }
+    }
+
+    fn render_central_panel(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
+        egui::CentralPanel::default().show(ctx, |ui| {
+            use utils::AmadeusMainView::*;
+            match self.config.main_panel_view {
+                Queue => self.lektord_queue.render(ui, self.config.dark_mode),
+                Historic => self.lektord_historic.render(ui, self.config.dark_mode),
+                SearchResults => {
+                    ui.style_mut().override_text_style = Some(utils::font::heading1());
+                    ui.label("Database search query");
+                    ui.add_space(constants::PADDING);
+
+                    let response = ui.add(
+                        egui::TextEdit::singleline(&mut self.lektord_search_query)
+                            .desired_width(f32::INFINITY),
+                    );
+                    self.lektord_updated_query |=
+                        response.lost_focus() || ui.input().key_pressed(egui::Key::Enter);
+                    ui.add_space(constants::PADDING * 2.);
+
+                    self.lektord_search_results
+                        .render(ui, self.config.dark_mode);
+                }
+            }
+        });
+    }
+
+    fn render_bottom_panel(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
+        let seconds = time::SystemTime::now()
+            .duration_since(time::SystemTime::UNIX_EPOCH)
+            .unwrap()
+            .as_secs()
+            % 90;
+        let progress = seconds as f32 / 90 as f32;
+        let text_duration = {
+            let duration = time::Duration::from_secs(seconds);
+            let mins = duration.as_secs() / 60;
+            let secs = duration.as_secs() % 60;
+            format!("{:02}:{:02}", mins, secs)
+        };
+
+        egui::TopBottomPanel::bottom("FOOTER")
+            .max_height(constants::BOTTOM_PANEL_MAX_SIZE)
+            .min_height(constants::BOTTOM_PANEL_MAX_SIZE)
+            .resizable(false)
+            .show(ctx, |ui| {
+                ui.horizontal(|ui| {
+                    if let Some(current_kara) = &self.lektord_current_kara {
+                        current_kara.render_compact(ui, self.config.dark_mode);
+                    }
+                    ui.with_layout(egui::Layout::right_to_left(), |ui| {
+                        ui.style_mut().override_text_style = Some(utils::font::heading3());
+                        ui.add_space(constants::PADDING * 2.);
+                        ui.label(text_duration);
+                    });
+                });
+                let style = ui.style().clone();
+                ui.style_mut().override_text_style = Some(utils::font::small());
+                ui.style_mut().visuals.override_text_color =
+                    Some(constants::get_text_color(self.config.dark_mode));
+                ui.add(widgets::progress_bar(self.config.dark_mode, progress));
+                ui.set_style(style);
+            });
+    }
+
+    fn render_top_panel(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
+        egui::TopBottomPanel::top("MENU").show(ctx, |ui| {
+            ui.add_space(constants::TOP_PANEL_PADDING * 2.);
+            egui::menu::bar(ui, |ui| {
+                ui.with_layout(egui::Layout::left_to_right(), |ui| {
+                    ui.style_mut().override_text_style = Some(utils::font::heading1());
+                    if ui.add(egui::Button::new("Amadeus").frame(false)).clicked() {
+                        self.config.side_panel_show = !self.config.side_panel_show;
+                    }
+
+                    ui.add(egui::Separator::default());
+                    egui::menu::menu_button(ui, "⚡", |ui| {
+                        ui.style_mut().override_text_style = Some(utils::font::body());
+                        ui.add(egui::Button::new("Connect lektord").wrap(false))
+                            .clicked()
+                            .then(|| {
+                                self.actions.push(action::Action::ConnectToLektord);
+                                ui.close_menu();
+                            });
+                        ui.add(egui::Button::new("Disconnect lektord").wrap(false))
+                            .clicked()
+                            .then(|| {
+                                self.actions.push(action::Action::DisconnectFromLektord);
+                                ui.close_menu();
+                            });
+                    })
+                    .response
+                    .on_hover_text("Action menu");
+                    ui.add(egui::Separator::default());
+
+                    ui.button("⚙")
+                        .on_hover_text("Settings window")
+                        .clicked()
+                        .then(|| self.need_settings_window = true);
+
+                    ui.button("☕")
+                        .on_hover_text("About window")
+                        .clicked()
+                        .then(|| self.need_about_window = true);
+
+                    ui.add(egui::Separator::default());
+                    ui.button("⏪")
+                        .on_hover_text("Previous")
+                        .clicked()
+                        .then(|| self.actions.push(action::Action::PlaybackPrevious));
+                    match self.lektord_state {
+                        LektorState::Stopped | LektorState::Pause(_) => ui
+                            .button("▶")
+                            .on_hover_text("Play")
+                            .clicked()
+                            .then(|| self.actions.push(action::Action::PlaybackPlay)),
+                        LektorState::Play(_) => ui
+                            .button("⏸")
+                            .on_hover_text("Pause")
+                            .clicked()
+                            .then(|| self.actions.push(action::Action::PlaybackPause)),
+                    };
+                    ui.button("⏩")
+                        .on_hover_text("Next")
+                        .clicked()
+                        .then(|| self.actions.push(action::Action::PlaybackNext));
+                });
+
+                ui.with_layout(egui::Layout::right_to_left(), |ui| {
+                    ui.style_mut().override_text_style = Some(utils::font::body());
+                    if ui.add(egui::Button::new("❌")).clicked() {
+                        frame.quit();
+                    }
+
+                    ui.style_mut().override_text_style = Some(utils::font::small_body());
+                    let frame_duration = self
+                        .begin_render_instant
+                        .duration_since(self.last_render_instant)
+                        .unwrap_or_default()
+                        .as_millis();
+                    let fps = 1000. / frame_duration as f64;
+                    ui.label(format!("{fps:02.1}fps | {frame_duration:02}ms"));
+                    let (drag_id, drag_space) = ui.allocate_space(ui.available_size());
+                    ui.interact(drag_space, drag_id, egui::Sense::drag())
+                        .dragged()
+                        .then(|| frame.drag_window());
+                });
+            });
+            ui.add_space(constants::TOP_PANEL_PADDING);
+        });
+    }
+
+    fn set_visuals(&mut self, ctx: &egui::Context) {
+        ctx.request_repaint();
+        let mut visuals =
+            either!(self.config.dark_mode => egui::Visuals::dark(); egui::Visuals::light());
+
+        visuals.window_rounding = egui::Rounding::none();
+        visuals.widgets.active.rounding = egui::Rounding::none();
+        visuals.widgets.noninteractive.rounding = egui::Rounding::none();
+        visuals.widgets.inactive.rounding = egui::Rounding::none();
+        visuals.widgets.hovered.rounding = egui::Rounding::none();
+        visuals.widgets.open.rounding = egui::Rounding::none();
+
+        visuals.hyperlink_color = constants::get_accent_color(self.config.dark_mode);
+
+        ctx.set_visuals(visuals);
+    }
+
+    fn handle_action(&mut self) {
+        use action::Action::*;
+
+        // Handle actions on lektor items
+        for (id, act) in self.collect_fulfilled_actions() {
+            match act {
+                OpenPlaylist => self.playlist_store.show_playlist(id),
+                ClearPlaylistContent => self.playlist_store.clear_playlist(id),
+                _ => debug!("Execute action {act:?} on lektor item with id {id}"),
+            }
+        }
+
+        // Handle top level actions
+        for act in self.actions.drain(..) {
+            match act {
+                ConnectToLektord => {
+                    if !self.status_deamon.is_some() {
+                        let connexion = utils::deamon::StatusDeamon::spawn(
+                            self.config.lektord_hostname.clone(),
+                            self.config.lektord_port.as_integer() as i16,
+                        );
+                        if let Ok(connexion) = connexion {
+                            self.status_deamon = Some(connexion);
+                        }
+                    }
+                    if !self.deamon.is_some() {
+                        let connexion = utils::deamon::CommandDeamon::spawn(
+                            self.config.lektord_hostname.clone(),
+                            self.config.lektord_port.as_integer() as i16,
+                        );
+                        if let Ok(connexion) = connexion {
+                            self.deamon = Some(connexion);
+                        }
+                    }
+                }
+                DisconnectFromLektord => {
+                    if let Some((_, status_deamon)) = &self.status_deamon {
+                        status_deamon.quit();
+                    }
+                    if let Some((_, deamon)) = &self.deamon {
+                        deamon.quit();
+                    }
+                }
+                _ => debug!("Execute action {act:?} on lektor"),
+            }
+        }
+
+        // Handle the deamon closing process.
+        if let Some((_, status_deamon)) = &self.status_deamon {
+            if status_deamon.should_joined() {
+                status_deamon.join();
+                self.status_deamon = None;
+            }
+        };
+        if let Some((_, deamon)) = &self.deamon {
+            if deamon.should_joined() {
+                deamon.join();
+                self.deamon = None;
+            }
+        };
+    }
+
+    fn apply_settings(&mut self) {
+        self.lektord_queue
+            .with_max_content(self.config.ui_max_queue_items.as_integer());
+        self.lektord_historic
+            .with_max_content(self.config.ui_max_history_items.as_integer());
+        self.lektord_search_results
+            .with_max_content(self.config.ui_max_search_items.as_integer());
+    }
+
+    fn fill_debug_values(&mut self) {
+        let default_kara_card = KaraCard::new(lkt_lib::types::Kara {
+            id: 1000,
+            source_name: "Totoro".to_owned(),
+            song_type: "OP".to_owned(),
+            language: "jp".to_owned(),
+            category: "vo".to_owned(),
+            title: "Totoro no Sampo".to_owned(),
+            song_number: Some(1),
+            author: "Geralt".to_owned(),
+            is_available: false,
+        });
+
+        self.lektord_current_kara = Some(KaraCard::new(lkt_lib::types::Kara {
+            id: 3000,
+            source_name: "Umineko no Naku Koro ni Saku".to_owned(),
+            song_type: "ED".to_owned(),
+            language: "jp".to_owned(),
+            category: "cdg".to_owned(),
+            title: "Birth of a New Witdh".to_owned(),
+            song_number: Some(6),
+            author: "Kubat".to_owned(),
+            is_available: true,
+        }));
+
+        self.lektord_queue
+            .add_card(default_kara_card.clone())
+            .add_card(default_kara_card.clone());
+        for _i in 1..=50 {
+            self.lektord_queue.add_card(default_kara_card.clone());
+        }
+
+        let mut some_playlist = KaraCardCollection::new("@Kubat".to_string());
+        some_playlist.add_card(default_kara_card.clone());
+
+        self.playlist_store.create(Playlist {
+            id: 1,
+            name: "@Kubat".to_owned(),
+        });
+        self.playlist_store.create(Playlist {
+            id: 2,
+            name: "@Geralt".to_owned(),
+        });
+        self.playlist_store.create(Playlist {
+            id: 3,
+            name: "@Elliu".to_owned(),
+        });
+        self.playlist_store.add("@Kubat", default_kara_card.inner);
+    }
+}
+
+impl App for Amadeus<'_> {
+    fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
+        self.begin_render_instant = time::SystemTime::now();
+        ctx.request_repaint();
+        self.set_visuals(ctx);
+
+        self.render_top_panel(ctx, frame);
+        self.render_side_panel(ctx, frame);
+        self.render_central_panel(ctx, frame);
+        self.render_bottom_panel(ctx, frame);
+
+        self.playlist_store
+            .render_playlist_contents(ctx, self.config.dark_mode);
+        widgets::render_about_window(
+            &mut self.need_about_window,
+            self.config.dark_mode,
+            ctx,
+            &self.amadeus_logo_texture.as_ref().unwrap(),
+        );
+        self.config.render_settings_window(
+            ctx,
+            &mut self.need_settings_window,
+            &mut self.has_config_changed,
+        );
+
+        self.last_render_instant = self.begin_render_instant;
+
+        self.handle_action();
+
+        if self.has_config_changed {
+            self.apply_settings();
+        }
+    }
+
+    fn setup(
+        &mut self,
+        ctx: &egui::Context,
+        _frame: &epi::Frame,
+        maybe_storage: Option<&dyn epi::Storage>,
+    ) {
+        if let Some(storage) = maybe_storage {
+            self.config = eframe::epi::get_value(storage, "amadeus-rs-config").unwrap_or_default();
+        }
+
+        ctx.set_fonts(utils::font::get_font_definitions());
+        self.load_amadeus_logo(ctx);
+
+        let mut style = (*ctx.style()).clone();
+        style.text_styles = utils::font::get_font_styles();
+        ctx.set_style(style);
+
+        self.apply_settings();
+        self.fill_debug_values();
+    }
+
+    fn save(&mut self, storage: &mut dyn eframe::epi::Storage) {
+        if self.has_config_changed {
+            eframe::epi::set_value(storage, "amadeus-rs-config", &self.config);
+            self.has_config_changed = false;
+        }
+    }
+
+    fn persist_egui_memory(&self) -> bool {
+        true
+    }
+
+    fn persist_native_window(&self) -> bool {
+        true
+    }
+
+    fn auto_save_interval(&self) -> time::Duration {
+        time::Duration::from_secs(1)
+    }
+
+    fn name(&self) -> &str {
+        "Amadeus"
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/cards/card.rs b/src/rust/amadeus-rs/amadeus/src/cards/card.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b3645f253037ba95fbe53e2f24464b742b21e84f
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/cards/card.rs
@@ -0,0 +1,224 @@
+use crate::{action, constants, utils};
+use amadeus_macro::either;
+use lkt_lib::types::*;
+
+/// A simple card trait
+pub trait Card<'a, LktType: LektorType<'a>>: ToString + Clone {
+    /// Create an instance of the card from the lektor type
+    fn new(lkt_type: LktType) -> Self;
+
+    /// Render the card to the screen
+    fn render(self: &mut Self, ui: &mut egui::Ui, dark_mode: bool, actions: &Vec<action::Action>);
+
+    /// Render the card to the screen. Variant to try to render the element in a
+    /// more compact way.
+    fn render_compact(self: &Self, ui: &mut egui::Ui, dark_mode: bool);
+
+    /// Returns the Ids of the activated actions.
+    fn fulfilled_actions(&mut self) -> Vec<action::Action>;
+
+    /// Get the unique id from the inner lkt type.
+    fn unique_id(&self) -> u64;
+
+    /// The card can indicate if it need to be separator by separators
+    const NEED_SEPARATOR: bool;
+
+    /// Indicate if there is a need to have a separator between the header and
+    /// the first row.
+    const NEED_HEADER_SEPARATOR: bool;
+
+    /// Set the spacing to insert after the last card is rendered.
+    const BOTTOM_SPACE: Option<f32>;
+}
+
+#[derive(Clone)]
+pub struct KaraCard {
+    pub inner: Kara,
+    actions: Vec<action::Action>,
+}
+
+#[derive(Clone)]
+pub struct PlaylistCard {
+    pub inner: Playlist,
+    actions: Vec<action::Action>,
+}
+
+impl ToString for KaraCard {
+    fn to_string(&self) -> String {
+        format!(
+            "{} - {} / {} - {}{} - {} [{}]{}",
+            self.inner.category,
+            self.inner.language,
+            self.inner.source_name,
+            self.inner.song_type,
+            match self.inner.song_number {
+                Some(num) => num.to_string(),
+                None => "".to_string(),
+            },
+            self.inner.title,
+            self.inner.author,
+            either!(self.inner.is_available => ""; " (unavailable)")
+        )
+    }
+}
+
+impl ToString for PlaylistCard {
+    fn to_string(&self) -> String {
+        format!("{} [#{}]", self.inner.name, self.inner.id)
+    }
+}
+
+impl Card<'_, Kara> for KaraCard {
+    const NEED_SEPARATOR: bool = true;
+    const NEED_HEADER_SEPARATOR: bool = true;
+    const BOTTOM_SPACE: Option<f32> = Some(constants::BOTTOM_PANEL_MAX_SIZE);
+
+    fn new(inner: Kara) -> Self {
+        let mut actions = Vec::new();
+        actions.reserve(5);
+        Self { inner, actions }
+    }
+
+    fn render(&mut self, ui: &mut egui::Ui, dark_mode: bool, actions: &Vec<action::Action>) {
+        ui.add_space(constants::PADDING);
+        static MIN_WIDTH_FOR_ADDITIONAL_INFOS: f32 = 1024.;
+        let song = format!(
+            "{} - {}{} - {}",
+            self.inner.source_name,
+            self.inner.song_type,
+            match self.inner.song_number {
+                Some(num) => num.to_string(),
+                None => "".to_string(),
+            },
+            self.inner.title
+        );
+        ui.horizontal(|ui| {
+            ui.add_space(constants::PADDING * 3.);
+            let left_space = ui.available_width();
+            ui.vertical(|ui| {
+                ui.horizontal(|ui| {
+                    ui.style_mut().override_text_style = Some(utils::font::body());
+                    ui.colored_label(
+                        constants::get_text_color(dark_mode),
+                        format!("{} - {}", self.inner.category, self.inner.language),
+                    );
+                    if left_space >= MIN_WIDTH_FOR_ADDITIONAL_INFOS {
+                        ui.with_layout(egui::Layout::right_to_left(), |ui| {
+                            ui.label(format!("kara by {}", self.inner.author));
+                        });
+                    }
+                });
+                ui.horizontal(|ui| {
+                    ui.style_mut().override_text_style = Some(utils::font::heading2());
+                    action::render_action_menu(ui, actions, "▶", &mut self.actions);
+                    ui.colored_label(constants::get_text_color(dark_mode), song);
+                    ui.style_mut().override_text_style = Some(utils::font::body());
+                    if left_space >= MIN_WIDTH_FOR_ADDITIONAL_INFOS {
+                        ui.with_layout(egui::Layout::right_to_left(), |ui| {
+                            if !self.inner.is_available {
+                                ui.colored_label(
+                                    constants::get_accent_color(dark_mode),
+                                    " (unavailable)",
+                                );
+                            }
+                            ui.label(format!(" #{}", self.inner.id));
+                        });
+                    }
+                });
+            });
+        });
+        ui.add_space(constants::PADDING);
+    }
+
+    fn render_compact(self: &Self, ui: &mut egui::Ui, dark_mode: bool) {
+        let header = format!(
+            "{} - {} by {}",
+            self.inner.category, self.inner.language, self.inner.author
+        );
+        let song = format!(
+            "{} - {}{} - {}",
+            self.inner.source_name,
+            self.inner.song_type,
+            match self.inner.song_number {
+                Some(num) => num.to_string(),
+                None => "".to_string(),
+            },
+            self.inner.title
+        );
+        ui.horizontal(|ui| {
+            ui.add_space(constants::PADDING * 3.);
+            ui.vertical(|ui| {
+                ui.add_space(constants::PADDING * 2.);
+                ui.horizontal(|ui| {
+                    ui.style_mut().override_text_style = Some(utils::font::heading3());
+                    ui.colored_label(constants::get_text_color(dark_mode), header);
+                    ui.style_mut().override_text_style = Some(utils::font::body());
+                    ui.label(egui::RichText::new(format!("#{}", self.inner.id)));
+                });
+                ui.horizontal(|ui| {
+                    ui.style_mut().override_text_style = Some(utils::font::heading1());
+                    ui.colored_label(constants::get_text_color(dark_mode), song);
+                    if !self.inner.is_available {
+                        ui.colored_label(constants::get_accent_color(dark_mode), " (unavailable)");
+                    }
+                });
+            });
+        });
+    }
+
+    fn fulfilled_actions(&mut self) -> Vec<action::Action> {
+        let ret = self.actions.clone();
+        self.actions.clear();
+        return ret;
+    }
+
+    fn unique_id(&self) -> u64 {
+        return self.inner.unique_id();
+    }
+}
+
+impl Card<'_, Playlist> for PlaylistCard {
+    const NEED_SEPARATOR: bool = false;
+    const NEED_HEADER_SEPARATOR: bool = false;
+    const BOTTOM_SPACE: Option<f32> = None;
+
+    fn new(inner: Playlist) -> Self {
+        let mut actions = Vec::new();
+        actions.reserve(5);
+        Self { inner, actions }
+    }
+
+    fn render(&mut self, ui: &mut egui::Ui, dark_mode: bool, actions: &Vec<action::Action>) {
+        ui.horizontal(|ui| {
+            ui.add_space(constants::PADDING * 2.);
+            ui.style_mut().override_text_style = Some(utils::font::body());
+            action::render_action_menu(ui, actions, "▶", &mut self.actions);
+            ui.colored_label(
+                constants::get_text_color(dark_mode),
+                format!("{}", self.inner.name),
+            );
+            ui.label(format!(" #{}", self.inner.id));
+        });
+        ui.add_space(constants::PADDING);
+    }
+
+    fn render_compact(self: &Self, ui: &mut egui::Ui, dark_mode: bool) {
+        ui.horizontal(|ui| {
+            ui.colored_label(
+                constants::get_text_color(dark_mode),
+                format!("{}", self.inner.name),
+            );
+            ui.label(format!(" #{}", self.inner.id));
+        });
+    }
+
+    fn fulfilled_actions(&mut self) -> Vec<action::Action> {
+        let ret = self.actions.clone();
+        self.actions.clear();
+        return ret;
+    }
+
+    fn unique_id(&self) -> u64 {
+        return self.inner.unique_id();
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/cards/collection.rs b/src/rust/amadeus-rs/amadeus/src/cards/collection.rs
new file mode 100644
index 0000000000000000000000000000000000000000..228bc9260db86b1b0893c572fa64d7e484ead129
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/cards/collection.rs
@@ -0,0 +1,131 @@
+use crate::{action, cards::card::*, constants, utils};
+use amadeus_macro::either;
+use lkt_lib::types::{Kara, LektorType};
+use std::{borrow::Borrow, cmp::min, marker::PhantomData};
+
+pub type KaraCardCollection<'a> = CardCollection<'a, KaraCard, Kara>;
+
+/// Struct to hold a collection of cards
+pub struct CardCollection<'a, CardType, LktType>
+where
+    CardType: Card<'a, LktType>,
+    LktType: LektorType<'a>,
+{
+    name: String,
+    content: Vec<CardType>,
+    actions: Vec<action::Action>,
+    max_content: Option<usize>,
+    phantom: PhantomData<&'a LktType>,
+}
+
+impl<'a, CardType: Card<'a, LktType>, LktType: LektorType<'a>> ToString
+    for CardCollection<'a, CardType, LktType>
+{
+    fn to_string(&self) -> String {
+        format!(
+            "Collection {}: {}",
+            self.name,
+            self.content
+                .iter()
+                .map(|card| card.to_string())
+                .fold(String::new(), |a, b| a + ", " + b.borrow())
+        )
+    }
+}
+
+impl<'a, CardType: Card<'a, LktType>, LktType: LektorType<'a>>
+    CardCollection<'a, CardType, LktType>
+{
+    pub fn new(name: String) -> Self {
+        Self {
+            name,
+            max_content: None,
+            content: vec![],
+            actions: vec![],
+            phantom: PhantomData,
+        }
+    }
+
+    pub fn with_max_content(&mut self, max_content: usize) -> &mut Self {
+        self.max_content = either!(max_content != 0 => Some(max_content); None);
+        return self;
+    }
+
+    pub fn add_card(&mut self, card: CardType) -> &mut Self {
+        self.content.push(card);
+        return self;
+    }
+
+    pub fn add_action(mut self, act: action::Action) -> Self {
+        self.actions.push(act);
+        return self;
+    }
+
+    pub fn empty(self: &Self) -> bool {
+        return self.content.len() == 0;
+    }
+
+    pub fn fulfilled_actions(&mut self) -> Vec<(u64, action::Action)> {
+        let mut ret = Vec::new();
+        for card in &mut self.content {
+            let actions = card.fulfilled_actions();
+            let actions_count = actions.len();
+            if actions_count != 0 {
+                let id = card.unique_id();
+                ret.reserve(ret.len() + actions_count);
+                for action in actions {
+                    ret.push((id, action));
+                }
+            }
+        }
+        return ret;
+    }
+
+    pub fn render(self: &mut Self, ui: &mut egui::Ui, dark_mode: bool) {
+        ui.vertical(|ui| {
+            ui.add_space(constants::PADDING);
+            ui.horizontal(|ui| {
+                ui.style_mut().override_text_style = Some(utils::font::heading1());
+                ui.colored_label(
+                    constants::get_text_color(dark_mode),
+                    format!("{}", self.name),
+                );
+                ui.style_mut().override_text_style = Some(utils::font::small_body());
+                ui.label(format!(" ({} elements)", self.content.len()));
+            });
+            if CardType::NEED_HEADER_SEPARATOR {
+                ui.add_space(constants::PADDING);
+                ui.add(egui::Separator::default());
+            }
+            if self.empty() {
+                ui.vertical_centered_justified(|ui| {
+                    ui.add_space(constants::PADDING * 2.);
+                    ui.add(egui::Spinner::new());
+                    ui.allocate_space(ui.available_size())
+                });
+            } else {
+                egui::ScrollArea::vertical()
+                    .hscroll(false)
+                    .always_show_scroll(false)
+                    .max_width(f32::INFINITY)
+                    .show(ui, |ui| {
+                        ui.horizontal(|ui| ui.add_space(ui.available_width()));
+                        let upper_bound = match self.max_content {
+                            None => self.content.len(),
+                            Some(x) => min(self.content.len(), x),
+                        };
+                        for card in &mut self.content[0..upper_bound] {
+                            card.render(ui, dark_mode, &self.actions);
+                            if CardType::NEED_SEPARATOR {
+                                ui.add(egui::Separator::default());
+                            }
+                        }
+                        if let Some(space) = CardType::BOTTOM_SPACE {
+                            ui.add_space(space);
+                        }
+                    });
+                ui.allocate_space(ui.available_size());
+            }
+        });
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/cards/mod.rs b/src/rust/amadeus-rs/amadeus/src/cards/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..46282020633d111cf7b9702b37782456608d4027
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/cards/mod.rs
@@ -0,0 +1,9 @@
+mod card;
+mod collection;
+
+pub use card::Card;
+pub use card::KaraCard;
+pub use card::PlaylistCard;
+
+pub use collection::CardCollection;
+pub use collection::KaraCardCollection;
diff --git a/src/rust/amadeus-rs/amadeus/src/constants.rs b/src/rust/amadeus-rs/amadeus/src/constants.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9580093768b1372303b43f9dcb5f0332b2099899
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/constants.rs
@@ -0,0 +1,41 @@
+use amadeus_macro::either;
+use eframe::egui::Color32;
+
+pub const PADDING: f32 = 6.0;
+pub const TOP_PANEL_PADDING: f32 = 0.5;
+pub const BOTTOM_PANEL_MAX_SIZE: f32 = 90.;
+const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
+const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
+const CYAN: Color32 = Color32::from_rgb(0, 255, 255);
+const RED: Color32 = Color32::from_rgb(255, 0, 0);
+
+pub fn get_fill_color(dark_mode: bool) -> Color32 {
+    let base_color = get_accent_color(dark_mode);
+    if dark_mode {
+        return base_color;
+    } else {
+        let factor = 0.69 as f32;
+        return Color32::from_rgb(
+            (base_color.r() as f32 * factor) as u8,
+            (base_color.g() as f32 * factor) as u8,
+            (base_color.b() as f32 * factor) as u8,
+        );
+    }
+}
+
+pub fn get_text_color(dark_mode: bool) -> Color32 {
+    return either!(dark_mode => WHITE; BLACK);
+}
+
+pub fn get_accent_color(dark_mode: bool) -> Color32 {
+    return either!(dark_mode => get_darker_color(CYAN); RED);
+}
+
+fn get_darker_color(color: Color32) -> Color32 {
+    let factor = 0.69 as f32;
+    return Color32::from_rgb(
+        (color.r() as f32 * factor) as u8,
+        (color.g() as f32 * factor) as u8,
+        (color.b() as f32 * factor) as u8,
+    );
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/logger.rs b/src/rust/amadeus-rs/amadeus/src/logger.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e5bb72b86c8bc6da134f91ae6bbc926d41d6312f
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/logger.rs
@@ -0,0 +1,58 @@
+use lazy_static::lazy_static;
+use log::{Level, Metadata, Record, SetLoggerError};
+use std::sync::{Arc, Mutex};
+
+struct SimpleLogger {
+    level: Arc<Mutex<Level>>,
+}
+
+#[repr(transparent)]
+struct SimpleLoggerRef {
+    pub inner: SimpleLogger,
+}
+
+lazy_static! {
+    static ref LOGGER: SimpleLoggerRef = SimpleLoggerRef {
+        inner: SimpleLogger {
+            level: Arc::new(Mutex::new(Level::Debug)),
+        }
+    };
+}
+
+impl log::Log for SimpleLogger {
+    fn enabled(&self, metadata: &Metadata) -> bool {
+        match self.level.lock() {
+            Ok(level) => metadata.level() <= *level,
+            Err(e) => panic!("Failed to lock the log level mutex: {e}"),
+        }
+    }
+
+    fn log(&self, record: &Record) {
+        if self.enabled(record.metadata()) {
+            eprintln!("{} - {}", record.level(), record.args());
+        }
+    }
+
+    fn flush(&self) {}
+}
+
+pub fn init() -> Result<(), SetLoggerError> {
+    let default_level = match LOGGER.inner.level.lock() {
+        Ok(lvl) => lvl,
+        Err(e) => panic!("Failed to lock the log level mutex: {e}"),
+    };
+
+    log::set_logger(&LOGGER.inner).map(|()| {
+        log::set_max_level(default_level.to_level_filter());
+    })
+}
+
+pub fn set_level(level: Level) {
+    match LOGGER.inner.level.lock() {
+        Ok(mut inner_level) => {
+            *inner_level = level;
+            log::set_max_level(level.to_level_filter());
+        }
+        Err(e) => panic!("Failed to lock the log level mutex: {e}"),
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/main.rs b/src/rust/amadeus-rs/amadeus/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..23ee924e005b8b70d3b83ba65d0d127348b0af38
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/main.rs
@@ -0,0 +1,30 @@
+mod action;
+mod amadeus;
+mod cards;
+mod constants;
+mod logger;
+mod playlists;
+mod utils;
+mod widgets;
+
+use eframe::egui::Vec2;
+
+fn main() {
+    if let Err(e) = logger::init() {
+        panic!("Failed to install logger: {e}")
+    }
+    logger::set_level(log::Level::Debug);
+
+    eframe::run_native(
+        amadeus::Amadeus::create(),
+        eframe::NativeOptions {
+            maximized: false,
+            decorated: true,
+            drag_and_drop_support: true,
+            resizable: true,
+            initial_window_size: Some(Vec2::new(800., 600.)),
+            icon_data: Some(utils::get_icon_data()),
+            ..Default::default()
+        },
+    );
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/playlists.rs b/src/rust/amadeus-rs/amadeus/src/playlists.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2fdf70f2541a90b74ae706a0e77b498c7d2263e0
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/playlists.rs
@@ -0,0 +1,225 @@
+use lkt_lib::types::{Kara, LektorType, Playlist};
+use std::collections::{HashMap, HashSet};
+
+use crate::{action, constants, utils, widgets};
+
+static DEFAULT_HASHSET_SIZE: usize = 10;
+
+pub struct PlaylistsStore {
+    /// The list of all playlists
+    playlists: HashMap<String, u64>,
+
+    /// The kara that are contained in the playlists. They are indexed by the
+    /// playlist's unique id. The not ordered and unique kara per playlist
+    /// aspects of playlists are enforced by the HashSet.
+    contents: HashMap<u64, HashSet<Kara>>,
+
+    /// Store the playlists to show.
+    playlists_to_show: HashMap<u64, bool>,
+
+    /// The fullfilled actions in this frame.
+    actions: Vec<(u64, action::Action)>,
+
+    /// A temp buffer for handling actions on a specific item, where we know the
+    /// id to put but not the render function.
+    temp_actions: Vec<action::Action>,
+}
+
+impl Default for PlaylistsStore {
+    fn default() -> Self {
+        Self {
+            playlists: HashMap::with_capacity(DEFAULT_HASHSET_SIZE),
+            contents: HashMap::with_capacity(DEFAULT_HASHSET_SIZE),
+            playlists_to_show: HashMap::with_capacity(DEFAULT_HASHSET_SIZE),
+            actions: Vec::new(),
+            temp_actions: Vec::new(),
+        }
+    }
+}
+
+impl PlaylistsStore {
+    pub fn fulfilled_actions(&mut self) -> Vec<(u64, action::Action)> {
+        if self.actions.len() != 0 {
+            let ret = self.actions.clone();
+            self.actions.clear();
+            return ret;
+        } else {
+            return vec![];
+        }
+    }
+
+    /// Render the opened playlists.
+    pub fn render_playlist_contents(&mut self, ctx: &egui::Context, dark_mode: bool) {
+        let to_show = self
+            .playlists_to_show
+            .iter()
+            .filter(|(_, flag)| **flag)
+            .map(|(id, _)| *id)
+            .collect::<Vec<u64>>();
+        for id in to_show {
+            let name = self
+                .playlists
+                .iter()
+                .filter(|(_, plt_id)| **plt_id == id)
+                .map(|(name, _)| name.as_str())
+                .next()
+                .unwrap();
+            let flag = self.playlists_to_show.get_mut(&id).unwrap();
+            let karas = self.contents.get(&id).unwrap();
+            let actions = widgets::WindowBuilder::new(name, dark_mode)
+                .with_resizable(false)
+                .with_default_size(egui::vec2(200., 500.))
+                .with_actions(vec![
+                    action::Action::AddPlaylistToQueue,
+                    action::Action::InsertPlaylistToQueue,
+                ])
+                .with_warning_actions(vec![action::Action::ClearPlaylistContent])
+                .show(flag, ctx, |ui| {
+                    egui::ScrollArea::vertical()
+                        .hscroll(false)
+                        .always_show_scroll(false)
+                        .max_width(f32::INFINITY)
+                        .show(ui, |ui| {
+                            if karas.len() == 0 {
+                                ui.horizontal(|ui| {
+                                    ui.label("Empty playlist");
+                                    ui.add_space(constants::PADDING);
+                                    ui.add(egui::Spinner::new());
+                                });
+                            } else {
+                                for kara in karas {
+                                    ui.horizontal(|ui| {
+                                        ui.style_mut().override_text_style =
+                                            Some(utils::font::body());
+                                        action::render_action_menu(
+                                            ui,
+                                            &vec![
+                                                action::Action::AddKaraToQueue,
+                                                action::Action::InsertKaraInQueue,
+                                                action::Action::AddKaraToPlaylist,
+                                                action::Action::DeleteKaraFromPlaylist(id),
+                                                action::Action::DeleteKaraFromQueue,
+                                            ],
+                                            widgets::get_label_text_for_kara(kara),
+                                            &mut self.temp_actions,
+                                        );
+                                        self.temp_actions
+                                            .iter()
+                                            .map(|act| (id, act.clone()))
+                                            .for_each(|fullfilled| self.actions.push(fullfilled));
+                                        self.temp_actions.clear();
+                                    });
+                                }
+                            }
+                        });
+                });
+            if let Some(actions) = actions {
+                actions
+                    .iter()
+                    .map(|act| (id, act.clone()))
+                    .for_each(|fullfilled| self.actions.push(fullfilled));
+            }
+        }
+    }
+
+    /// Render the list of playlists. Also register some of the user's actions
+    /// like opening a playlist, etc.
+    pub fn render(&mut self, ui: &mut egui::Ui, dark_mode: bool) {
+        ui.vertical(|ui| {
+            ui.add_space(constants::PADDING);
+            ui.horizontal(|ui| {
+                ui.style_mut().override_text_style = Some(utils::font::heading1());
+                ui.colored_label(constants::get_text_color(dark_mode), "🗀 Playlists");
+                ui.style_mut().override_text_style = Some(utils::font::small_body());
+            });
+            ui.style_mut().override_text_style = Some(utils::font::body());
+            if self.playlists.is_empty() {
+                ui.vertical_centered_justified(|ui| {
+                    ui.add(egui::Spinner::new());
+                    ui.allocate_space(ui.available_size())
+                });
+            } else {
+                egui::ScrollArea::vertical()
+                    .hscroll(false)
+                    .always_show_scroll(false)
+                    .max_width(f32::INFINITY)
+                    .show(ui, |ui| {
+                        ui.horizontal(|ui| ui.add_space(ui.available_width()));
+                        for (name, id) in &self.playlists {
+                            let plt_size = self.playlist_size(name).unwrap();
+                            ui.horizontal(|ui| {
+                                ui.add_space(constants::PADDING);
+                                action::render_action_menu(
+                                    ui,
+                                    &vec![
+                                        action::Action::OpenPlaylist,
+                                        action::Action::AddPlaylistToQueue,
+                                        action::Action::InsertPlaylistToQueue,
+                                    ],
+                                    "▶",
+                                    &mut self.temp_actions,
+                                );
+                                self.temp_actions
+                                    .iter()
+                                    .map(|act| (*id, act.clone()))
+                                    .for_each(|fullfilled| self.actions.push(fullfilled));
+                                self.temp_actions.clear();
+                                ui.colored_label(constants::get_text_color(dark_mode), name);
+                                ui.style_mut().override_text_style =
+                                    Some(utils::font::small_body());
+                                ui.label(format!("{plt_size} karas"));
+                            });
+                        }
+                    });
+                ui.allocate_space(ui.available_size());
+            }
+        });
+    }
+
+    fn get_playlist_by_name(&self, name: &str) -> Option<(&str, u64)> {
+        return self
+            .playlists
+            .iter()
+            .filter(|(plt_name, _)| *plt_name == name)
+            .map(|(name, id)| (name.as_str(), id.clone()))
+            .next();
+    }
+
+    pub fn show_playlist(&mut self, id: u64) {
+        if let Some(flag) = self.playlists_to_show.get_mut(&id) {
+            *flag = true;
+        }
+    }
+
+    pub fn clear_playlist(&mut self, id: u64) {
+        if let Some(content) = self.contents.get_mut(&id) {
+            content.clear();
+        }
+    }
+
+    pub fn create(&mut self, plt: Playlist) {
+        self.contents.insert(
+            plt.unique_id(),
+            HashSet::with_capacity(DEFAULT_HASHSET_SIZE),
+        );
+        let id = plt.unique_id();
+        self.playlists.insert(plt.name, id);
+        self.playlists_to_show.insert(id, false);
+    }
+
+    pub fn add(&mut self, name: &str, kara: Kara) {
+        if let Some((_, id)) = self.get_playlist_by_name(name) {
+            if let Some(plt_content) = self.contents.get_mut(&id) {
+                plt_content.insert(kara);
+            } else {
+                panic!("The playlist {name} has no content set, it should have been created when the playlist was created")
+            }
+        }
+    }
+
+    pub fn playlist_size(&self, name: &str) -> Option<usize> {
+        let (_, id) = self.get_playlist_by_name(name)?;
+        let content = self.contents.get(&id)?;
+        return Some(content.len());
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/utils/config.rs b/src/rust/amadeus-rs/amadeus/src/utils/config.rs
new file mode 100644
index 0000000000000000000000000000000000000000..75e8e9e957388242e40406da9e0261430e92a1cb
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/utils/config.rs
@@ -0,0 +1,130 @@
+use crate::widgets;
+
+/// Indicate which view should be displayed in the central panel of Amadeus'
+/// main window.
+#[derive(serde::Serialize, serde::Deserialize, PartialEq)]
+pub enum AmadeusMainView {
+    /// The main panel should show the queue of lektord, i.e. the kara that will
+    /// be played.
+    Queue,
+
+    /// The main panel should show the kara that where played by lektord.
+    Historic,
+
+    /// The main panel should show the search view.
+    SearchResults,
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+pub struct AmadeusConfig {
+    pub dark_mode: bool,
+    pub admin_password: String,
+    pub lektord_hostname: String,
+    pub lektord_port: super::NetworkPort,
+    pub lektord_auto_reconnect: bool,
+    pub main_panel_view: AmadeusMainView,
+    pub side_panel_show: bool,
+    pub ui_max_queue_items: super::IntegerTextBuffer<usize>,
+    pub ui_max_search_items: super::IntegerTextBuffer<usize>,
+    pub ui_max_history_items: super::IntegerTextBuffer<usize>,
+}
+
+impl Default for AmadeusMainView {
+    fn default() -> Self {
+        Self::Queue
+    }
+}
+
+impl Default for AmadeusConfig {
+    fn default() -> Self {
+        Self {
+            dark_mode: true,
+            admin_password: "".to_owned(),
+            lektord_hostname: "localhost".to_owned(),
+            lektord_port: super::NetworkPort::from("6600"),
+            lektord_auto_reconnect: false,
+            main_panel_view: Default::default(),
+            side_panel_show: true,
+            ui_max_queue_items: super::IntegerTextBuffer::from("100"),
+            ui_max_search_items: super::IntegerTextBuffer::from("100"),
+            ui_max_history_items: super::IntegerTextBuffer::from("100"),
+        }
+    }
+}
+
+impl AmadeusConfig {
+    pub fn render_settings_window(
+        &mut self,
+        ctx: &egui::Context,
+        show: &mut bool,
+        changed: &mut bool,
+    ) {
+        widgets::WindowBuilder::new("Settings", self.dark_mode).show(show, ctx, |ui| {
+            ui.separator();
+            widgets::add_heading2_label(ui, "☰  Lektord deamon");
+            widgets::add_labelled_edit_line(
+                ui,
+                "Hostname         ",
+                &mut self.lektord_hostname,
+                changed,
+            );
+            widgets::add_labelled_edit_line(
+                ui,
+                "Port             ",
+                &mut self.lektord_port,
+                changed,
+            );
+            widgets::add_labelled_toggle_switch(
+                ui,
+                self.dark_mode,
+                "Auto-reconnect",
+                &mut self.lektord_auto_reconnect,
+                changed,
+            );
+
+            ui.separator();
+            widgets::add_heading2_label(ui, "☰  Admin user");
+            widgets::add_labelled_password(
+                ui,
+                "Admin pwd        ",
+                &mut self.admin_password,
+                changed,
+            );
+
+            ui.separator();
+            widgets::add_heading2_label(ui, "☰  UI");
+            widgets::add_labelled_toggle_switch(
+                ui,
+                self.dark_mode,
+                "Dark theme",
+                &mut self.dark_mode,
+                changed,
+            );
+            widgets::add_labelled_toggle_switch(
+                ui,
+                self.dark_mode,
+                "Show side panel",
+                &mut self.side_panel_show,
+                changed,
+            );
+            widgets::add_labelled_edit_line(
+                ui,
+                "Max queue size   ",
+                &mut self.ui_max_queue_items,
+                changed,
+            );
+            widgets::add_labelled_edit_line(
+                ui,
+                "Max search size  ",
+                &mut self.ui_max_search_items,
+                changed,
+            );
+            widgets::add_labelled_edit_line(
+                ui,
+                "Max history size ",
+                &mut self.ui_max_history_items,
+                changed,
+            );
+        });
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/utils/deamon.rs b/src/rust/amadeus-rs/amadeus/src/utils/deamon.rs
new file mode 100644
index 0000000000000000000000000000000000000000..33363d7b63d3efb2b552fed178942807365ed4bc
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/utils/deamon.rs
@@ -0,0 +1,203 @@
+#![allow(dead_code)]
+
+use lkt_lib::{query::LektorQuery, response::LektorFormatedResponse};
+use log::error;
+use std::{
+    cell::Cell,
+    io,
+    sync::{
+        atomic::{self, AtomicBool},
+        mpsc::{channel, Receiver, Sender},
+        Arc, Mutex,
+    },
+    thread, time,
+};
+
+pub trait Deamon: Sized {
+    #[must_use]
+    type Channels;
+
+    /// Quit the deamon
+    fn quit(&self);
+
+    /// Spawn a deamon
+    #[must_use]
+    fn spawn(hostname: String, port: i16) -> io::Result<(Self::Channels, Self)>;
+
+    /// Returns true when the thread has terminated.
+    fn should_joined(&self) -> bool;
+
+    /// Join the deamon.
+    fn join(&self);
+}
+
+pub struct CommandDeamon {
+    thread: Arc<Mutex<Cell<Option<thread::JoinHandle<()>>>>>,
+    quit: Arc<AtomicBool>,
+    joined: Arc<AtomicBool>,
+}
+
+pub type StatusDeamonMessageType = (
+    lkt_lib::response::PlaybackStatus,
+    Option<lkt_lib::response::CurrentKara>,
+);
+
+pub struct StatusDeamon {
+    thread: Arc<Mutex<Cell<Option<thread::JoinHandle<()>>>>>,
+    quit: Arc<AtomicBool>,
+    joined: Arc<AtomicBool>,
+}
+
+macro_rules! return_when_flagged {
+    ($arc_atomic: expr, $joined_deamon: expr) => {
+        if $arc_atomic.load(atomic::Ordering::SeqCst) {
+            $joined_deamon.store(true, atomic::Ordering::SeqCst);
+            return;
+        }
+    };
+}
+
+macro_rules! implement_deamon_quit {
+    () => {
+        fn quit(&self) {
+            self.quit.store(true, atomic::Ordering::SeqCst);
+        }
+    };
+}
+
+macro_rules! implement_deamon_joined {
+    () => {
+        fn should_joined(&self) -> bool {
+            return self.joined.load(atomic::Ordering::SeqCst);
+        }
+
+        fn join(&self) {
+            let locked = self.thread.lock();
+            if locked.is_err() {
+                error!("Failed to lock the mutex that has the join handler",)
+            }
+            let locked = locked.unwrap();
+            let thread = locked.replace(None);
+            match thread {
+                Some(thread) => {
+                    let _ = thread.join();
+                }
+                None => error!("Nothing to join!"),
+            }
+        }
+    };
+}
+
+impl Deamon for CommandDeamon {
+    type Channels = (Sender<LektorQuery>, Receiver<LektorFormatedResponse>);
+    implement_deamon_quit!();
+    implement_deamon_joined!();
+
+    fn spawn(hostname: String, port: i16) -> io::Result<(Self::Channels, Self)> {
+        let mut connexion = lkt_lib::connexion::LektorConnexion::new(hostname, port)?;
+
+        let (responses_send, responses_recv) = channel::<LektorFormatedResponse>();
+        let (commands_send, commands_recv) = channel::<LektorQuery>();
+        let quit = Arc::<AtomicBool>::new(AtomicBool::default());
+        let joined = Arc::<AtomicBool>::new(AtomicBool::default());
+        quit.store(false, atomic::Ordering::SeqCst);
+        joined.store(false, atomic::Ordering::SeqCst);
+        let quit_deamon = quit.clone();
+        let joined_deamon = joined.clone();
+
+        let thread = thread::spawn(move || loop {
+            return_when_flagged!(quit_deamon, joined_deamon);
+            match commands_recv.recv() {
+                Ok(command) => {
+                    let maybe_res = connexion.send_query(command);
+                    if maybe_res.is_err() {
+                        error!("Failed to send query to lektor: {}", maybe_res.unwrap_err());
+                        continue;
+                    }
+                    let res = maybe_res.unwrap();
+                    let response = lkt_lib::response::LektorFormatedResponse::from(res);
+                    if let Err(e) = responses_send.send(response) {
+                        error!("Failed to send response to amadeus: {e}")
+                    }
+                }
+                Err(e) => error!("Failed to get command from amadeus: {e}"),
+            };
+        });
+
+        return Ok((
+            (commands_send, responses_recv),
+            Self {
+                thread: Arc::new(Mutex::new(Cell::new(Some(thread)))),
+                quit,
+                joined,
+            },
+        ));
+    }
+}
+
+impl Deamon for StatusDeamon {
+    type Channels = (Receiver<StatusDeamonMessageType>,);
+    implement_deamon_quit!();
+    implement_deamon_joined!();
+
+    fn spawn(hostname: String, port: i16) -> io::Result<(Self::Channels, StatusDeamon)> {
+        let mut connexion = lkt_lib::connexion::LektorConnexion::new(hostname, port)?;
+
+        let (responses_send, responses_recv) = channel();
+        let quit = Arc::<AtomicBool>::new(AtomicBool::default());
+        let joined = Arc::<AtomicBool>::new(AtomicBool::default());
+        quit.store(false, atomic::Ordering::SeqCst);
+        joined.store(false, atomic::Ordering::SeqCst);
+        let quit_deamon = quit.clone();
+        let joined_deamon = joined.clone();
+
+        let thread = thread::spawn(move || loop {
+            return_when_flagged!(quit_deamon, joined_deamon);
+            thread::sleep(time::Duration::from_secs(1));
+            return_when_flagged!(quit_deamon, joined_deamon);
+
+            let status = {
+                let maybe_res = connexion.send_query(lkt_lib::query::LektorQuery::PlaybackStatus);
+                if maybe_res.is_err() {
+                    error!(
+                        "Failed to send the playback status command to lektor: {}",
+                        maybe_res.unwrap_err()
+                    );
+                    continue;
+                }
+                let res = maybe_res.unwrap();
+                let mut response = lkt_lib::response::LektorFormatedResponse::from(res);
+                lkt_lib::response::PlaybackStatus::consume(&mut response)
+            };
+
+            let current = if status.state != lkt_lib::types::LektorState::Stopped {
+                let maybe_res = connexion.send_query(lkt_lib::query::LektorQuery::CurrentKara);
+                if maybe_res.is_err() {
+                    error!(
+                        "Failed to send the current kara command to lektor: {}",
+                        maybe_res.unwrap_err()
+                    );
+                    continue;
+                }
+                let res = maybe_res.unwrap();
+                let mut response = lkt_lib::response::LektorFormatedResponse::from(res);
+                Some(lkt_lib::response::CurrentKara::consume(&mut response))
+            } else {
+                None
+            };
+
+            if let Err(e) = responses_send.send((status, current)) {
+                error!("Failed to send a status response to amadeus: {}", e);
+            }
+        });
+
+        return Ok((
+            (responses_recv,),
+            Self {
+                thread: Arc::new(Mutex::new(Cell::new(Some(thread)))),
+                quit,
+                joined,
+            },
+        ));
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/utils/font.rs b/src/rust/amadeus-rs/amadeus/src/utils/font.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c14df9f72b464fc057a75ac7bcdff7ddbac7ff8f
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/utils/font.rs
@@ -0,0 +1,100 @@
+use std::collections::BTreeMap;
+
+use egui::{
+    FontData, FontDefinitions,
+    FontFamily::{Monospace, Proportional},
+    FontId, TextStyle,
+};
+
+#[inline]
+pub fn heading1() -> TextStyle {
+    TextStyle::Heading
+}
+
+#[inline]
+pub fn heading2() -> TextStyle {
+    TextStyle::Name("Heading2".into())
+}
+
+#[inline]
+pub fn heading3() -> TextStyle {
+    TextStyle::Name("ContextHeading".into())
+}
+
+#[inline]
+pub fn body() -> TextStyle {
+    TextStyle::Body
+}
+
+#[inline]
+pub fn small_body() -> TextStyle {
+    TextStyle::Name("SmallBody".into())
+}
+
+#[inline]
+pub fn monospace() -> TextStyle {
+    TextStyle::Monospace
+}
+
+#[inline]
+pub fn button() -> TextStyle {
+    TextStyle::Button
+}
+
+#[inline]
+pub fn small() -> TextStyle {
+    TextStyle::Small
+}
+
+pub fn get_font_definitions() -> FontDefinitions {
+    static FONT_NAME: &str = "UbuntuMono";
+    static FONT_DATA: &[u8] = include_bytes!("../../../rsc/UbuntuMono-Regular.ttf");
+    static CJK_NAME: &str = "IPAMincho";
+    static CJK_DATA: &[u8] = include_bytes!("../../../rsc/IPAMincho.ttf");
+    let mut fonts = FontDefinitions::default();
+
+    fonts
+        .font_data
+        .insert(FONT_NAME.to_owned(), FontData::from_static(FONT_DATA));
+    fonts
+        .font_data
+        .insert(CJK_NAME.to_owned(), FontData::from_static(CJK_DATA));
+
+    fonts
+        .families
+        .get_mut(&Proportional)
+        .unwrap()
+        .insert(0, FONT_NAME.to_owned());
+    fonts
+        .families
+        .get_mut(&Proportional)
+        .unwrap()
+        .push(CJK_NAME.to_owned());
+
+    fonts
+        .families
+        .get_mut(&Monospace)
+        .unwrap()
+        .insert(0, FONT_NAME.to_owned());
+    fonts
+        .families
+        .get_mut(&Monospace)
+        .unwrap()
+        .push(CJK_NAME.to_owned());
+
+    return fonts;
+}
+
+pub fn get_font_styles() -> BTreeMap<TextStyle, FontId> {
+    let styles = [
+        (heading1(), FontId::new(30.0, Proportional)),
+        (heading2(), FontId::new(25.0, Proportional)),
+        (heading3(), FontId::new(23.0, Proportional)),
+        (body(), FontId::new(18.0, Proportional)),
+        (monospace(), FontId::new(14.0, Proportional)),
+        (small_body(), FontId::new(14.0, Proportional)),
+        (button(), FontId::new(14.0, Proportional)),
+        (small(), FontId::new(10.0, Proportional)),
+    ];
+    return styles.into();
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/utils/int_text_buffer.rs b/src/rust/amadeus-rs/amadeus/src/utils/int_text_buffer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7bcdc8a356d31e5e400339618ab113e4217d9fba
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/utils/int_text_buffer.rs
@@ -0,0 +1,132 @@
+use egui::TextBuffer;
+use serde::{Deserialize, Serialize};
+use std::{fmt, marker::PhantomData, str::FromStr};
+
+#[derive(Serialize, Deserialize)]
+pub struct IntegerTextBuffer<T: private::Unsigned + private::Zero<T>> {
+    buffer: String,
+    phantom: PhantomData<T>,
+}
+
+impl<T: private::Unsigned + private::Zero<T>> IntegerTextBuffer<T> {
+    pub fn as_integer(&self) -> T
+    where
+        <T as FromStr>::Err: std::fmt::Display,
+    {
+        return if self.buffer.len() == 0 {
+            T::ZERO
+        } else {
+            match self.buffer.parse::<T>() {
+                Ok(int) => int,
+                Err(e) => panic!("Failed to parse integer: {}", e),
+            }
+        };
+    }
+
+    fn remove_all_non_digit_chars(&mut self) {
+        self.buffer = self.buffer.chars().filter(|c| c.is_digit(10)).collect();
+    }
+}
+
+impl<T: private::Unsigned + private::Zero<T>> fmt::Display for IntegerTextBuffer<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.buffer)
+    }
+}
+
+impl<T: private::Unsigned + private::Zero<T>> From<&str> for IntegerTextBuffer<T> {
+    fn from(text: &str) -> Self {
+        Self {
+            buffer: text.to_string(),
+            phantom: PhantomData,
+        }
+    }
+}
+
+impl<T: private::Unsigned + private::Zero<T>> AsRef<str> for IntegerTextBuffer<T> {
+    fn as_ref(&self) -> &str {
+        return self.buffer.as_str();
+    }
+}
+
+impl<T: private::Unsigned + private::Zero<T>> TextBuffer for IntegerTextBuffer<T> {
+    fn is_mutable(&self) -> bool {
+        true
+    }
+
+    fn as_str(&self) -> &str {
+        self.as_ref()
+    }
+
+    fn char_range(&self, char_range: std::ops::Range<usize>) -> &str {
+        assert!(char_range.start <= char_range.end);
+        let start_byte = self.byte_index_from_char_index(char_range.start);
+        let end_byte = self.byte_index_from_char_index(char_range.end);
+        &self.as_str()[start_byte..end_byte]
+    }
+
+    fn insert_text(&mut self, text: &str, char_index: usize) -> usize {
+        let ret = self.buffer.insert_text(text, char_index);
+        self.remove_all_non_digit_chars();
+        return ret;
+    }
+
+    fn delete_char_range(&mut self, char_range: std::ops::Range<usize>) {
+        self.buffer.delete_char_range(char_range);
+    }
+
+    fn clear(&mut self) {
+        self.delete_char_range(0..self.as_ref().len());
+    }
+
+    fn replace(&mut self, text: &str) {
+        self.clear();
+        self.insert_text(text, 0);
+    }
+
+    fn take(&mut self) -> String {
+        let s = self.as_ref().to_owned();
+        self.clear();
+        return s;
+    }
+}
+
+mod private {
+    use std::str::FromStr;
+
+    pub trait Unsigned: FromStr {}
+    impl Unsigned for u8 {}
+    impl Unsigned for u16 {}
+    impl Unsigned for u32 {}
+    impl Unsigned for u64 {}
+    impl Unsigned for u128 {}
+    impl Unsigned for usize {}
+
+    pub trait Zero<T: Unsigned> {
+        const ZERO: T;
+    }
+
+    impl Zero<u8> for u8 {
+        const ZERO: u8 = 0u8;
+    }
+
+    impl Zero<u16> for u16 {
+        const ZERO: u16 = 0u16;
+    }
+
+    impl Zero<u32> for u32 {
+        const ZERO: u32 = 0u32;
+    }
+
+    impl Zero<u64> for u64 {
+        const ZERO: u64 = 0u64;
+    }
+
+    impl Zero<u128> for u128 {
+        const ZERO: u128 = 0u128;
+    }
+
+    impl Zero<usize> for usize {
+        const ZERO: usize = 0usize;
+    }
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/utils/mod.rs b/src/rust/amadeus-rs/amadeus/src/utils/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8cb073149ccca5b9d67f44a1154c1ff2292c9cc4
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/utils/mod.rs
@@ -0,0 +1,29 @@
+pub(crate) mod deamon;
+pub(crate) mod font;
+
+mod config;
+pub(crate) use config::AmadeusConfig;
+pub(crate) use config::AmadeusMainView;
+
+mod int_text_buffer;
+pub(crate) use int_text_buffer::IntegerTextBuffer;
+pub(crate) type NetworkPort = IntegerTextBuffer<u16>;
+
+pub(crate) fn get_icon_as_dynamic_image() -> (image::RgbaImage, [u32; 2]) {
+    static LOGO_SIDE: u32 = 48u32;
+    let logo_data = image::load_from_memory(include_bytes!("../../../rsc/AmadeusLogo.jpg"))
+        .expect("Failed to load Amadeus Logo")
+        .thumbnail(LOGO_SIDE, LOGO_SIDE);
+    let size = [logo_data.width(), logo_data.height()];
+    let logo_buffer = logo_data.to_rgba8();
+    return (logo_buffer, size);
+}
+
+pub(crate) fn get_icon_data() -> epi::IconData {
+    let (logo_buffer, [size_x, size_y]) = get_icon_as_dynamic_image();
+    return epi::IconData {
+        rgba: logo_buffer.to_vec(),
+        height: size_y,
+        width: size_x,
+    };
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/widgets/about_window.rs b/src/rust/amadeus-rs/amadeus/src/widgets/about_window.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1a8d18a22776c57e06905f67caf75238fe7bfbc1
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/widgets/about_window.rs
@@ -0,0 +1,92 @@
+use crate::{constants, widgets};
+
+static THIRD_PARTIE_FONTS: [(&str, &str, Option<&str>); 2] = [
+    (
+        "UbuntuMono",
+        "https://design.ubuntu.com/font/",
+        Some("Dalton Maag (under the Ubuntu font licence)"),
+    ),
+    (
+        "IPAMincho",
+        "http://ossipedia.ipa.go.jp/ipafont/",
+        Some("Information-technology Promotion Agency, Japan (under IPA licence)"),
+    ),
+];
+
+static THIRD_PARTIE_LIBS: [(&str, &str, Option<&str>); 6] = [
+    (
+        "serde",
+        "https://github.com/serde-rs/serde",
+        Some("(under MIT or APACHE-2.0)"),
+    ),
+    (
+        "serde_json",
+        "https://github.com/serde-rs/json",
+        Some("(under MIT or APACHE-2.0)"),
+    ),
+    (
+        "egui",
+        "https://github.com/emilk/egui",
+        Some("(under MIT or APACHE-2.0)"),
+    ),
+    (
+        "lazy_static",
+        "https://github.com/rust-lang-nursery/lazy-static.rs",
+        Some("(under MIT or APACHE-2.0)"),
+    ),
+    (
+        "regex",
+        "https://github.com/rust-lang/regex",
+        Some("(under MIT or APACHE-2.0)"),
+    ),
+    (
+        "image",
+        "https://github.com/image-rs/image",
+        Some("(under MIT)"),
+    ),
+];
+
+pub fn render_about_window(
+    show: &mut bool,
+    dark_mode: bool,
+    ctx: &egui::Context,
+    logo: &egui::TextureHandle,
+) {
+    widgets::WindowBuilder::new("About", dark_mode)
+        .with_resizable(true)
+        .show(show, ctx, |ui| {
+            {
+                ui.separator();
+                widgets::add_heading2_label(ui, "☰  Amadeus RS");
+                ui.style_mut().override_text_style = Some(crate::utils::font::body());
+                ui.horizontal_top(|ui| {
+                    ui.image(logo, logo.size_vec2());
+                    ui.add_space(constants::PADDING);
+                    ui.add(
+                        egui::Label::new(concat!(
+                            "Amadeus RS is a rewrite of the Amadeus front end to lektord.\n",
+                            "Amadeus RS is under the MIT licence."
+                        ))
+                        .wrap(true),
+                    );
+                });
+                ui.add_space(constants::PADDING);
+            }
+
+            {
+                ui.separator();
+                widgets::add_heading2_label(ui, "☰  Third party libraries and fonts");
+                ui.style_mut().override_text_style = Some(crate::utils::font::body());
+                ui.label("The followinf thrid party libraries where used:");
+                for (label, url, after) in THIRD_PARTIE_LIBS {
+                    widgets::add_bullet_hyperlink(ui, label, url, after);
+                }
+                ui.add_space(constants::PADDING);
+                ui.label("The following third party fonts where used:");
+                for (label, url, after) in THIRD_PARTIE_FONTS {
+                    widgets::add_bullet_hyperlink(ui, label, url, after);
+                }
+                ui.add_space(constants::PADDING);
+            }
+        });
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/widgets/mod.rs b/src/rust/amadeus-rs/amadeus/src/widgets/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2bc632de7aaa183d402ab91cb446ed8b1e864f46
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/widgets/mod.rs
@@ -0,0 +1,79 @@
+mod about_window;
+mod progress_bar;
+mod toggle_switch;
+mod window;
+
+pub use about_window::render_about_window;
+pub use progress_bar::progress_bar;
+pub use toggle_switch::toggle_switch;
+pub use window::WindowBuilder;
+
+use crate::{constants, utils};
+
+pub fn add_bullet_hyperlink(ui: &mut egui::Ui, label: &str, url: &str, after: Option<&str>) {
+    ui.horizontal(|ui| {
+        ui.label("    -");
+        ui.hyperlink_to(label, url);
+        if let Some(text) = after {
+            ui.add(egui::Label::new(text).wrap(true));
+        }
+    });
+}
+
+pub fn add_heading2_label(ui: &mut egui::Ui, text: &str) {
+    ui.style_mut().override_text_style = Some(utils::font::heading2());
+    ui.add(egui::Label::new(text).wrap(false));
+    ui.add_space(constants::PADDING);
+}
+
+pub fn add_labelled_edit_line(
+    ui: &mut egui::Ui,
+    before: &str,
+    text: &mut dyn egui::TextBuffer,
+    changed: &mut bool,
+) {
+    ui.horizontal(|ui| {
+        ui.style_mut().override_text_style = Some(utils::font::body());
+        ui.add(egui::Label::new(before));
+        let response = ui.add(egui::TextEdit::singleline(text));
+        *changed |= response.lost_focus();
+    });
+}
+
+pub fn add_labelled_toggle_switch(
+    ui: &mut egui::Ui,
+    dark_mode: bool,
+    before: &str,
+    switch: &mut bool,
+    changed: &mut bool,
+) {
+    ui.horizontal(|ui| {
+        ui.style_mut().override_text_style = Some(utils::font::body());
+        ui.add(egui::Label::new(before));
+        ui.with_layout(egui::Layout::right_to_left(), |ui| {
+            let response = ui.add(toggle_switch(dark_mode, switch));
+            *changed |= response.changed();
+        });
+    });
+}
+
+pub fn add_labelled_password(
+    ui: &mut egui::Ui,
+    before: &str,
+    text: &mut dyn egui::TextBuffer,
+    changed: &mut bool,
+) {
+    ui.horizontal(|ui| {
+        ui.style_mut().override_text_style = Some(utils::font::body());
+        ui.add(egui::Label::new(before));
+        let response = ui.add(egui::TextEdit::singleline(text).password(true));
+        *changed |= response.lost_focus();
+    });
+}
+
+pub fn get_label_text_for_kara(kara: &lkt_lib::types::Kara) -> String {
+    format!(
+        "{} - {} / {} - {} - {} [{}]",
+        kara.category, kara.language, kara.source_name, kara.song_type, kara.title, kara.author
+    )
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/widgets/progress_bar.rs b/src/rust/amadeus-rs/amadeus/src/widgets/progress_bar.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f4df388394148de7f1a4ff878bc3c20b59ebf4ff
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/widgets/progress_bar.rs
@@ -0,0 +1,24 @@
+pub fn progress_bar(dark_mode: bool, progress: f32) -> impl egui::Widget + 'static {
+    return move |ui: &mut egui::Ui| {
+        let progress = progress.clamp(0.0, 1.0);
+        let fill_color = crate::constants::get_fill_color(dark_mode);
+        let desired_width = egui::NumExt::at_least(ui.available_size_before_wrap().x, 96.0);
+        let height = ui.spacing().interact_size.y;
+        let (outer_rect, response) =
+            ui.allocate_exact_size(egui::vec2(desired_width, height), egui::Sense::hover());
+
+        if ui.is_rect_visible(response.rect) {
+            let rect = egui::Rect::from_min_size(
+                outer_rect.min,
+                egui::vec2(
+                    egui::NumExt::at_least(outer_rect.width() * progress, outer_rect.height()),
+                    outer_rect.height(),
+                ),
+            );
+            ui.painter()
+                .rect(rect, 0.0, fill_color, egui::Stroke::none());
+        }
+
+        return response;
+    };
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/widgets/toggle_switch.rs b/src/rust/amadeus-rs/amadeus/src/widgets/toggle_switch.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7a16d52b77e3136b26a76db0886904a1e7cf197a
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/widgets/toggle_switch.rs
@@ -0,0 +1,44 @@
+use crate::constants;
+use amadeus_macro::either;
+
+pub fn toggle_switch(dark_mode: bool, on: &mut bool) -> impl egui::Widget + '_ {
+    return move |ui: &mut egui::Ui| {
+        let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
+        let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
+        if response.clicked() {
+            *on = !*on;
+            response.mark_changed();
+        }
+        response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
+
+        if ui.is_rect_visible(rect) {
+            let (bg_fill, bg_stroke, fg_stroke, expansion) = {
+                let visuals = ui.style().interact_selectable(&response, *on);
+                (
+                    visuals.bg_fill,
+                    visuals.bg_stroke,
+                    visuals.fg_stroke,
+                    visuals.expansion,
+                )
+            };
+            let fill_fore = constants::get_accent_color(dark_mode);
+            let fill_back = constants::get_fill_color(dark_mode);
+            let rect = rect.expand(expansion);
+            let radius = 0.5 * rect.height();
+
+            ui.painter()
+                .rect(rect, radius, either!(*on => fill_back; bg_fill), bg_stroke);
+
+            let circle_x = egui::lerp(
+                (rect.left() + radius)..=(rect.right() - radius),
+                ui.ctx().animate_bool(response.id, *on),
+            );
+            let center = egui::pos2(circle_x, rect.center().y);
+
+            ui.painter()
+                .circle(center, 0.75 * radius, fill_fore, fg_stroke);
+        }
+
+        return response;
+    };
+}
diff --git a/src/rust/amadeus-rs/amadeus/src/widgets/window.rs b/src/rust/amadeus-rs/amadeus/src/widgets/window.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c11e0b2329deb0d394d3aecf79aabb717eb01305
--- /dev/null
+++ b/src/rust/amadeus-rs/amadeus/src/widgets/window.rs
@@ -0,0 +1,131 @@
+use crate::{
+    action::{self, Action},
+    constants,
+};
+
+/// Structure used to hole informations about the window to create, we use a
+/// builder pattern here.
+pub struct WindowBuilder<'a> {
+    name: &'a str,
+    dark_mode: bool,
+    resizable: Option<bool>,
+    default_size: Option<egui::Vec2>,
+    actions: Option<Vec<Action>>,
+    warning_actions: Option<Vec<Action>>,
+}
+
+impl<'a> WindowBuilder<'a> {
+    pub fn new(name: &'a str, dark_mode: bool) -> Self {
+        Self {
+            name,
+            dark_mode,
+            resizable: None,
+            default_size: None,
+            actions: None,
+            warning_actions: None,
+        }
+    }
+
+    pub fn with_resizable(self, resizable: bool) -> Self {
+        Self {
+            name: self.name,
+            dark_mode: self.dark_mode,
+            resizable: Some(resizable),
+            default_size: self.default_size,
+            actions: self.actions,
+            warning_actions: self.warning_actions,
+        }
+    }
+
+    pub fn with_default_size(self, default_size: egui::Vec2) -> Self {
+        Self {
+            name: self.name,
+            dark_mode: self.dark_mode,
+            resizable: self.resizable,
+            default_size: Some(default_size),
+            actions: self.actions,
+            warning_actions: self.warning_actions,
+        }
+    }
+
+    pub fn with_actions(self, actions: Vec<Action>) -> Self {
+        Self {
+            name: self.name,
+            dark_mode: self.dark_mode,
+            resizable: self.resizable,
+            default_size: self.default_size,
+            actions: Some(actions),
+            warning_actions: self.warning_actions,
+        }
+    }
+
+    pub fn with_warning_actions(self, actions: Vec<Action>) -> Self {
+        Self {
+            name: self.name,
+            dark_mode: self.dark_mode,
+            resizable: self.resizable,
+            default_size: self.default_size,
+            actions: self.actions,
+            warning_actions: Some(actions),
+        }
+    }
+
+    pub fn show<R>(
+        self,
+        flag: &mut bool,
+        ctx: &egui::Context,
+        add_contents: impl FnOnce(&mut egui::Ui) -> R,
+    ) -> Option<Vec<Action>> {
+        let mut flag_copy = *flag;
+        let mut temp_actions = Vec::<Action>::new();
+
+        // Force the mut borrow of the flag to be dropped
+        {
+            let win = egui::Window::new(self.name.clone())
+                .open(flag)
+                .title_bar(false);
+            let (win, resizable) = match self.resizable {
+                Some(resizable) => (win.resizable(resizable), resizable),
+                None => (win, false),
+            };
+            let win = match self.default_size {
+                Some(sizes) => {
+                    if resizable {
+                        win.default_size(sizes).fixed_size(sizes)
+                    } else {
+                        win.default_size(sizes)
+                    }
+                }
+                None => win,
+            };
+            win.show(ctx, |ui| {
+                ui.horizontal(|ui| {
+                    ui.style_mut().override_text_style = Some(super::utils::font::heading3());
+                    ui.add(egui::Button::new("❌"))
+                        .on_hover_text("Close the window")
+                        .clicked()
+                        .then(|| flag_copy = false);
+                    if let Some(actions) = self.actions {
+                        action::render_action_menu(ui, &actions, "▶", &mut temp_actions);
+                    };
+                    if let Some(actions) = self.warning_actions {
+                        action::render_action_menu(ui, &actions, "⚠", &mut temp_actions);
+                    };
+                    ui.colored_label(constants::get_text_color(self.dark_mode), self.name);
+                });
+                ui.add_space(constants::PADDING / 2.);
+                add_contents(ui)
+            });
+        }
+
+        // See if we need to close the window
+        *flag = flag_copy;
+
+        // Return the actions if needed
+        if temp_actions.is_empty() {
+            None
+        } else {
+            Some(temp_actions)
+        }
+    }
+}
diff --git a/src/rust/amadeus-rs/lkt-lib/Cargo.toml b/src/rust/amadeus-rs/lkt-lib/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..d28a58181390b0bbe7207150be48431dfcef0619
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name    = "lkt_lib"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+
+[dependencies]
+amadeus_macro   = { path = "../amadeus-macro" }
+serde           = { version = "1", default-features = false, features = [ "derive", "std" ] }
+serde_json      = { version = "1", default-features = false, features = [ "std" ] }
+log             = { version = "0.4" }
+regex           = "1"
+lazy_static     = "1"
\ No newline at end of file
diff --git a/src/rust/amadeus-rs/lkt-lib/src/connexion.rs b/src/rust/amadeus-rs/lkt-lib/src/connexion.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2b63c4cc77b4dbbf650e8948e2a2b8c9c1ff0fbe
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/src/connexion.rs
@@ -0,0 +1,124 @@
+use log::error;
+
+use crate::{constants, query::LektorQuery, query::LektorQueryLineType};
+use std::{
+    fmt,
+    io::{self, BufRead, BufReader, Write},
+    net::TcpStream,
+};
+
+pub struct LektorConnexion {
+    stream: TcpStream,
+    pub version: String,
+
+    reader: BufReader<TcpStream>,
+}
+
+impl fmt::Debug for LektorConnexion {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("LektorConnexion")
+            .field("stream", &self.stream)
+            .field("version", &self.version)
+            .field("reader", &self.reader)
+            .finish()
+    }
+}
+
+impl LektorConnexion {
+    pub fn new(hostname: String, port: i16) -> io::Result<Self> {
+        let result = TcpStream::connect(format!("{}:{}", hostname, port));
+        match result {
+            Ok(lektord) => {
+                let mut ret: Self = Self {
+                    stream: lektord.try_clone().unwrap(),
+                    version: constants::MPD_VERSION.to_owned(),
+                    reader: BufReader::new(lektord),
+                };
+                let mut daemon_version: String = String::new();
+                ret.reader.read_line(&mut daemon_version).unwrap();
+                assert_eq!(
+                    daemon_version.trim(),
+                    format!("OK MPD {}", ret.version),
+                    "Expected MPD server version {}, but got version {}, abort!",
+                    ret.version,
+                    daemon_version
+                );
+                return Ok(ret);
+            }
+            Err(e) => {
+                error!("Failed to connect to lektor: {e}");
+                return Err(e);
+            }
+        }
+    }
+
+    pub fn send_query(&mut self, query: LektorQuery) -> io::Result<Vec<String>> {
+        let mut res: Vec<String> = Vec::new();
+        self.send_query_inner(query, &mut res)?;
+        return Ok(res);
+    }
+
+    fn send_query_inner(
+        &mut self,
+        query: LektorQuery,
+        previous_ret: &mut Vec<String>,
+    ) -> io::Result<()> {
+        self.write_string(query.clone().to_string())?;
+        loop {
+            match self.read_replies() {
+                Ok((res, None)) => {
+                    previous_ret.extend(res);
+                    return Ok(());
+                }
+                Ok((res, Some(cont))) => {
+                    previous_ret.extend(res);
+                    self.write_string(
+                        LektorQuery::create_continuation(query.clone(), cont).to_string(),
+                    )?;
+                }
+                Err(e) => return Err(e),
+            }
+        }
+    }
+
+    fn read_replies(&mut self) -> io::Result<(Vec<String>, Option<usize>)> {
+        let error_return_value = io::Result::Err(io::Error::from(io::ErrorKind::Other));
+        let mut ret: Vec<String> = Vec::new();
+        let mut reply_line: String = String::new();
+        loop {
+            reply_line.clear();
+            match self.reader.read_line(&mut reply_line) {
+                Err(e) => return io::Result::Err(e),
+                Ok(size) => {
+                    if size <= 0 {
+                        panic!("Got nothing in the line... consider this to be an error")
+                    }
+                    let msg = reply_line.trim();
+                    match LektorQueryLineType::from_str(&msg) {
+                        LektorQueryLineType::Ok => return Ok((ret, None)),
+                        LektorQueryLineType::Ack => return error_return_value,
+                        LektorQueryLineType::Data => ret.push(msg.to_string()),
+                        LektorQueryLineType::Continuation(cont) => return Ok((ret, Some(cont))),
+                    }
+                }
+            }
+        }
+    }
+
+    fn write_string(self: &mut Self, buffer: String) -> io::Result<()> {
+        if buffer.len() >= constants::LKT_MESSAGE_MAX {
+            panic!(
+                "Try to write a string that is too long for MPD! String length is {}, max is {}",
+                buffer.len(),
+                constants::LKT_MESSAGE_MAX
+            );
+        }
+        if buffer.chars().last() != Some('\n') {
+            panic!(
+                "A line to be send must end with a newline (\\n), it was not the case for: {}",
+                buffer
+            );
+        }
+        self.stream.write_all(buffer.as_bytes())
+    }
+}
diff --git a/src/rust/amadeus-rs/lkt-lib/src/constants.rs b/src/rust/amadeus-rs/lkt-lib/src/constants.rs
new file mode 100644
index 0000000000000000000000000000000000000000..960c2792931d7c33d317828f06ba51caf8a19f1e
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/src/constants.rs
@@ -0,0 +1,19 @@
+#![allow(dead_code)]
+
+/// MPD commands are at most 32 character long.
+pub(crate) const LKT_COMMAND_LEN_MAX: usize = 32;
+
+/// At most 32 words in a command are supported.
+pub(crate) const LKT_MESSAGE_ARGS_MAX: usize = 32;
+
+/// A message is at most <defined> chars long
+pub(crate) const LKT_MESSAGE_MAX: usize = 2048;
+
+/// At most 64 commands per client.
+pub(crate) const COMMAND_LIST_MAX: usize = 64;
+
+/// At most 16 messages per client.
+pub(crate) const BUFFER_OUT_MAX: usize = 16;
+
+/// Expected version from the lektord daemin.
+pub(crate) const MPD_VERSION: &str = "0.21.16";
diff --git a/src/rust/amadeus-rs/lkt-lib/src/lib.rs b/src/rust/amadeus-rs/lkt-lib/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3975593c3a2061f61079af11887bd6ec93b3ae97
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/src/lib.rs
@@ -0,0 +1,6 @@
+pub mod connexion;
+mod constants;
+pub mod query;
+pub mod response;
+pub mod types;
+pub mod uri;
diff --git a/src/rust/amadeus-rs/lkt-lib/src/query.rs b/src/rust/amadeus-rs/lkt-lib/src/query.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ecc9278889c086501ab959d22565bad8169e0e0a
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/src/query.rs
@@ -0,0 +1,100 @@
+use crate::uri::LektorUri;
+use amadeus_macro::lkt_command_from_str;
+use std::string::ToString;
+
+pub enum LektorQueryLineType {
+    Ok,
+    Ack,
+    Continuation(usize),
+    Data,
+}
+
+#[derive(Debug, Clone)]
+pub enum LektorQuery {
+    Ping,
+    Close,
+    KillServer,
+    ConnectAsUser(String),
+
+    CurrentKara,
+    PlaybackStatus,
+
+    PlayNext,
+    PlayPrevious,
+    ShuffleQueue,
+
+    ListAllPlaylists,
+    ListPlaylist(String),
+
+    FindAddKara(LektorUri),
+    InsertKara(LektorUri),
+    AddKara(LektorUri),
+
+    Continuation(usize, Box<LektorQuery>),
+}
+
+impl LektorQueryLineType {
+    pub fn from_str(line: &str) -> Self {
+        if Self::is_line_ok(&line) {
+            return Self::Ok;
+        } else if Self::is_line_ack(&line) {
+            return Self::Ack;
+        } else if let Some(cont) = Self::is_line_continuation(&line) {
+            return Self::Continuation(cont);
+        } else {
+            return Self::Data;
+        }
+    }
+
+    fn is_line_continuation(line: &str) -> Option<usize> {
+        if line.starts_with("continue:") {
+            match line.trim_start_matches("continue:").trim().parse::<usize>() {
+                Ok(cont) => Some(cont),
+                Err(_) => None,
+            }
+        } else {
+            None
+        }
+    }
+
+    fn is_line_ok(line: &str) -> bool {
+        return (line == "OK\n") || (line == "OK");
+    }
+
+    fn is_line_ack(line: &str) -> bool {
+        return line.starts_with("ACK: ");
+    }
+}
+
+impl LektorQuery {
+    pub fn create_continuation(query: Self, cont: usize) -> Self {
+        Self::Continuation(cont, Box::new(query))
+    }
+}
+
+impl ToString for LektorQuery {
+    fn to_string(&self) -> String {
+        match self {
+            Self::Ping => lkt_command_from_str!("ping"),
+            Self::Close => lkt_command_from_str!("close"),
+            Self::KillServer => lkt_command_from_str!("kill"),
+            Self::ConnectAsUser(password) => format!("password {}\n", password),
+
+            Self::CurrentKara => lkt_command_from_str!("currentsong"),
+            Self::PlaybackStatus => lkt_command_from_str!("status"),
+
+            Self::PlayNext => lkt_command_from_str!("next"),
+            Self::PlayPrevious => lkt_command_from_str!("previous"),
+            Self::ShuffleQueue => lkt_command_from_str!("shuffle"),
+
+            Self::ListAllPlaylists => lkt_command_from_str!("listplaylists"),
+            Self::ListPlaylist(plt_name) => format!("listplaylist {}\n", plt_name),
+
+            Self::FindAddKara(uri) => format!("findadd {}\n", uri.to_string()),
+            Self::InsertKara(uri) => format!("__insert {}\n", uri.to_string()),
+            Self::AddKara(uri) => format!("add {}\n", uri.to_string()),
+
+            Self::Continuation(cont, query) => format!("{} {}", cont, query.to_owned().to_string()),
+        }
+    }
+}
diff --git a/src/rust/amadeus-rs/lkt-lib/src/response.rs b/src/rust/amadeus-rs/lkt-lib/src/response.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fd4c97e3bafa46ab5a55dec0fdc6c7e5b43e8823
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/src/response.rs
@@ -0,0 +1,135 @@
+use crate::types::LektorState;
+use std::collections::HashMap;
+
+#[derive(Debug)]
+pub struct LektorFormatedResponse {
+    content: HashMap<String, String>,
+}
+
+impl LektorFormatedResponse {
+    pub fn pop(&mut self, key: &str) -> String {
+        match self.content.remove(key) {
+            Some(x) => x,
+            None => panic!("Failed to find a value with the key {key} in the formated response"),
+        }
+    }
+}
+
+impl From<Vec<String>> for LektorFormatedResponse {
+    fn from(vec: Vec<String>) -> Self {
+        Self {
+            content: vec
+                .into_iter()
+                .map(|line| {
+                    let key_pair: Vec<&str> = line.splitn(2, ':').collect();
+                    if let [key, value] = key_pair[..] {
+                        (
+                            key.trim().to_lowercase().to_owned(),
+                            value.trim().to_owned(),
+                        )
+                    } else {
+                        panic!("Invalid line for formated response: {}", line);
+                    }
+                })
+                .collect(),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum LektordResponse {
+    PlaybackStatus(PlaybackStatus),
+    CurrentKara(CurrentKara),
+}
+
+#[derive(Debug)]
+pub struct PlaybackStatus {
+    pub elapsed: usize,
+    pub songid: Option<usize>,
+    pub song: Option<usize>,
+    pub volume: usize,
+    pub state: LektorState,
+    pub duration: usize,
+    pub updating_db: usize,
+    pub playlistlength: usize,
+    pub random: bool,
+    pub consume: bool,
+    pub single: bool,
+    pub repeat: bool,
+}
+
+impl PlaybackStatus {
+    pub fn consume(response: &mut LektorFormatedResponse) -> Self {
+        let mut ret = Self {
+            elapsed: match response.pop("elapsed").parse::<usize>() {
+                Ok(x) => x,
+                Err(_) => 0,
+            },
+            songid: match response.pop("songid").parse::<isize>() {
+                Ok(x) if x <= 0 => None,
+                Ok(x) => Some(x as usize),
+                Err(_) => None,
+            },
+            song: match response.pop("song").parse::<usize>() {
+                Ok(x) => Some(x),
+                Err(_) => None,
+            },
+            volume: response
+                .pop("volume")
+                .parse::<usize>()
+                .unwrap()
+                .clamp(0, 100),
+            duration: response.pop("duration").parse::<usize>().unwrap(),
+            updating_db: response.pop("updating_db").parse::<usize>().unwrap(),
+            playlistlength: response.pop("playlistlength").parse::<usize>().unwrap(),
+            random: response.pop("random").parse::<usize>().unwrap() != 0,
+            consume: response.pop("consume").parse::<usize>().unwrap() != 0,
+            single: response.pop("single").parse::<usize>().unwrap() != 0,
+            repeat: response.pop("repeat").parse::<usize>().unwrap() != 0,
+            state: LektorState::Stopped,
+        };
+        ret.state = match &response.pop("state")[..] {
+            "play" => LektorState::Play(ret.songid.unwrap()),
+            "pause" => LektorState::Pause(ret.songid.unwrap()),
+            _ => LektorState::Stopped,
+        };
+        return ret;
+    }
+}
+
+#[derive(Debug)]
+pub struct CurrentKara {
+    pub title: String,
+    pub author: String,
+    pub source: String,
+    pub song_type: String,
+    pub song_number: Option<usize>,
+    pub category: String,
+    pub language: String,
+}
+
+impl CurrentKara {
+    pub fn consume(response: &mut LektorFormatedResponse) -> Self {
+        let song_type_number = response.pop("type");
+        let (song_type, song_number) = match song_type_number.find(char::is_numeric) {
+            Some(index) => (
+                (&song_type_number[..index]).to_owned(),
+                match (&song_type_number[index..]).parse::<usize>() {
+                    Ok(x) => Some(x),
+                    Err(_) => None,
+                },
+            ),
+            None => panic!("Oupsy"),
+        };
+
+        Self {
+            title: response.pop("title"),
+            author: response.pop("author"),
+            source: response.pop("source"),
+            category: response.pop("category"),
+            language: response.pop("language"),
+            song_type,
+            song_number,
+        }
+    }
+}
diff --git a/src/rust/amadeus-rs/lkt-lib/src/types.rs b/src/rust/amadeus-rs/lkt-lib/src/types.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2ac21fcfe4b917a618133b8180cad982292ebd7f
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/src/types.rs
@@ -0,0 +1,91 @@
+use std::hash::{Hash, Hasher};
+
+/// Lektor Types are the Kara and the Playlist types. You should not implement
+/// this type yourself.
+pub trait LektorType<'a>:
+    Clone + serde::Serialize + serde::Deserialize<'a> + Hash + private::Sealed
+{
+    fn unique_id(&self) -> u64;
+}
+
+#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct Kara {
+    pub id: u32,
+    pub source_name: String,
+
+    #[serde(rename = "type")]
+    pub song_type: String,
+
+    pub language: String,
+    pub category: String,
+    pub title: String,
+    pub song_number: Option<u32>,
+    pub author: String,
+    pub is_available: bool,
+}
+
+#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct Playlist {
+    pub id: u32,
+    pub name: String,
+}
+
+impl LektorType<'_> for Kara {
+    fn unique_id(&self) -> u64 {
+        return private::unique_id(self);
+    }
+}
+
+impl LektorType<'_> for Playlist {
+    fn unique_id(&self) -> u64 {
+        return private::unique_id(self);
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
+pub enum LektorState {
+    Stopped,
+    Play(usize),
+    Pause(usize),
+}
+
+impl Default for LektorState {
+    fn default() -> Self {
+        Self::Stopped
+    }
+}
+
+/// We seal the implementation only for supported types.
+mod private {
+    use super::{Kara, Playlist};
+    use std::{
+        collections::hash_map::DefaultHasher,
+        hash::{Hash, Hasher},
+    };
+
+    pub trait Sealed {}
+
+    impl Sealed for Kara {}
+    impl Sealed for Playlist {}
+
+    pub(crate) fn unique_id<T: Hash + Sealed>(object: &T) -> u64 {
+        let mut s = DefaultHasher::new();
+        object.hash(&mut s);
+        return s.finish();
+    }
+}
+
+/// Sealed implementation of Hash for Kara.
+impl Hash for Kara {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        ((self.id as usize) * 2).hash(state);
+    }
+}
+
+/// Sealed implementation of Hash for Playlist.
+impl Hash for Playlist {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.id.hash(state);
+        self.name.hash(state);
+    }
+}
diff --git a/src/rust/amadeus-rs/lkt-lib/src/uri.rs b/src/rust/amadeus-rs/lkt-lib/src/uri.rs
new file mode 100644
index 0000000000000000000000000000000000000000..95892a1c798782fc79ac630f072a01b70c279a39
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-lib/src/uri.rs
@@ -0,0 +1,18 @@
+#[derive(Debug, Clone)]
+pub enum LektorUri {
+    Id(i32),
+    Author(String),
+    Playlist(String),
+    Query(String),
+}
+
+impl ToString for LektorUri {
+    fn to_string(&self) -> String {
+        match self {
+            Self::Id(id) => format!("id://{}", id),
+            Self::Author(author) => format!("author://{}", author),
+            Self::Playlist(plt_name) => format!("playlist://{}", plt_name),
+            Self::Query(sql_query) => format!("query://%{}%", sql_query),
+        }
+    }
+}
diff --git a/src/rust/amadeus-rs/lkt-rs/Cargo.toml b/src/rust/amadeus-rs/lkt-rs/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..89387867e53c6b24a6e3b80a1075e8c16a5658a1
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-rs/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name    = "lkt-rs"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+
+[dependencies]
+lkt_lib       = { path = "../lkt-lib" }
+amadeus_macro = { path = "../amadeus-macro" }
+serde         = { version = "1",                           features = [ "derive" ] }
+serde_json    = { version = "1", default-features = false, features = [ "std" ] }
+log           = { version = "0.4" }
+lazy_static   = "1"
\ No newline at end of file
diff --git a/src/rust/amadeus-rs/lkt-rs/src/main.rs b/src/rust/amadeus-rs/lkt-rs/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9864808a0973d0e0def07693be61369ad86819e7
--- /dev/null
+++ b/src/rust/amadeus-rs/lkt-rs/src/main.rs
@@ -0,0 +1,24 @@
+use lkt_lib::{connexion::LektorConnexion, query::LektorQuery, response};
+
+fn main() {
+    let mut lektor = LektorConnexion::new("localhost".to_owned(), 6600).unwrap();
+    if let Ok(_) = lektor.send_query(LektorQuery::Ping) {}
+
+    if let Ok(res) = lektor.send_query(LektorQuery::CurrentKara) {
+        let mut response = response::LektorFormatedResponse::from(res);
+        let current_kara = response::CurrentKara::consume(&mut response);
+        println!("CURRENT {:?}", current_kara);
+    }
+
+    if let Ok(res) = lektor.send_query(LektorQuery::PlaybackStatus) {
+        let mut response = response::LektorFormatedResponse::from(res);
+        let playback_status = response::PlaybackStatus::consume(&mut response);
+        println!("PLAYBACK-STATUS {:?}", playback_status);
+    }
+
+    if let Ok(res) = lektor.send_query(LektorQuery::ListAllPlaylists) {
+        for item in res {
+            println!("ALL PLAYLISTS {}", item);
+        }
+    }
+}
diff --git a/src/rust/amadeus-rs/rsc/AmadeusLogo.jpg b/src/rust/amadeus-rs/rsc/AmadeusLogo.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..62b445c1c20d056de0b3badafe7d845733a78694
Binary files /dev/null and b/src/rust/amadeus-rs/rsc/AmadeusLogo.jpg differ
diff --git a/src/rust/amadeus-rs/rsc/IPAMincho.ttf b/src/rust/amadeus-rs/rsc/IPAMincho.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..df7b361fc7e67658e9b86f3085c5273a51338adf
Binary files /dev/null and b/src/rust/amadeus-rs/rsc/IPAMincho.ttf differ
diff --git a/src/rust/amadeus-rs/rsc/UbuntuMono-Regular.ttf b/src/rust/amadeus-rs/rsc/UbuntuMono-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..4977028d132b681c035d427748f74afab9fd70bf
Binary files /dev/null and b/src/rust/amadeus-rs/rsc/UbuntuMono-Regular.ttf differ
diff --git a/src/rust/amadeus-rs/rsc/UbuntuMono-UFL.txt b/src/rust/amadeus-rs/rsc/UbuntuMono-UFL.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6e722c88daa5e66b0b037de6ffa1bda86cc21f6e
--- /dev/null
+++ b/src/rust/amadeus-rs/rsc/UbuntuMono-UFL.txt
@@ -0,0 +1,96 @@
+-------------------------------
+UBUNTU FONT LICENCE Version 1.0
+-------------------------------
+
+PREAMBLE
+This licence allows the licensed fonts to be used, studied, modified and
+redistributed freely. The fonts, including any derivative works, can be
+bundled, embedded, and redistributed provided the terms of this licence
+are met. The fonts and derivatives, however, cannot be released under
+any other licence. The requirement for fonts to remain under this
+licence does not require any document created using the fonts or their
+derivatives to be published under this licence, as long as the primary
+purpose of the document is not to be a vehicle for the distribution of
+the fonts.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this licence and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Original Version" refers to the collection of Font Software components
+as received under this licence.
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to
+a new environment.
+
+"Copyright Holder(s)" refers to all individuals and companies who have a
+copyright ownership of the Font Software.
+
+"Substantially Changed" refers to Modified Versions which can be easily
+identified as dissimilar to the Font Software by users of the Font
+Software comparing the Original Version with the Modified Version.
+
+To "Propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification and with or without charging
+a redistribution fee), making available to the public, and in some
+countries other activities as well.
+
+PERMISSION & CONDITIONS
+This licence does not grant any rights under trademark law and all such
+rights are reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to propagate the Font Software, subject to
+the below conditions:
+
+1) Each copy of the Font Software must contain the above copyright
+notice and this licence. These can be included either as stand-alone
+text files, human-readable headers or in the appropriate machine-
+readable metadata fields within text or binary files as long as those
+fields can be easily viewed by the user.
+
+2) The font name complies with the following:
+(a) The Original Version must retain its name, unmodified.
+(b) Modified Versions which are Substantially Changed must be renamed to
+avoid use of the name of the Original Version or similar names entirely.
+(c) Modified Versions which are not Substantially Changed must be
+renamed to both (i) retain the name of the Original Version and (ii) add
+additional naming elements to distinguish the Modified Version from the
+Original Version. The name of such Modified Versions must be the name of
+the Original Version, with "derivative X" where X represents the name of
+the new work, appended to that name.
+
+3) The name(s) of the Copyright Holder(s) and any contributor to the
+Font Software shall not be used to promote, endorse or advertise any
+Modified Version, except (i) as required by this licence, (ii) to
+acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
+their explicit written permission.
+
+4) The Font Software, modified or unmodified, in part or in whole, must
+be distributed entirely under this licence, and must not be distributed
+under any other licence. The requirement for fonts to remain under this
+licence does not affect any document created using the Font Software,
+except any version of the Font Software extracted from a document
+created using the Font Software may only be distributed under this
+licence.
+
+TERMINATION
+This licence becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.