URI: 
       Added plugin system. - icy_draw - icy_draw is the successor to mystic draw. fork / mirror
  HTML git clone https://git.drkhsh.at/icy_draw.git
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit f81428a1a205a3267ce9985aae9a0cffae2145cb
   DIR parent eeb2cf7adcce56bb13b7dc641718f8009b7a0393
  HTML Author: Mike Krüger <mkrueger@posteo.de>
       Date:   Sun, 17 Sep 2023 21:04:22 +0200
       
       Added plugin system.
       
       Diffstat:
         M i18n/de/icy_draw.ftl                |       3 +++
         M i18n/en/icy_draw.ftl                |       4 +++-
         M src/main.rs                         |       6 ++++++
         A src/plugins/elite-writing.lua.txt   |      43 ++++++++++++++++++++++++++++++
         A src/plugins/mod.rs                  |     484 +++++++++++++++++++++++++++++++
         M src/ui/commands.rs                  |       5 +++++
         M src/ui/messages.rs                  |      27 ++++++++++++++++++++++++++-
         M src/ui/settings.rs                  |      22 +++++++++++++++++++++-
         M src/ui/top_bar.rs                   |      26 +++++++++++++++++++++++++-
       
       9 files changed, 616 insertions(+), 4 deletions(-)
       ---
   DIR diff --git a/i18n/de/icy_draw.ftl b/i18n/de/icy_draw.ftl
       @@ -104,6 +104,8 @@ menu-discuss=Diskussion
        menu-open_log_file=Logdatei öffnen
        menu-report-bug=Fehler melden
        menu-about=Info…
       +menu-plugins=Erweiterungen
       +menu-open_plugin_directory=Erweiterungsverzeichnis öffnen…
        
        tool-fg=Fg
        tool-bg=Bg
       @@ -273,6 +275,7 @@ undo-bitfont-edit=Editieren
        undo-render_character=Zeichen rendern
        undo-delete_character=Zeichen löschen
        undo-select=Auswahl
       +undo-plugin=Erweiterung { $title }
        
        autosave-dialog-title=Autosave
        autosave-dialog-description=Icy Draw hat eine Autosave Datei gefunden.
   DIR diff --git a/i18n/en/icy_draw.ftl b/i18n/en/icy_draw.ftl
       @@ -102,7 +102,8 @@ menu-discuss=Discuss
        menu-open_log_file=Open log file
        menu-report-bug=Report Bug
        menu-about=About…
       -
       +menu-plugins=Plugins
       +menu-open_plugin_directory=Open Plugin Directory…
        
        tool-fg=Fg
        tool-bg=Bg
       @@ -272,6 +273,7 @@ undo-bitfont-edit=Edit
        undo-render_character=Render character
        undo-delete_character=Delete character
        undo-select=Select
       +undo-plugin=Plugin { $title }
        
        font_selector-builtin_font=BUILTIN
        font_selector-library_font=LIBRARY
   DIR diff --git a/src/main.rs b/src/main.rs
       @@ -6,6 +6,7 @@
        use eframe::egui;
        const VERSION: &str = env!("CARGO_PKG_VERSION");
        mod model;
       +mod plugins;
        mod ui;
        mod util;
        
       @@ -47,12 +48,15 @@ static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
        #[cfg(not(target_arch = "wasm32"))]
        fn main() {
            use std::fs;
       +
       +    use crate::plugins::Plugin;
            let options = eframe::NativeOptions {
                initial_window_size: Some(egui::vec2(1280., 841.)),
                multisampling: 0,
                renderer: eframe::Renderer::Glow,
                ..Default::default()
            };
       +
            if let Ok(log_file) = Settings::get_log_file() {
                // delete log file when it is too big
                if let Ok(data) = fs::metadata(&log_file) {
       @@ -108,6 +112,8 @@ fn main() {
            }
        
            log::info!("Starting iCY DRAW {}", VERSION);
       +    Plugin::read_plugin_directory();
       +
            if let Err(err) = eframe::run_native(
                &DEFAULT_TITLE,
                options,
   DIR diff --git a/src/plugins/elite-writing.lua.txt b/src/plugins/elite-writing.lua.txt
       @@ -0,0 +1,42 @@
       +-- Title: Elite Writing
       +table = {
       +    [101] = 238,
       +    [69] = 228,
       +    [73] = 173,
       +    [114] = 231,
       +    [82] = 158,
       +    [70] = 159,
       +    [102]= 159,
       +    [97] = 224,
       +    [65] = 92,
       +    [62] = 225,
       +    [66] = 225,
       +    [110]= 227,
       +    [78] = 227,
       +    [117] = 150,
       +    [85] = 239,
       +    [89] = 157,
       +    [111] = 237,
       +    [79] = 229,
       +    [76] = 156,
       +    [108] = 156,
       +    [88] = 145,
       +    [120] = 145,
       +    [83] = 36,
       +    [115] = 36,
       +    [67] = 155,
       +    [99] = 155,
       +    [68] = 235,
       +    [100] = 235,
       +    [121] = 230,
       +    [116] = 226
       +}
       +
       +for y = start_y, end_y, 1 do
       +    for x = start_x, end_x, 1 do
       +        local ch = buf:get_char(x, y)
       +        if table[ch] then
       +            buf:set_char(x, y, table[ch])
       +        end
       +    end
       +end
       +\ No newline at end of file
   DIR diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs
       @@ -0,0 +1,484 @@
       +use std::{fs, path::Path, sync::Arc};
       +
       +use i18n_embed_fl::fl;
       +use icy_engine::{AttributedChar, Position, TextPane};
       +use mlua::{Lua, UserData};
       +use regex::Regex;
       +use walkdir::WalkDir;
       +
       +use crate::{model::font_imp::FontTool, Settings, PLUGINS};
       +
       +pub struct Plugin {
       +    pub title: String,
       +    pub text: String,
       +}
       +
       +impl Plugin {
       +    pub fn load(path: &Path) -> anyhow::Result<Self> {
       +        let text = fs::read_to_string(path).unwrap();
       +
       +        let re = Regex::new(r"--\s*Title:\s*(.*)")?;
       +
       +        if let Some(cap) = re.captures(&text) {
       +            let title = cap.get(1).unwrap().as_str().to_string();
       +            return Ok(Self { title, text });
       +        }
       +        Err(anyhow::anyhow!("No plugin file"))
       +    }
       +
       +    pub(crate) fn run_plugin(
       +        &self,
       +        _window: &mut crate::MainWindow,
       +        editor: &crate::AnsiEditor,
       +    ) -> anyhow::Result<()> {
       +        let lua = Lua::new();
       +        let globals = lua.globals();
       +
       +        globals
       +            .set(
       +                "log",
       +                lua.create_function(move |_lua, txt: String| {
       +                    log::info!("{txt}");
       +                    Ok(())
       +                })?,
       +            )
       +            .unwrap();
       +
       +        globals.set(
       +            "buf",
       +            LuaBufferView {
       +                buffer_view: editor.buffer_view.clone(),
       +            },
       +        )?;
       +
       +        let sel = editor.buffer_view.lock().get_selection();
       +
       +        let rect = if let Some(l) = editor.buffer_view.lock().get_edit_state().get_cur_layer() {
       +            l.get_rectangle()
       +        } else {
       +            return Err(anyhow::anyhow!("No layer selected"));
       +        };
       +
       +        if let Some(sel) = sel {
       +            let mut selected_rect = sel.as_rectangle().intersect(&rect);
       +            selected_rect -= rect.start;
       +
       +            globals.set("start_x", selected_rect.left())?;
       +            globals.set("end_x", selected_rect.right() - 1)?;
       +            globals.set("start_y", selected_rect.top())?;
       +            globals.set("end_y", selected_rect.bottom() - 1)?;
       +        } else {
       +            globals.set("start_x", 0)?;
       +            globals.set("end_x", rect.get_width())?;
       +            globals.set("start_y", 0)?;
       +            globals.set("end_y", rect.get_height())?;
       +        }
       +        let _undo = editor
       +            .buffer_view
       +            .lock()
       +            .get_edit_state_mut()
       +            .begin_atomic_undo(fl!(
       +                crate::LANGUAGE_LOADER,
       +                "undo-plugin",
       +                title = self.title.clone()
       +            ));
       +        lua.load(&self.text).exec()?;
       +        Ok(())
       +    }
       +
       +    pub fn read_plugin_directory() {
       +        let walker = WalkDir::new(Settings::get_plugin_directory().unwrap()).into_iter();
       +        for entry in walker.filter_entry(|e| !FontTool::is_hidden(e)) {
       +            match entry {
       +                Ok(entry) => {
       +                    if entry.file_type().is_dir() {
       +                        continue;
       +                    }
       +                    unsafe {
       +                        match Plugin::load(entry.path()) {
       +                            Ok(plugin) => {
       +                                PLUGINS.push(plugin);
       +                            }
       +                            Err(err) => log::error!("Error loading plugin: {err}"),
       +                        }
       +                    }
       +                }
       +                Err(err) => log::error!("Error loading plugin: {err}"),
       +            }
       +        }
       +    }
       +}
       +
       +struct LuaBufferView {
       +    buffer_view: Arc<eframe::epaint::mutex::Mutex<icy_engine_egui::BufferView>>,
       +}
       +
       +impl UserData for LuaBufferView {
       +    fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
       +        fields.add_field_method_get("height", |_, this| {
       +            Ok(this.buffer_view.lock().get_buffer_mut().get_height())
       +        });
       +        fields.add_field_method_set("height", |_, this, val| {
       +            this.buffer_view.lock().get_buffer_mut().set_height(val);
       +            Ok(())
       +        });
       +        fields.add_field_method_get("width", |_, this| {
       +            Ok(this.buffer_view.lock().get_buffer_mut().get_width())
       +        });
       +        fields.add_field_method_set("width", |_, this, val| {
       +            this.buffer_view.lock().get_buffer_mut().set_width(val);
       +            Ok(())
       +        });
       +
       +        fields.add_field_method_get("font_page", |_, this| {
       +            Ok(this.buffer_view.lock().get_caret_mut().get_font_page())
       +        });
       +        fields.add_field_method_set("font_page", |_, this, val| {
       +            this.buffer_view.lock().get_caret_mut().set_font_page(val);
       +            Ok(())
       +        });
       +
       +        fields.add_field_method_get("layer", |_, this| {
       +            Ok(this
       +                .buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .get_current_layer())
       +        });
       +        fields.add_field_method_set("layer", |_, this, val| {
       +            if val < this.buffer_view.lock().get_buffer_mut().layers.len() {
       +                this.buffer_view
       +                    .lock()
       +                    .get_edit_state_mut()
       +                    .set_current_layer(val);
       +                Ok(())
       +            } else {
       +                Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Layer {} out of range (0..<{})",
       +                        val,
       +                        this.buffer_view.lock().get_buffer_mut().layers.len()
       +                    ),
       +                    incomplete_input: false,
       +                })
       +            }
       +        });
       +
       +        fields.add_field_method_get("fg", |_, this| {
       +            Ok(this
       +                .buffer_view
       +                .lock()
       +                .get_caret_mut()
       +                .get_attribute()
       +                .get_foreground())
       +        });
       +        fields.add_field_method_set("fg", |_, this, val| {
       +            let mut attr = this.buffer_view.lock().get_caret_mut().get_attribute();
       +            attr.set_foreground(val);
       +            this.buffer_view.lock().get_caret_mut().set_attr(attr);
       +            Ok(())
       +        });
       +
       +        fields.add_field_method_get("bg", |_, this| {
       +            Ok(this
       +                .buffer_view
       +                .lock()
       +                .get_caret_mut()
       +                .get_attribute()
       +                .get_background())
       +        });
       +        fields.add_field_method_set("bg", |_, this, val| {
       +            let mut attr = this.buffer_view.lock().get_caret_mut().get_attribute();
       +            attr.set_background(val);
       +            this.buffer_view.lock().get_caret_mut().set_attr(attr);
       +            Ok(())
       +        });
       +
       +        fields.add_field_method_get("x", |_, this| {
       +            Ok(this.buffer_view.lock().get_caret_mut().get_position().x)
       +        });
       +        fields.add_field_method_set("x", |_, this, val| {
       +            this.buffer_view.lock().get_caret_mut().set_x_position(val);
       +            Ok(())
       +        });
       +
       +        fields.add_field_method_get("y", |_, this| {
       +            Ok(this.buffer_view.lock().get_caret_mut().get_position().y)
       +        });
       +        fields.add_field_method_set("y", |_, this, val| {
       +            this.buffer_view.lock().get_caret_mut().set_y_position(val);
       +            Ok(())
       +        });
       +
       +        fields.add_field_method_get("layer_count", |_, this| {
       +            Ok(this.buffer_view.lock().get_buffer_mut().layers.len())
       +        });
       +    }
       +
       +    fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
       +        methods.add_method_mut("fg_rgb", |_, this, (r, g, b): (u8, u8, u8)| {
       +            let color = this
       +                .buffer_view
       +                .lock()
       +                .get_buffer_mut()
       +                .palette
       +                .insert_color_rgb(r, g, b);
       +            this.buffer_view
       +                .lock()
       +                .get_caret_mut()
       +                .set_foreground(color);
       +            Ok(color)
       +        });
       +
       +        methods.add_method_mut("bg_rgb", |_, this, (r, g, b): (u8, u8, u8)| {
       +            let color = this
       +                .buffer_view
       +                .lock()
       +                .get_buffer_mut()
       +                .palette
       +                .insert_color_rgb(r, g, b);
       +            this.buffer_view
       +                .lock()
       +                .get_caret_mut()
       +                .set_background(color);
       +            Ok(color)
       +        });
       +
       +        methods.add_method_mut("set_char", |_, this, (x, y, ch): (i32, i32, u32)| {
       +            let cur_layer = this
       +                .buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .get_current_layer();
       +            let layer_len = this.buffer_view.lock().get_buffer_mut().layers.len();
       +            if cur_layer >= layer_len {
       +                return Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Current layer {} out of range (0..<{})",
       +                        cur_layer, layer_len
       +                    ),
       +                    incomplete_input: false,
       +                });
       +            }
       +            let attr = this.buffer_view.lock().get_caret_mut().get_attribute();
       +
       +            this.buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .set_char(
       +                    (x, y),
       +                    AttributedChar::new(unsafe { std::char::from_u32_unchecked(ch) }, attr),
       +                )
       +                .unwrap();
       +            Ok(())
       +        });
       +
       +        methods.add_method_mut("get_char", |_, this, (x, y): (i32, i32)| {
       +            let cur_layer = this
       +                .buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .get_current_layer();
       +            let layer_len = this.buffer_view.lock().get_buffer_mut().layers.len();
       +            if cur_layer >= layer_len {
       +                return Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Current layer {} out of range (0..<{})",
       +                        cur_layer, layer_len
       +                    ),
       +                    incomplete_input: false,
       +                });
       +            }
       +
       +            let ch = this.buffer_view.lock().get_buffer_mut().layers[cur_layer].get_char((x, y));
       +            Ok(ch.ch as u32)
       +        });
       +
       +        methods.add_method_mut("set_fg", |_, this, (x, y, col): (i32, i32, u32)| {
       +            let cur_layer = this
       +                .buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .get_current_layer();
       +            let layer_len = this.buffer_view.lock().get_buffer_mut().layers.len();
       +            if cur_layer >= layer_len {
       +                return Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Current layer {} out of range (0..<{})",
       +                        cur_layer, layer_len
       +                    ),
       +                    incomplete_input: false,
       +                });
       +            }
       +            let mut ch =
       +                this.buffer_view.lock().get_buffer_mut().layers[cur_layer].get_char((x, y));
       +            ch.attribute.set_foreground(col);
       +            this.buffer_view.lock().get_buffer_mut().layers[cur_layer].set_char((x, y), ch);
       +            Ok(())
       +        });
       +
       +        methods.add_method_mut("get_fg", |_, this, (x, y): (i32, i32)| {
       +            let cur_layer = this
       +                .buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .get_current_layer();
       +            let layer_len = this.buffer_view.lock().get_buffer_mut().layers.len();
       +            if cur_layer >= layer_len {
       +                return Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Current layer {} out of range (0..<{})",
       +                        cur_layer, layer_len
       +                    ),
       +                    incomplete_input: false,
       +                });
       +            }
       +
       +            let ch = this.buffer_view.lock().get_buffer_mut().layers[cur_layer].get_char((x, y));
       +            Ok(ch.attribute.get_foreground())
       +        });
       +
       +        methods.add_method_mut("set_bg", |_, this, (x, y, col): (i32, i32, u32)| {
       +            let cur_layer = this
       +                .buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .get_current_layer();
       +            let layer_len = this.buffer_view.lock().get_buffer_mut().layers.len();
       +            if cur_layer >= layer_len {
       +                return Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Current layer {} out of range (0..<{})",
       +                        cur_layer, layer_len
       +                    ),
       +                    incomplete_input: false,
       +                });
       +            }
       +            let mut ch =
       +                this.buffer_view.lock().get_buffer_mut().layers[cur_layer].get_char((x, y));
       +            ch.attribute.set_background(col);
       +            this.buffer_view.lock().get_buffer_mut().layers[cur_layer].set_char((x, y), ch);
       +            Ok(())
       +        });
       +
       +        methods.add_method_mut("get_bg", |_, this, (x, y): (i32, i32)| {
       +            let cur_layer = this
       +                .buffer_view
       +                .lock()
       +                .get_edit_state_mut()
       +                .get_current_layer();
       +            let layer_len = this.buffer_view.lock().get_buffer_mut().layers.len();
       +            if cur_layer >= layer_len {
       +                return Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Current layer {} out of range (0..<{})",
       +                        cur_layer, layer_len
       +                    ),
       +                    incomplete_input: false,
       +                });
       +            }
       +            let ch = this.buffer_view.lock().get_buffer_mut().layers[cur_layer].get_char((x, y));
       +            Ok(ch.attribute.get_background())
       +        });
       +
       +        methods.add_method_mut("print", |_, this, str: String| {
       +            for c in str.chars() {
       +                let mut pos = this.buffer_view.lock().get_caret_mut().get_position();
       +                let _ = this.buffer_view.lock().get_edit_state_mut().set_char(
       +                    pos,
       +                    AttributedChar::new(c, this.buffer_view.lock().get_caret_mut().get_attribute()),
       +                );
       +                pos.x += 1;
       +                this.buffer_view.lock().get_caret_mut().set_position(pos);
       +            }
       +            Ok(())
       +        });
       +
       +        methods.add_method_mut("gotoxy", |_, this, (x, y): (i32, i32)| {
       +            this.buffer_view
       +                .lock()
       +                .get_caret_mut()
       +                .set_position(Position::new(x, y));
       +            Ok(())
       +        });
       +
       +        methods.add_method_mut(
       +            "set_layer_position",
       +            |_, this, (layer, x, y): (usize, i32, i32)| {
       +                if layer < this.buffer_view.lock().get_buffer_mut().layers.len() {
       +                    let _ = this
       +                        .buffer_view
       +                        .lock()
       +                        .get_edit_state_mut()
       +                        .move_layer(Position::new(x, y));
       +                    Ok(())
       +                } else {
       +                    Err(mlua::Error::SyntaxError {
       +                        message: format!(
       +                            "Layer {} out of range (0..<{})",
       +                            layer,
       +                            this.buffer_view.lock().get_buffer_mut().layers.len()
       +                        ),
       +                        incomplete_input: false,
       +                    })
       +                }
       +            },
       +        );
       +        methods.add_method_mut("get_layer_position", |_, this, layer: usize| {
       +            if layer < this.buffer_view.lock().get_buffer_mut().layers.len() {
       +                let pos = this.buffer_view.lock().get_buffer_mut().layers[layer].get_offset();
       +                Ok((pos.x, pos.y))
       +            } else {
       +                Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Layer {} out of range (0..<{})",
       +                        layer,
       +                        this.buffer_view.lock().get_buffer_mut().layers.len()
       +                    ),
       +                    incomplete_input: false,
       +                })
       +            }
       +        });
       +
       +        methods.add_method_mut(
       +            "set_layer_visible",
       +            |_, this, (layer, is_visible): (i32, bool)| {
       +                let layer = layer as usize;
       +                if layer < this.buffer_view.lock().get_buffer_mut().layers.len() {
       +                    // todo
       +                    this.buffer_view.lock().get_buffer_mut().layers[layer].is_visible = is_visible;
       +                    Ok(())
       +                } else {
       +                    Err(mlua::Error::SyntaxError {
       +                        message: format!(
       +                            "Layer {} out of range (0..<{})",
       +                            layer,
       +                            this.buffer_view.lock().get_buffer_mut().layers.len()
       +                        ),
       +                        incomplete_input: false,
       +                    })
       +                }
       +            },
       +        );
       +
       +        methods.add_method_mut("get_layer_visible", |_, this, layer: usize| {
       +            if layer < this.buffer_view.lock().get_buffer_mut().layers.len() {
       +                Ok(this.buffer_view.lock().get_buffer_mut().layers[layer].is_visible)
       +            } else {
       +                Err(mlua::Error::SyntaxError {
       +                    message: format!(
       +                        "Layer {} out of range (0..<{})",
       +                        layer,
       +                        this.buffer_view.lock().get_buffer_mut().layers.len()
       +                    ),
       +                    incomplete_input: false,
       +                })
       +            }
       +        });
       +
       +        methods.add_method_mut("clear", |_, this, ()| {
       +            this.buffer_view.lock().get_buffer_mut().reset_terminal();
       +            Ok(())
       +        });
       +    }
       +}
   DIR diff --git a/src/ui/commands.rs b/src/ui/commands.rs
       @@ -363,4 +363,9 @@ keys![
                "menu-show_line_numbers",
                ToggleLineNumbers
            ),
       +    (
       +        open_plugin_directory,
       +        "menu-open_plugin_directory",
       +        OpenPluginDirectory
       +    ),
        ];
   DIR diff --git a/src/ui/messages.rs b/src/ui/messages.rs
       @@ -17,7 +17,7 @@ use icy_engine::{
        use crate::{
            util::autosave::{self},
            AnsiEditor, DocumentOptions, MainWindow, NewFileDialog, SaveFileDialog, SelectCharacterDialog,
       -    SelectOutlineDialog, Settings, SETTINGS,
       +    SelectOutlineDialog, Settings, PLUGINS, SETTINGS,
        };
        
        #[derive(Clone)]
       @@ -138,6 +138,8 @@ pub enum Message {
            SelectPalette,
            ToggleLayerBorders,
            ToggleLineNumbers,
       +    RunPlugin(usize),
       +    OpenPluginDirectory,
        }
        
        pub const CTRL_SHIFT: egui::Modifiers = egui::Modifiers {
       @@ -1019,6 +1021,29 @@ impl MainWindow {
                    Message::ToggleLineNumbers => unsafe {
                        SETTINGS.show_line_numbers = !SETTINGS.show_line_numbers;
                    },
       +            Message::RunPlugin(i) => {
       +                self.run_editor_command(i, |window, editor, i| {
       +                    let mut msg = None;
       +                    unsafe {
       +                        if let Err(err) = PLUGINS[i].run_plugin(window, editor) {
       +                            msg = Some(Message::ShowError(format!("Error running plugin: {err}")));
       +                        }
       +                    }
       +                    msg
       +                });
       +            }
       +            Message::OpenPluginDirectory => match Settings::get_plugin_directory() {
       +                Ok(dir) => {
       +                    if let Err(err) = open::that(dir) {
       +                        self.handle_message(Some(Message::ShowError(format!(
       +                            "Can't open font directory: {err}"
       +                        ))));
       +                    }
       +                }
       +                Err(err) => {
       +                    self.handle_message(Some(Message::ShowError(format!("{err}"))));
       +                }
       +            },
                }
            }
        }
   DIR diff --git a/src/ui/settings.rs b/src/ui/settings.rs
       @@ -7,7 +7,7 @@ use std::{
            path::{Path, PathBuf},
        };
        
       -use crate::TerminalResult;
       +use crate::{plugins::Plugin, TerminalResult};
        
        const MAX_RECENT_FILES: usize = 10;
        
       @@ -132,6 +132,24 @@ impl Settings {
                Err(IcyDrawError::ErrorCreatingDirectory("log_file".to_string()).into())
            }
        
       +    pub(crate) fn get_plugin_directory() -> TerminalResult<PathBuf> {
       +        if let Some(proj_dirs) = ProjectDirs::from("com", "GitHub", "icy_draw") {
       +            let dir = proj_dirs.config_dir().join("data/plugins");
       +
       +            if !dir.exists() {
       +                if fs::create_dir_all(&dir).is_err() {
       +                    return Err(IcyDrawError::ErrorCreatingDirectory(format!("{dir:?}")).into());
       +                }
       +                fs::write(
       +                    dir.join("elite-writing.lua"),
       +                    include_bytes!("../plugins/elite-writing.lua.txt"),
       +                )?;
       +            }
       +            return Ok(dir);
       +        }
       +        Err(IcyDrawError::ErrorCreatingDirectory("font directory".to_string()).into())
       +    }
       +
            pub(crate) fn load(path: &PathBuf) -> io::Result<Settings> {
                let file = File::open(path)?;
                let reader = BufReader::new(file);
       @@ -164,6 +182,8 @@ impl Settings {
            }
        }
        
       +pub static mut PLUGINS: Vec<Plugin> = Vec::new();
       +
        pub static mut SETTINGS: Settings = Settings {
            font_outline_style: 0,
            character_set: 5,
   DIR diff --git a/src/ui/top_bar.rs b/src/ui/top_bar.rs
       @@ -9,7 +9,7 @@ use icy_engine::{
            BufferType,
        };
        
       -use crate::{button_with_shortcut, MainWindow, Message, Settings, SETTINGS};
       +use crate::{button_with_shortcut, MainWindow, Message, Settings, PLUGINS, SETTINGS};
        
        pub struct TopBar {
            pub dock_left: RetainedImage,
       @@ -528,6 +528,8 @@ impl MainWindow {
                        });
        
                        self.commands.show_layer_borders.ui(ui, &mut result);
       +                self.commands.show_line_numbers.ui(ui, &mut result);
       +
                        self.commands.fullscreen.ui(ui, &mut result);
        
                        ui.separator();
       @@ -542,6 +544,28 @@ impl MainWindow {
                            .ui_enabled(ui, has_buffer, &mut result);
                    });
        
       +            unsafe {
       +                if !PLUGINS.is_empty() {
       +                    ui.menu_button(fl!(crate::LANGUAGE_LOADER, "menu-plugins"), |ui| {
       +                        for (i, p) in PLUGINS.iter().enumerate() {
       +                            if ui
       +                                .add_enabled(
       +                                    has_buffer,
       +                                    egui::Button::new(p.title.clone()).wrap(false),
       +                                )
       +                                .clicked()
       +                            {
       +                                result = Some(Message::RunPlugin(i));
       +                                ui.close_menu();
       +                            }
       +                        }
       +
       +                        ui.separator();
       +                        self.commands.open_plugin_directory.ui(ui, &mut result);
       +                    });
       +                }
       +            }
       +
                    ui.menu_button(fl!(crate::LANGUAGE_LOADER, "menu-help"), |ui| {
                        let r = ui.hyperlink_to(
                            fl!(crate::LANGUAGE_LOADER, "menu-discuss"),