diff --git a/app.py b/app.py
index 6efa89113fafc6ebd085f2fca2d83dd94433ee7f..72323858509971fc985b782c8faa75c2c481ae21 100644
--- a/app.py
+++ b/app.py
@@ -6,8 +6,53 @@ import dearpygui.dearpygui as dpg
 import numpy as np
 from dotenv import load_dotenv
 
-from static_ui import construct_ui
-from constants import ICON_PATH
+import config_ui
+import remote_run_ui
+import map_edit_ui
+from style import setup_fonts, setup_themes
+from handlers import setup_handlers
+from paths import ICON_PATH
+from remote_run_constants import LOMONOSOV
+
+
+def exit_callback():
+    connections = dpg.get_item_user_data('run_window').get('connections', [])
+    builds = dpg.get_item_user_data('run_window').get('builds', [])
+
+    # with open('credentials.txt', 'a+', encoding='utf-8') as log:
+    #     for host in connections:
+    #         conn = connections[host]
+    #         if conn.host == LOMONOSOV:
+    #             if conn.id_rsa_path:
+    #                 desc = f'{conn.host};{conn.username};{conn.id_rsa_path}'
+    #             else:
+    #                 desc = f'{conn.host};{conn.username}'
+    #         else:
+    #             desc = f'{conn.host};{conn.username};{conn.password}'
+    #         log.write(desc + '\n')
+
+    # with open('builds.txt', 'a+', encoding='utf-8') as log:
+    #     for machine, model in builds:
+    #         log.write(f'{machine};{model};{builds[(machine, model)]}\n')
+
+    # with open('sessions.txt', 'a+', encoding='utf-8') as log:
+    #     for row in dpg.get_item_children('models_table', 1):
+    #         running_session_id = dpg.get_item_alias(row)
+    #         model = dpg.get_value(f'{running_session_id}_model')
+    #         machine = dpg.get_value(f'{running_session_id}_machine')
+    #         log.write(f'{running_session_id};{machine};{model}\n')
+
+    for conn in connections:
+        connections[conn].close()
+
+def construct_ui():
+    remote_run_ui.construct_ui()
+    map_edit_ui.construct_ui()
+    config_ui.construct_ui()
+
+    setup_handlers()
+    setup_fonts()
+    setup_themes()
 
 load_dotenv()
 
@@ -25,6 +70,7 @@ if __name__ == '__main__':
     dpg.show_viewport()
 
     construct_ui()
+    dpg.set_exit_callback(exit_callback)
     
     dpg.set_primary_window('main', True)
     dpg.start_dearpygui()
diff --git a/callbacks.py b/callbacks.py
index ad52205f5859b18291ab078d2f2772f8132a0d42..4e8e0a4bed84d76efba5bcd2d8cb4903b4697d7a 100644
--- a/callbacks.py
+++ b/callbacks.py
@@ -1,9 +1,10 @@
 import dearpygui.dearpygui as dpg
 import tifffile
+from uuid import uuid4
 
 from utils import *
 from constants import SPACE, TREE_COLOR, ROAD_COLOR, BUILDING_COLOR
-from remote_run import start_remote_execution, start_model_build, get_server_output_structure
+from remote_run import start_execution_lab_server, start_model_build, get_server_output_structure
 from map_edit import dump_height_map, load_height_map, update_texture
 from styles import get_accent_color, apply_theme
 from generator import LCZ
@@ -166,7 +167,7 @@ def generate_map_clb(s, a, u):
     set_map_window_value('buildings_map', height_map)
     set_map_window_value('roads_map', np.zeros((h, w)))
     set_map_window_value('trees_map', np.zeros((h, w)))
-    set_map_window_value('height_map_file_path', f'{dpg.generate_uuid()}.txt')
+    set_map_window_value('height_map_file_path', f'{uuid4()}.txt')
     set_map_window_value('max_height', max_height)
 
     construct_map_layers(w, h)
@@ -231,7 +232,7 @@ def start_remote_execution_clb(s, a, u):
     if u == 'build_model':
         start_model_build(dpg.get_value('server_username'), dpg.get_value('server_password'), dpg.get_value('gitlab_username'), dpg.get_value('gitlab_password'))
     elif u == 'run_on_lab_server':
-        start_remote_execution(dpg.get_value('server_username'), dpg.get_value('server_password'))
+        start_execution_lab_server(dpg.get_value('server_username'), dpg.get_value('server_password'))
     elif u == 'download_output':
         s = get_server_output_structure(dpg.get_value('server_username'), dpg.get_value('server_password'))
         folders, filepaths = parse_dirs(s)
diff --git a/config-parser/config-parser.cpp b/config-parser/config-parser.cpp
index d81338429958f7dd1a0ce14eeec4fea28ccfebc7..2c699a3b3683945e70c89b7dfb2cfdd9f151a0f7 100644
--- a/config-parser/config-parser.cpp
+++ b/config-parser/config-parser.cpp
@@ -709,7 +709,6 @@ void nse::ConfigParser::print() const
 {
 	for (int i = 0; i < nvars; i++) {
 		var[i].print();
-		// getc(stdin);
 	}
 }
 
diff --git a/config.py b/config.py
index 0096a2b6ac9dc8076fc716330cfa3f3922999a4c..a35a5417feb8dee77fd42fb9c372fbabdaaa6460 100644
--- a/config.py
+++ b/config.py
@@ -3,6 +3,7 @@ import subprocess
 import os
 from enum import Enum
 
+from paths import CONFIG_PARSER_EXECUTABLE
 
 class CfgVar:
     def __init__(self, key, value=None, value_type='STRING', comment=""):
@@ -45,11 +46,7 @@ class Namespace(Token):
         return f"NAMESPACE {self.name}"
 
 
-def tokenize(file_path):
-    text = ""
-    if os.path.exists(file_path):
-        text = open(file_path, "r", encoding="utf-8").read()
-
+def tokenize(text):
     tokens = []
     it = text.__iter__()
 
@@ -114,12 +111,12 @@ def tokenize(file_path):
         return tokens
 
 
-def parse(file_path):
+def parse(text):
     stack = []
 
     data = dict()
 
-    tokens = tokenize(file_path)
+    tokens = tokenize(text)
     it = tokens.__iter__()
 
     try:
@@ -140,13 +137,32 @@ def parse(file_path):
     except StopIteration as e:
         return data
 
+class ConfigParserError(Exception):
+    pass
 
 def load_config(file_path):
-    config = parse(file_path)
+    try:
+        text = open(file_path, "r", encoding="utf-8").read()
+    except OSError as e:
+        raise OSError
+
+    lines = []
+
+    p = subprocess.Popen([CONFIG_PARSER_EXECUTABLE, file_path], stdout=subprocess.PIPE)
+    while True:
+        line = p.stdout.readline().decode()
+
+        if not line:
+            break
+
+        if 'unexpected' in line:
+            raise ConfigParserError
+
+        lines.append(line)
 
-    output = subprocess.run(["config-parser/parser-cli", file_path], capture_output=True, text=True)
+    config = parse(text)
 
-    for line in output.stdout.split("\n"):
+    for line in lines:
         match = re.match(r" > (\w+) '(.*)' = *(.*)", line)
         if match:
             value_type, path, value = match.groups()
@@ -235,4 +251,4 @@ def dump_config(config, file_path):
 
 
 if __name__ == '__main__':
-    print(load_config('config.txt'))
\ No newline at end of file
+    print(load_config('../config-ex.txt'))
\ No newline at end of file
diff --git a/config.txt b/config.txt
deleted file mode 100644
index ccd18ce4fd3ac017333c962d8008fde5c36075eb..0000000000000000000000000000000000000000
--- a/config.txt
+++ /dev/null
@@ -1,977 +0,0 @@
-MAIN_DIR = "";
-
-# sometimes you have to set main directory, e.g. on Lengau working in Lustre:
-# MAIN_DIR = "/mnt/lustre/users/my_account/build/";
-
-domain
-{
-#
-# domain setup in [meters]
-#   x - streamwise, y - spanwise, z - wall-normal directions
-#
-    x = 0.0; y = 0.0; z = 0.0;              # point of origin
-    length = 400.0; width = 200.0; height = 80.0;   # domain length(x), width(y) and height(z)
-}
-# -----------------------------------------------------------------------------
-
-time
-{
-    begin = 0.0; 
-    end = 50.0 * 3600.0;    # start and end time of integration [s]
-
-    dt = 0.2;       # time step [s]
-
-    # use_calendar = false;     # optional, default = false
-    calendar 
-    {
-        mode = "local"; 
-        # mode = "local" - local time
-        # mode = "local-UTC" - local UTC time
-        # mode = "set" - prescribed time & date as 
-        #       [year, month, day, hour, min, sec, UTC_offset] values
-
-        year = 2020;
-        month = 9;
-        day = 1;
-        hour = 12;
-        min = 0;
-        sec = 0.0;
-        UTC_offset = 3;
-    }
-}
-# -----------------------------------------------------------------------------
-
-grid
-{
-    type = "uniform";       # type = "uniform" || "stretched" || "stretched-up" || "z-coord-ascii"
-
-    filename = "z-coord.txt";   # argument for type = "z-coord-ascii"
-
-    cx = 80; cy = 40; cz = 16;  # number of cells in each direction
-
-    ksi_z = 1.2;            # near-wall grid stretching parameter
-
-    adaptive {
-        # mode = false;     # enable adaptive grid [optional, default = false]
-
-        beta = 0.3;     # relaxation time scale [<= 1]
-
-        dz_min = 0.25 * (domain.height / grid.cz);  # min grid step
-        dz_max = 4.0 * (domain.height / grid.cz);   # max grid step
-
-        # --- adaptation parameters
-        TKE_threshold_coeff = 0.1;  # threshold coeff. to define hbl
-        hbl_max_coeff = 1.1;        # boundary layer height multiplier
-        C_smooth_coeff = 5.0;       # number of cells in smoothing region
-
-        # --- begin & end time of adaptation [optional, default: all integration period]
-        # begin = time.begin; end = time.end;
-
-        nskip = 1;      # adaptation once in nskip iterations, each iteration := 1
-    }
-}
-# -----------------------------------------------------------------------------
-
-mpi_setup
-{
-#
-# MPI-process distribution
-#   in 'mpirun -np [N]' [N] overrides config specs if differs
-#
-    dimx = 2; dimy = 2; dimz = 1;
-}
-# -----------------------------------------------------------------------------
-
-phys
-{   
-    f = 0.0;                # coriolis frequency [1/s]
-
-    nu = 1.25 * 0.00001;            # kinematic viscosity [m^2/s]
-    xi = (1.0 / 0.7) * nu;          # thermal diffusivity [m^2/s]
-    
-    rho_ref = 1.25;             # reference density of air [kg/m^3]
-
-    g = 9.81;               # gravitational acceleration [m/s^2]
-    Theta_ref = 288.15;         # reference temperature [K]
-
-    # --- no buoyancy
-    beta = 0.0;                 # = g * thermal expansion coefficient = g / Theta_ref [m/(K*s^2)]
-}
-# -----------------------------------------------------------------------------
-
-# optinally define state variables
-#  default defs. depend on model configuration 
-state_def
-{
-    # Theta_p = false;
-    # Tabs = false; 
-
-    # Rho = false;
-    # Rho_ideal = false;
-
-    # Exner_pz = false;
-    # Pressure_ref_pz = false;
-
-    # Qliquid = false;
-    # Qsolid = false;
-    # Qvapor = false;
-
-    # Qvs_water = false; Pvs_water = false;
-    # Qvs_ice = false; Pvs_ice = false;
-}
-# -----------------------------------------------------------------------------
-
-topography
-{
-    mode = "hand";  # topography def. mode
-                # mode = "hand" -- define only 'patches' in configuration file
-                # mode = "ascii-mask" -- read height map from file
-
-    # --- using file height map parameters
-    # mode = "ascii-mask";
-    # filename = "map-ex.txt";      # filename path
-    # xmin = domain.x;          # map -x coordinates in computation domain
-    # xmax = domain.x + domain.length;
-    # ymin = domain.y;          # map -y coordinates in computation domain
-    # ymax = domain.y + domain.width;
-    # height_scale = 1.0;           # scale height factor
-
-    npatch = 1;         # number of patches     
-    patch_1 {
-        type = "box";           # patch type: "box" || "hill"
-        
-        xmin = 15.0; xmax = 35.0;   # patch dimensions
-        ymin = 10.0; ymax = 90.0;
-        height = 20.0;
-
-        xperiod = 50.0;         # optional periodicity in -x
-        yperiod = 100.0;                # optional periodicity in -y
-    }
-}
-# -----------------------------------------------------------------------------
-
-geo_wind
-{
-# NOTE: skipped if mode not set
-#   forcing priority: [t,z], [t], [const]
-
-    # --- geostrophic wind components
-    U = 0.0; V = 0.0;
-}
-# -----------------------------------------------------------------------------
-
-external_pressure_grad
-{
-# NOTE: skipped if mode not set
-#   forcing priority: [t,z], [t], [const]
-
-    # --- pressure gradient components [optional]
-    dPdx = -0.0004;         # [Pa/m]
-    # dPdy = 0.0;               # [Pa/m]
-}
-# -----------------------------------------------------------------------------
-
-subsidence 
-{
-}
-# -----------------------------------------------------------------------------
-
-external_tendency
-{
-}
-# -----------------------------------------------------------------------------
-
-nudging
-{
-}
-# -----------------------------------------------------------------------------
-
-rayleigh_friction
-{
-}
-# -----------------------------------------------------------------------------
-
-surface
-{
-    Theta {
-        mode = "const";
-
-        # --- fixed surface temperature
-        value = 288.15;     # initial surface temperature [K]
-    }
-    Qhum {
-        # --- predefined mode [land surface model]
-        mode = "lsm";
-    }
-
-    z0_m = 0.1;     # aerodynamic roughness [m]
-    z0_h = 0.1;     # heat roughness [m]
-
-    kappa = 0.4;        # von Karman constant
-    Pr_t_0 = 1.0;       # turbulent Prandt number (neutral)
-
-    # --- stability function coefficients
-    Cm = 4.8;
-    Ch = 7.8;
-
-    alpha_m = 16.0;
-    alpha_h = 16.0;
-
-    # --- latent heat flux alpha/beta model
-    lhflux_alpha = 1.0;
-    lhflux_beta = 0.025;
-}
-# -----------------------------------------------------------------------------
-
-initial_conditions
-{
-    # optional [U], [V]
-    #   if not set initial profiles are set to match geostrophic wind
-
-    U {
-        # mode = "half-channel";
-        # bulk = 6.0;
-    }
-
-    Theta { 
-        # --- predefined mode
-        mode = "const";
-
-        surface_value = 288.15; # initial boundary layer temperature [K]
-        height = 0.0;       # boundary layer height [m]
-        grad_z = 0.0;       # humidity gradient above boundary layer [kg/(kg*m)]
-    }
-
-    Qhum { 
-        # --- predefined mode
-        mode = "mixed-layer";
-
-        surface_value = 0.0025; # initial boundary layer humidity [kg/kg]
-        height = 0.0;       # boundary layer height [m]
-        grad_z = 0.0;       # humidity gradient above boundary layer [kg/(kg*m)]
-    }
-}
-# -----------------------------------------------------------------------------
-
-damping
-{
-    is_enabled = false;
-
-    # use_avgxy_ref = false;        # damp to -xy average profile [optional]
-
-    f = 0.2;                # damping frequency [1/s], = 0.2 (WRF model)
-
-    # --- damping layer [z1, z2]
-    z1 = domain.z + 0.75 * domain.height;   # [m]
-    z2 = domain.z + domain.height;      # [m]
-}
-# -----------------------------------------------------------------------------
-
-les
-{
-    is_dynamic_momentum = true;
-    is_dynamic_scalar = true;
-
-    is_ssm_mixed_momentum = false;
-    is_ssm_mixed_scalar = false;
-
-    is_amd_model = false;       # use AMD subgrid model 
-                    # dynamic, ssm keys not supported, have to be := false
-
-
-    # --- SSM model params
-    C_ssm_momentum = 1.0;
-    C_ssm_scalar = 1.0;
-
-
-    # --- static LES model params
-    C_smag = 0.08;          # GABLS-1 fit = 0.08
-                    # Lilly = 0.17
-    Prandtl_sgs = 0.7;      # subgrid scale Prandtl [0.4, 1.0]  
-
-
-    # --- dynamic LES model params
-    dynamic {
-        # --- dynamic coefficient = (C_{s} * delta_{g})^2 clipping
-        C_smag_max = 0.25;
-        Prandtl_sgs_min = 0.4;
-
-        alpha = 1.73;       # test-to-base filter width ratio
-
-        avg_mode = "lagrange";  # "none", "plane", "filter", "lagrange"
-
-        nskip = 3;
-        use_transport = false;
-
-        C_T_lagrange_momentum = 1.5;
-        C_T_lagrange_scalar = 3.0;
-    }
-
-    base_filter_reset = false;
-    test_filter_reset = false;
-    clip_filter_reset = false;
-}
-# -----------------------------------------------------------------------------
-
-passive_tracers
-{
-    # num = 2;  # number of tracers, skipped if not defined
-
-    # --- each tracer field defines diffusivity & surface values
-    tracer_1 { 
-        diffusivity = phys.xi;
- 
-        surface { 
-            flux = -0.01;
-
-            # --- setting localized source, all surface if not defined
-            xmin = 180.0; xmax = 220.0;
-            ymin = 180.0; ymax = 220.0;
-
-            # --- active in [begin, end], [time.begin, time.end] if not defined
-            begin = 7.0 * 3600.0;
-            # end = time.end;
-        }
-    }
-
-    tracer_2 {
-        # name = "tracer[2]";   # optional tracer name
- 
-        diffusivity = phys.xi; 
-
-        # --- setting 'life-time' using decay frequency [1/s] [optional]
-        # f_decay = 1.0 / 600.0;
-
-        surface {
-            flux = -0.02;
-
-            # --- active in [begin, end], [time.begin, time.end] if not defined
-            begin = 7.0 * 3600.0;
-            # end = time.end;    
-        }
-    }
-}
-# -----------------------------------------------------------------------------
-
-# particle simulation setup
-#  used only if INCLUDE_PARTICLES is defined, see [model-defines.h]
-ptcl
-{
-    is_passive_transport = false;       # passive particles flag
-    
-    # --- particle parameters for all sources
-    density = 1000.0;           # particle density [kg/m^3]
-    diameter = 0.000001;            # particle diameter [m]
-
-    g = -9.81;              # gravity acceleration [m/s^2]
-
-    # f_decay = 1.0 / 100.0;        # optional decay constant [1/s]
-                        # half-life = ln(2) / f
-
-    # --- number of sources
-    nsources = 1;
-
-    source_1 {
-        n = 524288 * 8;             # number of particles to release
-        begin = 5.0 * 3600.0;           # release time
-        end = time.end;         # instant release if not defined
-
-        # --- setting flux [lower priority than 'n' -- number of particles]
-        #   both begin & end must be specified -- not instant source
-        # flux = 10.0;
-        # flux_direction = "X";
-
-        # --- source volume
-        xmin = domain.x + 50.0 - 10.0; xmax = domain.x + 50.0 + 10.0;
-        ymin = domain.y;               ymax = domain.y + domain.width;
-        zmin = domain.z;               zmax = domain.z + 5.0;
-    }
-
-    # --- number of sinks [optional]
-    nsinks = 1;
-
-    sink_1 {
-        # --- optional, default mode = "inside"
-        mode = "outside";       # "inside" || "outside"
-
-        # --- define volume
-        xmin = domain.x;              xmax = domain.x + domain.length;
-        ymin = domain.y - 0.5 * domain.width; ymax = domain.y + 1.5 * domain.width;
-        zmin = domain.z + 0.01;           zmax = domain.z + domain.height;      
-    }
-}
-# -----------------------------------------------------------------------------
-
-# particle tracking setup
-#  used only if INCLUDE_PARTICLES_TRACKING is defined, see [model-defines.h]
-ptcl_track
-{
-    is_passive_transport = ptcl.is_passive_transport;
-    
-    # --- particle parameters for all sources
-    density = ptcl.density;     # particle density [kg/m^3]
-    diameter = ptcl.diameter;   # particle diameter [m]
-
-    g = ptcl.g;         # gravity acceleration [m/s^2]
-
-    # f_decay = ptcl.f_decay;       # optional decay constant [1/s]
-                        # half-life = ln(2) / f
-
-    group_max_size = 256;           # max number of particles per group
-    max_memory = 10 * 1024 * 1024;      # max memory in bytes for keeping trajectories in memory
-
-    # --- number of sources
-    nsources = 1;
-
-    source_1
-    {
-        n = 128;        # number of particles to release
-        begin = 8.0 * 3600.0;   # release time
-
-        # --- setting flux [lower priority than 'n' -- number of particles]
-        #   both begin & end must be specified -- not instant source
-        # flux = 100.0;
-        # flux_direction = "Z";
-
-        # --- source volume
-        xmin = domain.x + 0.4 * domain.length; xmax = domain.x + 0.6 * domain.length;
-        ymin = domain.y + 0.4 * domain.width; ymax = domain.y + 0.6 * domain.width;
-        zmin = domain.z; zmax = domain.z + 0.1 * domain.height;
-    }
-
-    # --- number of sinks [optional]
-    nsinks = 0;     
-}
-# -----------------------------------------------------------------------------
-
-# canopy setup
-#  used only if INCLUDE_CANOPY_DRAG is defined, see [model-defines.h]
-canopy
-{
-    Cd = 0.15;
-    drag_type = "non-linear"; # || "linearized" || "mean"
-
-    num = 0;
-    patch_1 { 
-        type = "sharp"; # || "fancy" || "obs"
-
-        # --- patch volume
-        xmin = domain.x; xmax = domain.x + domain.length;
-        ymin = domain.y; ymax = domain.y + domain.width;
-        zmin = domain.z; zmax = domain.z + 50.0;
-
-        # --- make patch 'periodic' in -x and/or -y directions
-        #   single patch if not defined
-        # xperiod = 1.0;
-        # yperiod = 1.0;
-
-        LAI = 1.0;
-        # --- set zm - height of max(LAD) for type = "obs":
-        # zm = zmin + 0.8 * (zmax - zmin);
-
-        # --- OR: set a file
-        # type = "sharp-map";
-        # filename = "map-ex.txt";
-
-        # switch_ij_order = false;  # [optional, default = false]
-        # normalize_height = 1.0;   # [optional, default = 1.0]
-    }
-}
-# -----------------------------------------------------------------------------
-
-poisson
-{
-#
-# Poisson equation solver setup
-#
-    # use_cg_solver = false;        # use CG as base solver [optional, default = BiCGstab]
-
-    retol = 0.0001; abstol = 0.00001;   # relative and absolute tolerance
-    miniters = 1; maxiters = 100;       # minimum and maximum number of iterations
-
-    piters = 1;             # number of preconditioner (multigrid) iterations
-
-    multigrid 
-    {
-        ngrid = 5;          # number of grids in multigrid sequence (= [0] - auto definition)
-
-        down_iters = 2;         # number of smoother iterations on fine->coarse traverse
-        up_iters = 3;           # number of smoother iterations on coarse->fine traverse
-        direct_iters = 5;       # number of smoother iterations on coarsest grid
-
-        smooth_up_omega = 1.84;     # relaxation value on coarse->fine traverse
-        smooth_up_omega_fine = 1.64;    # relaxation value on reaching finest grid
-    }
-}
-# -----------------------------------------------------------------------------
-
-output 
-{
-# NOTE: netcdf output is enabled only if INCLUDE_NETCDF is defined in [nse-sys.h]
-
-    DIR = MAIN_DIR + "output/";     # output directory
-    make_unique_DIR = true;     # make output directory unique for each run
-
-    convert_dsq_to_tecplot = true;  # convert series .dsq output to .plt [tecplot] format
-                        #   *: on model completion only
-    # convert_dsq_to_netcdf = false;    # convert series .dsq output to .nc [netcdf] format
-                        #   *: on model completion only
-
-    num = 1;            # number of output units
-
-    unit_1 
-    {
-        SUBDIR = "";                # unit sub-directory
-
-        begin = 1.0 * 3600.0;           # start time [s] for writing output
-        dt = 1.0 * 3600.0;          # time step [s] for writing output
-
-        # --- output subdomain [optional, applicable in 3D output only]
-        xmin = domain.x; xmax = domain.x + domain.length;   # -x output domain setup
-        ymin = domain.y; ymax = domain.y + domain.width;    # -y output domain setup
-        zmin = domain.z; zmax = domain.z + domain.height;   # -z output domain setup
-
-        # --- output controls [default, if not defined, value is 'false']
-        cntrl_avgxy_plt = true;     # 1D -xy averaged .plt fields
-        cntrl_avgxy_netcdf = false;     # 1D -xy averaged netcdf fields
-
-        cntrl_3d_plt = true;            # 3D .plt fields
-        cntrl_3d_bin = false;           # 3D .nsx fields
-        cntrl_3d_netcdf = false;        # 3D netcdf fields
-
-        cntrl_geometry_3d_plt = false;      # geometry 3D .plt fields
-        cntrl_geometry_3d_netcdf = false;   # geometry 3D netcdf fields
-
-        cntrl_topography_plt = true;        # topography .plt fields
-        cntrl_topography_netcdf = false;    # topography netcdf fields
-
-        cntrl_2d_plt = false;           # 2D .plt fields
-        cntrl_2d_netcdf = false;        # 2D netcdf fields
-
-        cntrl_surface_plt = true;       # surface .plt fields
-        cntrl_surface_netcdf = false;       # surface netcdf fields
-
-        cntrl_grid_plt = false;         # grid .plt data
-        cntrl_grid_netcdf = false;      # grid netcdf data
-
-        cntrl_meteo_avgxy_plt = false;      # 1D -xy averaged meteo forcing .plt fields
-        cntrl_meteo_all_plt = false;        # all meteo forcing .plt fields
-
-        # --- output keys for filtered fields
-        cntrl_filtered_3d_plt = false;
-        # filter { kxmin = 0; kxmax = 0; kymin = 0; kymax = 5; is_remove_mean = true; }
-
-        # --- spectrum output
-        spectrum
-        {
-            # --- controls
-            x_cntrl = true;
-            y_cntrl = true;
-            xy_cntrl = true;
-
-            # np = 2;       # number of z planes, skipped if not defined
-            z_1 = 50.0; 
-            z_2 = 100.0;
-
-            # --- additional options for 1d & 2d spectrum, default all = true if not defined
-            # is_remove_mean_1d = true;
-            # is_centered_fft_1d = true;
-
-            # is_remove_mean_2d = true;
-            # is_centered_fft_2d = true;
-            # is_log10_2d = true;
-        }
-
-        # --- particles & trajectories output
-        #  used only if INCLUDE_PARTICLES is defined
-        cntrl_ptcl_bin = false;         # particles binary output
-        cntrl_ptcl_plt = true;          # particles .plt output
-        cntrl_ptcl_coords_plt = false;      # particles coordinates .plt output
-        #  used only if INCLUDE_PARTICLES_TRACKING is defined
-        cntrl_ptcl_traj_bin = false;        # particles trajectories binary output
-        cntrl_ptcl_traj_plt = false;        # particles trajectories .plt output
-
-        # --- profiles [= 0 if not defined]
-        x_profile_num = 0;
-        y_profile_num = 0;
-        z_profile_num = 0;
-        # --- e.g.:
-        x_profile_1 { y = domain.y + 0.5 * domain.width; z = domain.z + 0.5 * domain.height; }
-
-        # --- slices [ = 0 if not defined]
-        xy_slice_num = 0;
-        xz_slice_num = 0;
-        yz_slice_num = 0;
-        # --- e.g.: 
-        xy_slice_1 { z = domain.z + 0.5 * domain.height; }
-    }
-
-    screen {
-        begin = time.begin; # start time [s] of onscreen output
-
-        # nskip = 360;      # output once in nskip iterations, each iteration := 1
-        dt = 0.05 * 3600.0; # output time step [s], --higher-- priority than 'nskip'
-
-        # --- screen controls [optional, default = true]
-        # cntrl_status = true;
-        # cntrl_progress_bar = true;
-        
-        # --- screen controls [optional, default = false]
-        # cntrl_terminal_mode = false;
-    }
-}
-# -----------------------------------------------------------------------------
-
-checkup
-{
-    # --- additional checkups [InF, NaN values etc.]
-    begin = 0.0;        # start time of checks
-
-    nskip = 3600;       # check once in nskip iterations, each iteration := 1
-    # dt = 1.0 * 3600.0;    # check time step, --higher-- priority than 'nskip'
-}
-# -----------------------------------------------------------------------------
-
-dump 
-{
-    DIR = MAIN_DIR + "dump/";   # dump directory
-
-    begin = 10.0 * 3600.0;      # start time [s] for writing model dump
-    dt    = 10.0 * 3600.0;      # time step [s] for writing model dump
-}
-# -----------------------------------------------------------------------------
-
-startup
-{
-    DIR = MAIN_DIR + "init/";   # initial conditions directory
-}
-# -----------------------------------------------------------------------------
-
-series
-{
-    begin = 60.0;       # start time [s]
-
-    # nskip = 2;        # calculate once in nskip iterations, each iteration := 1
-    dt = 1.0;       # time step [s], --higher-- priority than 'nskip'
-
-    # --- point flow measurements
-    point_set
-    {
-        # mode = "define";  # optional point set mode
-                    #  = "define" [default] || "grid-xy" 
-
-        # np = 3;   # number of points, skipped if not defined
-        point_1 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; z = 25.0; }
-        point_2 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; z = 50.0; }
-        point_3 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; z = 100.0; }
-
-        # --- OR: set point set grid on z=const planes
-        # mode = "grid-xy";
-
-        # --- domain
-        # x = domain.x; y = domain.y;
-        # length = domain.length; width = domain.width;
-
-        # --- grid dimensions, number of edges
-        # nx = 7; ny = 5;
-        # --- number of z=const planes
-        # nz = 3;
-        # z_1 = 25.0;
-        # z_2 = 50.0;
-        # z_3 = 100.0;
-    }
-
-    # --- [xy] averaged energy at fixed 'z'
-    energy_avgxy_set 
-    {
-        # np = 2;   # number of z planes, skipped if not defined
-        z_1 = 50.0; 
-        z_2 = 100.0;
-    }
-
-    # --- [xy] averaged fluxes at fixed 'z'
-    flux_avgxy_set
-    {
-        # np = 2;   # number of z planes, skipped if not defined
-        z_1 = 50.0;
-        z_2 = 100.0;
-    }
-
-    # --- additional parameters
-    #   --- TKE hbl def. [optional]
-    # TKE_hbl_threshold_stable = 0.3;
-    # TKE_hbl_threshold_unstable = 0.1;
-
-    #   --- max length to hold data in memory [optional]
-    # max_mem_length = 100 * 1024;
-}
-# -----------------------------------------------------------------------------
-
-spectrum_series
-{
-    begin = time.begin; # start time
-    
-    nskip = 1;      # calculate once in nskip iterations, each iteration := 1
-    # dt = 1.0;     # time step, --higher-- priority than 'nskip'
-
-    # num = 3;      # number of series, skipped if not defined
-
-    # --- wavevenumbers in: - kxmax <= kx <= kmax, 0 <= ky <= kymax 
-    unit_1 { zp = domain.z + 0.3 * domain.height; kxmax = 3; kymax = 3; }
-    unit_2 { zp = domain.z + 0.2 * domain.height; kxmax = 3; kymax = 3; }
-    unit_3 { zp = domain.z + 0.1 * domain.height; kxmax = 3; kymax = 3; }
-
-    #   --- max length to hold data in memory [optional]
-    # max_mem_length = 100 * 1024;
-}
-# -----------------------------------------------------------------------------
-
-time_scan
-{
-# NOTE: dump is not supported for time scans
-
-    begin = 60.0;       # scan start time [s]
-
-    # nskip = 1000;     # calculate once in nksip iterations, each iteration := 1
-    dt = 60.0;      # scan time step [s], --higher-- priority than 'nskip'
-
-    # --- optionally set output variables
-
-    cntrl_grid = false;     # all keys, optional, default = all 'false'
-    cntrl_grid {
-        # dz = true;
-
-        # --- cell center & edge coordinates
-        # pz = true; 
-        # ez = true;
-
-        # --- only in adaptive grid mode
-        # monitor_function = true;
-    }
-
-    cntrl_avgxy = true;     # all keys, optional, default = all 'false'
-    cntrl_avgxy {
-        # U = true;
-        # V = true;
-        # W = true;
-
-        # Theta = true;
-        # Theta_p = true;
-        # Tabs = true;
-
-        # Q = true;
-        # Qvapor = true;
-
-        # UW_flux = true;
-        # VW_flux = true;
-        # TW_flux = true;
-        # QW_flux = true;
-
-        # U_variance = true;
-        # V_variance = true;
-        # W_variance = true;
-        # Theta_variance = true;
-        # Q_variance = true;
-    }
-
-    cntrl_z_profile = false;    # all keys, optional, default = all 'false'
-    cntrl_z_profile {
-        # U = true;
-        # V = true;
-        # W = true;
-
-        # Theta = true;
-        # Theta_p = true;
-        # Tabs = true;
-
-        # Q = true;
-        # Qvapor = true;
-    }
-    
-    z_profile_num = 0;
-    z_profile_1 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; }
-}
-# -----------------------------------------------------------------------------
-
-stats
-{
-    num = 2;
-
-    unit_1 {
-        begin = time.end - 4.0 * 3600.0;        # start time for averaging
-        end = time.end;     # end time for averaging, to \infty if not defined  
-
-        nskip = 10;
-        axis = "XYZ";
-        type = "energy-eq";     
-
-        output.SUBDIR = "stat_end/";
-        dump.SUBDIR = "stat_end/";
-
-        # --- regular mode [optional] -- shifts accumulation window
-        # is_regular = false;
-
-        # --- ensemble mode [optional] -- ensemble averaging
-        # is_ensemble = false;
-    }
-    unit_2 {
-        begin = time.end - 8.0 * 3600.0;        # start time for averaging
-        end = time.end - 4.0 * 3600.0;      # end time for averaging, to \infty if not defined  
-
-        nskip = 10;
-        axis = "XYZ";
-        type = "energy-eq";     
-
-        output.SUBDIR = "stat_end-1/";
-        dump.SUBDIR = "stat_end-1";
-
-        # --- regular mode [optional] -- shifts accumulation window
-        # is_regular = false;
-
-        # --- ensemble mode [optional] -- ensemble averaging
-        # is_ensemble = false;
-    }
-}
-# -----------------------------------------------------------------------------
-
-pdf
-{
-    # num = 1;
-
-    unit_1 { 
-        begin = 7.5 * 3600.0;
-        end = 9.0 * 3600.0;
-        dt = 1.0;
-
-        zp = domain.z + 0.25 * domain.height;
-
-        output.SUBDIR = "pdf/";
-        dump.SUBDIR = "pdf/";
-
-        cntrl_default = true;
-        nbins_default = 256;
-
-        # --- reset options: [begin, reset_time] -- find histogram parameters 
-        #   if not defined set min-max range for each variable in controls
-        reset_time = 8.0 * 3600.0;
-        reset_safety = 0.25;    # relative to min-max found
-                    # e.g.: min' = min - reset_safety * (max - min)
-
-        # --- regular mode [optional] -- shifts accumulation window
-        # is_regular = false;
-
-        # --- change default control using keys
-        # cntrl { 
-        #   U = true; V = true; W = true; Pressure = true;
-        #   U_grad_x = true; U_grad_y = true; U_grad_z = true;
-        #   V_grad_x = true; V_grad_y = true; V_grad_z = true;
-        #   W_grad_x = true; W_grad_y = true; W_grad_z = true;
-        #   Suv = true; Suw = true; Svw = true;         # strain-tensor
-        #   Omega_uv = true; Omega_uw = true; Omega_vw = true;  # vorticity-tensor
-        #   Theta = true;
-        #   Theta_grad_x = true; Theta_grad_y = true; Theta_grad_z = true;
-        #   Q = true;
-        #   Q_grad_x = true; Q_grad_y = true; Q_grad_z = true;
-
-        # --- tracers 'C' correspond to passive_tracers {} def. [use index in: 1...passive_tracers.num]
-        #   C1 = true;
-        #   C1_grad_x = true; C1_grad_y = true; C1_grad_z = true;
-        #   C2 = true;
-        #
-        # }
-
-        # --- setting min-max for any variable
-        #   this has to be defined if 'reset_time' is skipped, e.g.:
-        # U { min = -1.0; max = 1.0; }
-
-        # --- setting number of bins for any variable, e.g.:
-        # U { nbins = 512; }
-    }
-}
-# -----------------------------------------------------------------------------
-
-joint_pdf
-{
-    # num = 1;
-
-    unit_1 { 
-        begin = 7.5 * 3600.0;
-        end = 9.0 * 3600.0;
-        dt = 1.0;
-
-        zp = domain.z + 0.25 * domain.height;
-
-        output.SUBDIR = "joint-pdf/";
-        dump.SUBDIR = "joint-pdf/";
-
-        cntrl_default = true;
-        nxbins_default = 256;
-        nybins_default = 256;
-
-        # --- reset options: [begin, reset_time] -- find histogram parameters 
-        #   if not defined set min-max range for each variable in controls
-        reset_time = 8.0 * 3600.0;
-        reset_safety = 0.25;    # relative to min-max found
-                    # e.g.: min' = min - reset_safety * (max - min)
-
-        # --- regular mode [optional] -- shifts accumulation window
-        # is_regular = false;
-
-        # --- change default control using keys
-        # cntrl { 
-        #   UV = true; UW = true; VW = true;
-        #   PSuu = true; PSvv = true; PSww = true;
-        #   PSuv = true; PSuw = true; PSvw = true;
-        #   TU = true; TV = true; TW = true;
-        #   QU = true; QV = true; QW = true;
-        #   QT = true;
-
-        # --- tracers 'C' correspond to passive_tracers {} def. [use index in: 1...passive_tracers.num]
-        #   C1U = true; C1V = true; C1W = true; 
-        #   C1T = true; C1Q = true;
-        # --- tracer/tracer joint pdf, using strict upper diagonal notation, e.g.:
-        #   C1C2 = true; C1C3 = true; C2C3 = true;
-        #
-        # }
-
-        # --- setting min-max for any variable
-        #   this has to be defined if 'reset_time' is skipped, e.g.:
-        # UV { xmin = -1.0; xmax = 1.0; ymin = - 1.0; ymax = 1.0; }
-
-        # --- setting number of bins for any variable, e.g.:
-        # UV { nxbins = 512; nybins = 512; }
-    }
-}
-# -----------------------------------------------------------------------------
-
-runtime_filter
-{
-    # begin = 600.0;    # set a regular filter
-    # dt = 600.0;
-
-    mark = 600.0;       # OR: apply filter at time = mark only
-                # mark has --higher-- priority than regular mode
-
-    # num = 0;  # number of filters applied, output = sum of all units
-            # u = u_f[1] + u_f[2] + ...
-            # --- skipped if not defined
-
-    unit_1 { 
-        # --- keep some rolls
-        mode = "include";
-        is_remove_mean = false;
-
-        kxmin = 0; kxmax = 0;
-        kymin = 0; kymax = 5;
-    }
-
-    unit_2 {
-        # --- add small scale disturbances
-        mode = "exclude";   # adding (u - u_f)
-        is_remove_mean = false;
-
-        kxmin = 0; kxmax = 10;
-        kymin = 0; kymax = 10;
-    }
-}
-# -----------------------------------------------------------------------------
diff --git a/config_callbacks.py b/config_callbacks.py
new file mode 100644
index 0000000000000000000000000000000000000000..0cf9b6c5e9ca9b32cdd104694b4f67fb6b2ef6e5
--- /dev/null
+++ b/config_callbacks.py
@@ -0,0 +1,76 @@
+import dearpygui.dearpygui as dpg
+
+from utils import get_config_file_path, delete_config_items_and_clear_aliases, update_config_values, get_config, update_config_values, set_main_window_value, change_color_temporarily, path_iter
+
+from style_constants import SPACE, COLLAPSING_HEADERS_UI
+from style import get_accent_color, apply_theme
+from config import load_config, dump_config, ConfigParserError
+from dynamic_ui import construct_config_ui, construct_model_output_structure_ui
+
+def set_file_path_clb(s, a, u):
+    file_path = a['file_path_name']
+    dpg.set_value(u, file_path)
+
+def construct_config_ui_clb(s, a, u):
+    if not get_config_file_path():
+        return
+
+    delete_config_items_and_clear_aliases()
+    update_config_values()
+    construct_config_ui(get_config_file_path(), get_config())
+
+def config_save_as_clb(s, a, u):
+    update_config_values()
+    dump_config(get_config(), a['file_path_name'])
+    show_message('file saved successfully:)')
+
+def config_open_clb(s, a, u):
+    delete_config_items_and_clear_aliases()
+    config_file_path = a['file_path_name']
+
+    try:
+        config = load_config(config_file_path)
+    except OSError:
+        show_message('no file found')
+        return
+    except ConfigParserError:
+        show_message('bad file format')
+        return
+
+    set_main_window_value('config', config)
+    set_main_window_value('config_file_path', config_file_path)
+    construct_config_ui(config_file_path, config)
+
+def config_save_clb(s, a, u):
+    update_config_values()
+    dump_config(get_config(), get_config_file_path())
+    show_message('file saved successfully:)')
+
+def open_config_open_dialog_clb(s, a, u):
+    dpg.show_item('config_open')
+
+def show_config_search_window_clb(s, a, u):
+    dpg.show_item('search_popup')
+
+def open_config_save_as_dialog_clb(s, a, u):
+    dpg.show_item('config_open')
+
+def search_clb(s, data, u):
+    if not get_config_file_path():
+        return
+
+    if not data:
+        return
+
+    for path in get_config():
+        if data.lower() in path.split('.')[-1]:
+            dpg.set_y_scroll('main', dpg.get_item_pos('.'.join([get_config_file_path(), path.split('.')[0]]))[1])
+            for tag in path_iter(path, prefix=get_config_file_path()):
+                if dpg.get_value(COLLAPSING_HEADERS_UI) == True:
+                    dpg.set_value(tag, True)
+                else:
+                    dpg.set_value(dpg.get_item_parent(dpg.get_item_parent(tag)), dpg.get_item_parent(tag))
+            change_color_temporarily('.'.join([get_config_file_path(), path, 'text']), color=get_accent_color())
+
+def apply_theme_clb(s, a, u):
+    apply_theme(s)
\ No newline at end of file
diff --git a/config_ui.py b/config_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..69c389611cc1b14aeda5a480c6f452d878e81d98
--- /dev/null
+++ b/config_ui.py
@@ -0,0 +1,61 @@
+import dearpygui.dearpygui as dpg
+
+from utils import combo_menu
+from style_constants import *
+from style import apply_theme, get_accent_color
+from config_callbacks import *
+
+def construct_main_window_ui():
+    with dpg.window(label='config file editor', tag='main', user_data={'config': None, 'config_file_path': None}):
+        with dpg.menu_bar():
+            with dpg.menu(label='config'):
+                dpg.add_menu_item(label='open\t\t', shortcut='ctrl+o', callback=open_config_open_dialog_clb, tag='open_config_open_dialog')
+                dpg.add_menu_item(label='search\t\t', shortcut='ctrl+f', callback=show_config_search_window_clb, tag='show_config_search_window')
+                dpg.add_menu_item(label='save\t\t', shortcut='ctrl+s', callback=config_save_clb, tag='save_config')
+                dpg.add_menu_item(label='save as\t\t', shortcut='ctrl+shift+s', callback=open_config_save_as_dialog_clb, tag='open_config_save_as_dialog')
+
+            dpg.add_menu_item(label='map', callback=lambda: dpg.show_item('map_window'))
+            dpg.add_menu_item(label='run', callback=lambda: dpg.show_item('run_window'))
+
+            with dpg.menu(label='view'):
+                with dpg.menu(label='ui', tag='ui'):
+                    for ui in UIS:
+                        dpg.add_menu_item(label=ui, check=True, default_value=False, callback=combo_menu(construct_config_ui_clb), tag=ui)
+                    dpg.set_value(UIS[1], True)
+
+                with dpg.menu(label='theme', tag='theme'):
+                    for theme in THEMES:
+                        dpg.add_menu_item(label=theme, check=True, default_value=False, callback=combo_menu(apply_theme_clb), tag=theme)
+                    dpg.set_value('orange', True)
+
+def construct_config_open_dialog():
+    with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=config_open_clb, tag='config_open', width=600, height=600):
+        dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_config_save_as_dialog():
+    with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=config_save_as_clb, tag='config_save_as', width=600, height=400):
+        dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_set_file_path_dialog():
+    with dpg.file_dialog(directory_selector=True, modal=True, show=False, callback=set_file_path_clb, tag='set_folder_dialog', width=500, height=400):
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_message_window():
+    with dpg.window(label='', no_collapse=True, autosize=True, no_resize=True, modal=True, tag='message_window', show=False, pos=(dpg.get_viewport_width() / 3, dpg.get_viewport_height() / 3)):
+        dpg.add_text('file saved successfully:)', tag='message')
+
+def construct_search_window():
+    with dpg.window(label='', no_collapse=True, autosize=True, no_resize=True, tag='search_popup', show=False, pos=(40, 40)):
+        dpg.add_input_text(hint='search for...', on_enter=True, callback=search_clb, tag='search')
+
+def construct_ui():
+    construct_main_window_ui()
+    construct_message_window()
+
+    construct_config_save_as_dialog()
+    construct_config_open_dialog()
+    construct_search_window()
+
+    construct_set_file_path_dialog()
\ No newline at end of file
diff --git a/constants.py b/constants.py
index ba44caf50b11c1534b5d27630dd7a8737c7d4411..28d924d673225bb3c0da17f43c524dfc691ac59d 100644
--- a/constants.py
+++ b/constants.py
@@ -42,7 +42,7 @@ TEXT = (0, 1)
 ACCENT = (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50)
 PRIMARY = (2, 3, 39)
 SECONDARY = (36, 33, 21, 24, 4, 7, 8, 9, 13, 14)
-APPLY_ALPHA = (1, 22, 25, 31, 34, 16)
+APPLY_ALPHA = (22, 25, 31, 34, 16)
 COLOR_TYPE_TO_DPG_ITEM = {
     'TEXT': (0, 1),
 	'PRIMARY': (2, 3, 39),
@@ -51,17 +51,33 @@ COLOR_TYPE_TO_DPG_ITEM = {
 COLOR_TO_COLOR_TYPE = {0: 'TEXT', 1: 'TEXT', 2: 'PRIMARY', 3: 'PRIMARY', 39: 'PRIMARY', 36: 'SECONDARY', 21: 'SECONDARY', 24: 'SECONDARY', 4: 'SECONDARY', 7: 'SECONDARY', 8: 'SECONDARY', 9: 'SECONDARY', 13: 'SECONDARY', 33: 'SECONDARY', 14: 'SECONDARY', 35: 'ACCENT', 20: 'ACCENT', 37: 'ACCENT', 22: 'ACCENT', 23: 'ACCENT', 5: 'ACCENT', 25: 'ACCENT', 26: 'ACCENT', 27: 'ACCENT', 28: 'ACCENT', 29: 'ACCENT', 10: 'ACCENT', 30: 'ACCENT', 11: 'ACCENT', 31: 'ACCENT', 12: 'ACCENT', 32: 'ACCENT', 49: 'ACCENT', 34: 'ACCENT', 15: 'ACCENT', 50: 'ACCENT', 16: 'ACCENT', 18: 'ACCENT', 19: 'ACCENT', 1: 'TEXT'}
 
 BLUE = {
-    'TEXT': (255, 255, 255, 255),
-    'PRIMARY': (37, 37, 38, 255),
-    'SECONDARY': (51, 51, 55, 255),
-    'ACCENT': (0, 119, 200, 153)
+    'enabled': {
+        'TEXT': (255, 255, 255, 255),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (0, 119, 200, 153)
+    },
+    'disabled': {
+        'TEXT': (255, 255, 255, 150),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (0, 119, 200, 153)
+    },
 }
 
 ORANGE = {
-    'TEXT': (255, 255, 255, 255),
-    'PRIMARY': (37, 37, 38, 255),
-    'SECONDARY': (51, 51, 55, 255),
-    'ACCENT': (251, 133, 0, 153)
+    'enabled': {
+        'TEXT': (255, 255, 255, 255),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (251, 133, 0, 153)
+    },
+    'disabled': {
+        'TEXT': (255, 255, 255, 150),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (251, 133, 0, 153)
+    },
 }
 
 THEMES = {
diff --git a/dynamic_ui.py b/dynamic_ui.py
index 1b1df188c38216c9e4daa8d711237778fb84a08a..7a8d52922b8c599acaaf534440b7828dba36f841 100644
--- a/dynamic_ui.py
+++ b/dynamic_ui.py
@@ -1,13 +1,13 @@
 import dearpygui.dearpygui as dpg
 
-from utils import *
-from constants import *
+from utils import change_height, get_current_ui_type, get_config, get_config_file_path, does_tab_bar_child_exist, get_max_n_emission_point, set_main_window_value
+from style_constants import *
+from config import CfgVar
 
 def check_every_child_checkbox(s, a):
     parent = dpg.get_item_parent(s)
 
     for child in dpg.get_item_children(parent, 1):
-        print(s, dpg.get_item_alias(child), child, dpg.get_item_type(child))
         t = dpg.get_item_type(child)
         if t == 'mvAppItemType::mvGroup':
             dpg.set_value(dpg.get_item_children(child, 1)[1], a)
@@ -19,6 +19,29 @@ def check_every_child_checkbox(s, a):
 def folder_checkbox_clb(s, a, u):
     check_every_child_checkbox(s, a)
 
+def construct_model_output_structure_ui(filepaths, folders):
+    dpg.add_collapsing_header(label='output', parent='model_output_child_window', tag='output_header', indent=0, default_open=True)
+    dpg.add_checkbox(label='select all', default_value=False, parent='output_header', tag='output', callback=folder_checkbox_clb)
+
+    for path in folders:
+        folders = path.split('/')
+        parent = 'output_header'
+        for folder in folders:
+            if not dpg.does_item_exist(f'{folder}_header'):
+                with dpg.collapsing_header(label=folder, parent=parent, tag=f'{folder}_header', indent=INDENT):
+                    dpg.add_checkbox(label='select all', default_value=False, tag=folder, callback=folder_checkbox_clb)
+
+            parent = f'{folder}_header'
+
+    for filepath in filepaths:
+        if '/' not in filepath:
+            parent = 'output_header'
+        else:
+            parent = filepath.split('/')[-2] + '_header'
+        with dpg.group(horizontal=True, parent=parent, indent=INDENT, xoffset=XOFFSET):
+            dpg.add_text(filepath.split('/')[-1], tag=f'{filepath}.text')
+            dpg.add_checkbox(default_value=False, tag=filepath)
+
 def open_folder_dialog_clb(s, a, u):
     dpg.show_item('set_folder_dialog')
     dpg.set_item_user_data('set_folder_dialog', u)
@@ -79,6 +102,7 @@ def add_emission_point_and_update_ui(x, y):
         'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'),
         'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]')
     }
+    
     prefix = f'passive_tracers.tracer_{get_max_n_emission_point() + 1}.point_emission'
 
     config = get_config()
@@ -90,51 +114,6 @@ def add_emission_point_and_update_ui(x, y):
 
     set_main_window_value('config', config)
 
-def delete_emission_point_and_update_ui(x, y):
-    emission_point = {
-        'value': CfgVar(key='value', value=28481176.531511, value_type='DOUBLE', comment=''),
-        'begin': CfgVar(key='begin', value=25200.0, value_type='DOUBLE', comment='[s]'),
-        'xpos': CfgVar(key='xpos', value=x, value_type='DOUBLE', comment='[m]'),
-        'ypos': CfgVar(key='ypos', value=y, value_type='DOUBLE', comment='[m]'),
-        'zpos': CfgVar(key='zpos', value=60.0, value_type='DOUBLE', comment='[m]'),
-        'sx': CfgVar(key='sx', value=20.0, value_type='DOUBLE', comment='[m]'),
-        'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'),
-        'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]')
-    }
-    prefix = f'passive_tracers.tracer_{get_max_n_emission_point() + 1}.point_emission'
-
-    config = get_config()
-
-    for key in emission_point:
-        full_path = '.'.join([prefix, key])
-        config[full_path] = emission_point[key]
-        add_item_to_config_ui(full_path)
-
-    set_main_window_value('config', config)
-
-def construct_model_output_structure_ui(filepaths, folders):
-    dpg.add_collapsing_header(label='output', parent='model_output_window', tag='output_header', indent=0, default_open=True)
-    dpg.add_checkbox(label='select all', default_value=False, parent='output_header', tag='output', callback=folder_checkbox_clb)
-
-    for path in folders:
-        folders = path.split('/')
-        parent = 'output_header'
-        for folder in folders:
-            if not dpg.does_item_exist(f'{folder}_header'):
-                with dpg.collapsing_header(label=folder, parent=parent, tag=f'{folder}_header', indent=INDENT):
-                    dpg.add_checkbox(label='select all', default_value=False, tag=folder, callback=folder_checkbox_clb)
-
-            parent = f'{folder}_header'
-
-    for filepath in filepaths:
-        if '/' not in filepath:
-            parent = 'output_header'
-        else:
-            parent = filepath.split('/')[-2] + '_header'
-        with dpg.group(horizontal=True, parent=parent, indent=INDENT, xoffset=XOFFSET):
-            dpg.add_text(filepath.split('/')[-1], tag=f'{filepath}.text')
-            dpg.add_checkbox(default_value=False, tag=filepath)
-
 def construct_config_ui_collapsing_headers(config_file_path, config):
     dpg.add_collapsing_header(label=config_file_path, parent='main', tag=config_file_path, indent=0, default_open=True)
 
diff --git a/handlers.py b/handlers.py
index 77be6bd95c2ee07d4bd05dd45824d783f5663ee2..4f9eedd62228222f891b6b8ee75b9d35d58dc1ff 100644
--- a/handlers.py
+++ b/handlers.py
@@ -1,26 +1,24 @@
 import dearpygui.dearpygui as dpg
 
-from utils import *
-from callbacks import *
+from utils import get_config_file_path, change_height
 from map_edit import mouse_down_callback, mouse_release_callback, mouse_click_callback
+from config_callbacks import config_save_clb
 
 def on_ctrl_o(sender, app_data):
-    if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_O):
+    if dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_O):
         dpg.show_item('config_open')
 
 def on_ctrl_f(sender, app_data):
-    if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_F):
+    if dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_F):
         dpg.show_item('search_popup')
 
 def on_ctrl_s(sender, app_data):
-    if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_Shift) and dpg.is_key_down(dpg.mvKey_S):
+    if dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_LShift) and dpg.is_key_down(dpg.mvKey_S):
         dpg.show_item('config_save_as')
-    elif dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_S):
-        config_save_clb()
+    elif dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_S):
+        config_save_clb('', '', '')
 
 def change_height_():
-    dpg.set_item_pos('status_text', (dpg.get_item_width('main') - dpg.get_text_size(dpg.get_value('status_text'))[0] - 2 * SPACE, dpg.get_item_pos('status_text')[1]))
-
     if get_config_file_path():
         change_height(get_config_file_path())
 
diff --git a/map_edit.py b/map_edit.py
index 4b2a8618f4652125610b2c0281b13c7c6e408750..5b96d1dbe38c9c4f9992eab4aa5edf98e153cac6 100644
--- a/map_edit.py
+++ b/map_edit.py
@@ -1,12 +1,13 @@
 import dearpygui.dearpygui as dpg
 
-from utils import *
-from callbacks import *
-from constants import MAX_MAP_WIDTH, MAX_MAP_HEIGHT, EMISSION_POINT_COLOR
-from dynamic_ui import construct_config_ui, add_emission_point_and_update_ui
+import numpy as np
+
+from utils import get_map_window_value, get_config_file_path, is_drawing, get_current_action, set_map_window_value, get_emission_points, get_prev_coords, get_height_map_file_path
+from map_edit_constants import EMISSION_POINT_COLOR
+from dynamic_ui import add_emission_point_and_update_ui
 
 def load_height_map(file_path):
-    with open(file_path, "r") as file:
+    with open(file_path, "r", encoding='utf-8') as file:
         lines = file.readlines()[1:]
         height_map = [list(map(int, line.strip().split())) for line in lines]
     return np.array(height_map, dtype=np.float64)
@@ -26,12 +27,12 @@ def update_texture():
             x = int(emission_points[n]['xpos'])
             y = int(emission_points[n]['ypos'])
             coords = get_triangle_coords(x, y)
-            dpg.draw_polygon(coords, color=EMISSION_POINT_COLOR, fill=(255, 0, 0, 255), parent=f'{get_height_map_file_path()}map_drawlist', thickness=0)
+            tag = f'emission_point_{n}_triangle'
+            if not dpg.does_item_exist(tag):
+                dpg.draw_polygon(coords, color=EMISSION_POINT_COLOR, fill=(255, 0, 0, 255), tag=tag, parent='map_drawlist', thickness=0)
 
     texture_map = np.repeat(get_map_window_value(f'{dpg.get_value('layer')}_map'), 4)
 
-    print(texture_map.shape, np.all(texture_map == 0), f'map_{dpg.get_value('layer')}_texture', np.all(get_map_window_value(f'{dpg.get_value('layer')}_map') == 0))
-
     texture_map[3::4] = (texture_map[::4] != 0)
 
     dpg.set_value(f'map_{dpg.get_value('layer')}_texture', texture_map)
@@ -59,6 +60,7 @@ def mouse_down_callback():
         return
     if not dpg.is_item_focused('map_child_window'):
         return
+        
     if not is_drawing():
         set_map_window_value('prev_coords', dpg.get_drawing_mouse_pos())
         set_map_window_value('drawing', True)
@@ -80,8 +82,6 @@ def mouse_release_callback():
     if not dpg.is_item_focused('map_child_window'):
         return
 
-    dpg.hide_item('emission_values')
-
     coords = dpg.get_drawing_mouse_pos()
 
     if get_current_action() == 'erase':
@@ -106,6 +106,10 @@ def mouse_click_callback():
     if not get_config_file_path():
         return
 
+    if dpg.is_item_visible('emission_settings_window'):
+        dpg.hide_item('emission_settings_window')
+        return
+
     emission_points = get_emission_points()
 
     x, y = dpg.get_drawing_mouse_pos()
@@ -113,11 +117,11 @@ def mouse_click_callback():
     is_pos_emission_point, n = is_mouse_pos_emission_point(x, y, emission_points)
 
     if is_pos_emission_point:
-        dpg.set_item_width('emission_values', 300)
-        dpg.set_item_height('emission_values', 300)
-        dpg.set_item_pos('emission_values', dpg.get_drawing_mouse_pos())
-        dpg.show_item('emission_values')
-        dpg.set_value('emission_settings', f'value: {emission_points[n]['value']}\nbegin: {emission_points[n]['begin']}')
+        for key in emission_points[n]:
+            dpg.set_value(f'emission_point_{key}', emission_points[n][key])
+        dpg.set_item_user_data('emission_settings_window', n)
+        dpg.set_item_pos('emission_settings_window', dpg.get_drawing_mouse_pos())
+        dpg.show_item('emission_settings_window')
     else:
         if 0 <= x < dpg.get_item_width('map_child_window') and 0 <= y < dpg.get_item_height('map_child_window'):
             add_emission_point_and_update_ui(x, y)
@@ -150,21 +154,6 @@ def is_mouse_pos_emission_point(posx, posy, emission_points, side=30):
             return True, n
     return False, 0
 
-def draw_triangle(array, x, y, side=30, color=1.):
-    
-    h = (3 ** 0.5 / 2) * side
-
-    x1, y1 = x, y - (2 / 3) * h
-    x2, y2 = x + side / 2, y + (1 / 3) * h
-    x3, y3 = x - side / 2, y + (1 / 3) * h
-
-    for i in range(y - side, y + side):
-        for j in range(x - side, x + side):
-            if is_point_in_triangle(x1, y1, x2, y2, x3, y3, j, i):
-                array[i, j] = color
-
-    return array
-
 def get_triangle_coords(x, y, side=30):
     h = (3 ** 0.5 / 2) * side
 
diff --git a/map_edit_callbacks.py b/map_edit_callbacks.py
new file mode 100644
index 0000000000000000000000000000000000000000..f13050f26e26fe1e3f2e6c4347fbfdee780cdac0
--- /dev/null
+++ b/map_edit_callbacks.py
@@ -0,0 +1,217 @@
+import dearpygui.dearpygui as dpg
+import tifffile
+import numpy as np
+
+from map_edit_constants import TREE_COLOR, ROAD_COLOR, BUILDING_COLOR, LAYER_TO_COLOR
+from map_edit import dump_height_map, load_height_map, update_texture
+from generator import LCZ
+from utils import show_message, set_main_window_value, get_config, get_config_file_path, delete_item_and_clear_alias, delete_item_children_and_clear_aliases, set_map_window_value, get_map_window_value, get_height_map_file_path
+
+def delete_emission_point_clb(s, a, u):
+    n = dpg.get_item_user_data('emission_settings_window')
+    prefix = f'{get_config_file_path()}.passive_tracers.tracer_{n}.point_emission'
+
+    keys = ['value', 'begin', 'xpos', 'ypos', 'zpos', 'sx', 'sy', 'sz']
+
+    config = get_config()
+
+    for key in keys:
+        path = f'passive_tracers.tracer_{n}.point_emission.{key}'
+        del config[path]
+
+    set_main_window_value('config', config)
+
+    delete_item_children_and_clear_aliases(prefix)
+
+    dpg.hide_item('emission_settings_window')
+
+    tag = f'emission_point_{n}_triangle'
+
+    dpg.delete_item(tag)
+
+def set_emission_point_value_clb(s, value, u):
+    n = dpg.get_item_user_data('emission_settings_window')
+
+    key = s.split('_')[-1]
+
+    path = f'{get_config_file_path()}.passive_tracers.tracer_{n}.point_emission.{key}'
+
+    if dpg.does_item_exist(path):
+        dpg.set_value(path, value)
+
+    config = get_config()
+
+    config[path].value = value
+
+    set_main_window_value('config', config)
+
+def clear_map():
+    delete_item_and_clear_alias('map_buildings_texture')
+    delete_item_and_clear_alias('map_trees_texture')
+    delete_item_and_clear_alias('map_roads_texture')
+    delete_item_children_and_clear_aliases('map_drawlist')
+    settings = {
+        'buildings_map': None,
+        'trees_map': None,
+        'roads_map': None,
+        'height_map_file_path': None,
+        'drawing': False,
+        'prev_coords': None,
+        'current_action': None,
+        'max_height': 0
+    }
+    dpg.set_item_user_data('map_window', settings)
+
+def map_window_on_close_clb(s, a, u):
+    clear_map()
+
+def construct_map_layers(w, h):
+    dpg.set_item_width('map_child_window', w)
+    dpg.set_item_height('map_child_window', h)
+
+    dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag='map_buildings_texture', parent='textures')
+    dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag='map_trees_texture', parent='textures')
+    dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag='map_roads_texture', parent='textures')
+
+    dpg.bind_item_theme('map_child_window', 'map_image_theme')
+    dpg.add_drawlist(w, h, tag=f'map_drawlist', parent='map_child_window', pos=[0, 0])
+
+    dpg.draw_image(f'map_buildings_texture', color=BUILDING_COLOR, tag=f'map_buildings_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist')
+    dpg.draw_image(f'map_trees_texture', color=TREE_COLOR, tag=f'map_trees_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist')
+    dpg.draw_image(f'map_roads_texture', color=ROAD_COLOR, tag=f'map_roads_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist')
+
+def map_open_layer_clb(s, a, u):
+    height_map_file_path = a['file_path_name']
+
+    layer = dpg.get_value('layer_radio_button')
+
+    if height_map_file_path.endswith('txt'):
+        try:
+            height_map = load_height_map(height_map_file_path)
+        except FileNotFoundError:
+            show_message('no file found')
+            return
+        except Exception:
+            show_message('bad file format')
+            return
+        max_height = np.max(height_map)
+        height_map /= max_height
+    else:
+        height_map = tifffile.imread(height_map_file_path)
+        max_height = 1
+
+    h, w = height_map.shape
+
+    delete_item_and_clear_alias(f'map_{layer}_texture')
+
+    if dpg.does_alias_exist(f'map_{layer}_texture'):
+        dpg.remove_alias(f'map_{layer}_texture')
+
+    delete_item_and_clear_alias(f'map_{layer}_image')
+
+    if dpg.does_alias_exist(f'map_{layer}_image'):
+        dpg.remove_alias(f'map_{layer}_image')
+
+    set_map_window_value(f'{layer}_map', height_map)
+
+    dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag=f'map_{layer}_texture', parent='textures')
+    
+    dpg.draw_image(f'map_{layer}_texture', color=LAYER_TO_COLOR[layer], tag=f'map_{layer}_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist')
+
+    dpg.set_value('layer', layer)
+
+    update_texture()
+
+def map_open_clb(s, a, u):
+    height_map_file_path = a['file_path_name']
+
+    if height_map_file_path.endswith('txt'):
+        try:
+            height_map = load_height_map(height_map_file_path)
+        except FileNotFoundError:
+            show_message('no file found')
+            return
+        except Exception:
+            show_message('bad file format')
+            return
+        max_height = np.max(height_map)
+        height_map /= max_height
+    else:
+        height_map = tifffile.imread(height_map_file_path)
+        max_height = 1
+
+    h, w = height_map.shape
+
+    clear_map()
+
+    set_map_window_value('buildings_map', height_map)
+    set_map_window_value('roads_map', np.zeros((h, w)))
+    set_map_window_value('trees_map', np.zeros((h, w)))
+    set_map_window_value('height_map_file_path', height_map_file_path)
+    set_map_window_value('max_height', max_height)
+
+    construct_map_layers(w, h)
+
+    dpg.set_value('layer', 'buildings')
+    
+    update_texture()
+
+    dpg.show_item('map_window')
+
+def generate_map_clb(s, a, u):
+    height_map = LCZ(config_path=f'configs/{s}.json').to_height_map(dtype=np.float64)
+
+    max_height = np.max(height_map)
+    height_map /= max_height
+
+    h, w = height_map.shape
+
+    clear_map()
+
+    set_map_window_value('buildings_map', height_map)
+    set_map_window_value('roads_map', np.zeros((h, w)))
+    set_map_window_value('trees_map', np.zeros((h, w)))
+    set_map_window_value('height_map_file_path', 'map.txt')
+    set_map_window_value('max_height', max_height)
+
+    construct_map_layers(w, h)
+
+    dpg.set_value('layer', 'buildings')
+
+    update_texture()
+
+    dpg.show_item('map_window')
+
+def save_map(file_path):
+    if file_path.endswith('.tif') or file_path.endswith('.tiff'):
+        with TiffWriter(file_path) as tif:
+            tif.write(get_map_window_value('buildings_map'))
+            tif.write(get_map_window_value('roads_map'))
+            tif.write(get_map_window_value('trees_map'))
+    else:
+        dump_height_map(get_map_window_value('buildings_map'), file_path)
+
+def map_save_clb(s, a, u):
+    file_path = get_height_map_file_path()
+    save_map(file_path)
+    show_message('file saved successfully:)')
+
+def map_save_as_clb(s, a, u):
+    file_path = a['file_path_name']
+    save_map(file_path)
+    show_message('file saved successfully:)')
+
+def open_map_save_as_dialog_clb(s, a, u):
+    dpg.show_item('map_save_as')
+
+def open_map_open_dialog_clb(s, a, u):
+    dpg.show_item('map_open')
+
+def open_map_open_layer_dialog_clb(s, a, u):
+    dpg.show_item('map_open_layer')
+
+def set_action_clb(s, a, u):
+    set_map_window_value('current_action', s)
+
+def set_action_none_clb(s, a, u):
+    set_map_window_value('current_action', None)
\ No newline at end of file
diff --git a/map_edit_constants.py b/map_edit_constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..0265eb5bf3092b815d711699fb64d8b742bc32c3
--- /dev/null
+++ b/map_edit_constants.py
@@ -0,0 +1,25 @@
+from config import CfgVar
+
+ROAD_COLOR = (221, 161, 94, 255)
+TREE_COLOR = (96, 108, 56, 255)
+BUILDING_COLOR = (188, 108, 37, 255)
+EMISSION_POINT_COLOR = (255, 0, 0, 255)
+
+LAYER_TO_COLOR = {
+    'buildings': BUILDING_COLOR,
+    'trees': TREE_COLOR,
+    'roads': ROAD_COLOR
+}
+
+LCZS = ['compact_high_rise', 'compact_low_rise', 'compact_mid_rise', 'heavy_industry', 'large_low_rise', 'lightweight_low_rise', 'open_high_rise', 'open_low_rise', 'open_mid_rise', 'sparsley_build']
+
+EMISSION_POINT_CONFIG_VARIABLES = {
+    'value': CfgVar(key='value', value=28481176.531511, value_type='DOUBLE', comment=''),
+    'begin': CfgVar(key='begin', value=25200.0, value_type='DOUBLE', comment='[s]'),
+    'xpos': CfgVar(key='xpos', value=200.0, value_type='DOUBLE', comment='[m]'),
+    'ypos': CfgVar(key='ypos', value=200.0, value_type='DOUBLE', comment='[m]'),
+    'zpos': CfgVar(key='zpos', value=60.0, value_type='DOUBLE', comment='[m]'),
+    'sx': CfgVar(key='sx', value=20.0, value_type='DOUBLE', comment='[m]'),
+    'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'),
+    'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]')
+}
\ No newline at end of file
diff --git a/map_edit_ui.py b/map_edit_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff877d3bf233799749b746ac30b270cc1cb7307c
--- /dev/null
+++ b/map_edit_ui.py
@@ -0,0 +1,123 @@
+import dearpygui.dearpygui as dpg
+
+from map_edit_constants import *
+from utils import combo_menu, apply_tint_func
+from map_edit_callbacks import *
+
+def construct_map_open_dialog():
+    with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=map_open_clb, tag='map_open', width=600, height=400):
+        dpg.add_file_extension("Map files (*.txt *.tif *.tiff){.txt,.tif,.tiff}", color=(0, 255, 0, 255))
+
+def construct_map_open_layer_dialog():
+    with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=map_open_layer_clb, tag='map_open_layer', width=600, height=400):
+        dpg.add_file_extension("Map files (*.txt *.tif *.tiff){.txt,.tif,.tiff}", color=(0, 255, 0, 255))
+        
+        dpg.add_radio_button(items=('buildings', 'trees', 'roads'), default_value='buildings', tag='layer_radio_button')
+
+def construct_map_save_as_dialog():
+    with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=map_save_as_clb, tag='map_save_as', width=600, height=400):
+        dpg.add_file_extension("Map files (*.txt *.tif *.tiff){.txt,.tif,.tiff}", color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_map_edit_window():
+    user_data = {
+        'buildings_map': None,
+        'trees_map': None,
+        'roads_map': None,
+        'height_map_file_path': None,
+        'drawing': False,
+        'prev_coords': None,
+        'current_action': None,
+        'max_height': 0
+    }
+
+    dpg.add_texture_registry(tag='textures')
+
+    with dpg.theme() as item_theme:
+        with dpg.theme_component(dpg.mvAll):
+            dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (0, 0, 0, 0), category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 0, category=dpg.mvThemeCat_Core)
+
+    with dpg.theme(tag='map_image_theme'):
+        with dpg.theme_component(dpg.mvAll):
+            dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0, 0, category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_ItemSpacing, 0, 0, category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 0, 0, category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_ItemInnerSpacing, 0, 0, category=dpg.mvThemeCat_Core)
+
+    dpg.add_colormap_registry(label='colormap registry', tag='colormap_registry')
+    dpg.add_colormap(list(map(apply_tint_func(BUILDING_COLOR), [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='buildings_colormap', parent='colormap_registry')
+    dpg.add_colormap(list(map(apply_tint_func(TREE_COLOR), [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='trees_colormap', parent='colormap_registry')
+    dpg.add_colormap(list(map(apply_tint_func(ROAD_COLOR), [[255, 255, 255], [255, 255, 255], [255, 255, 255]])), False, tag='roads_colormap', parent='colormap_registry')
+
+    with dpg.window(
+                label='edit map',
+                tag='map_window',
+                user_data=user_data,
+                horizontal_scrollbar=True,
+                show=False,
+                width=600,
+                height=400,
+                pos=[20, 60],
+                on_close=map_window_on_close_clb):
+        with dpg.menu_bar():
+            with dpg.menu(label='file'):
+                dpg.add_menu_item(label='open', callback=open_map_open_dialog_clb, tag='open_map_open_dialog')
+                dpg.add_menu_item(label='open layer', callback=open_map_open_layer_dialog_clb, tag='open_map_open_layer_dialog')
+                with dpg.menu(label='generate'):
+                    for lcz_type in LCZS:
+                        dpg.add_menu_item(label=lcz_type, check=False, callback=combo_menu(generate_map_clb), tag=lcz_type)
+                dpg.add_menu_item(label='save', callback=map_save_clb, tag='map_save')
+                dpg.add_menu_item(label='save as', callback=open_map_save_as_dialog_clb, tag='open_map_save_as_dialog')
+                dpg.add_menu_item(label='add map to config', tag='add_map_to_config')
+
+        with dpg.group(horizontal=True):
+            with dpg.group(width=200, tag='tools'):
+                dpg.add_radio_button(items=('buildings', 'trees', 'roads'), tag='layer', default_value='buildings', callback=lambda s, a, u: dpg.bind_colormap('height_input', f'{a}_colormap'))
+                dpg.add_button(label='erase', callback=set_action_clb, tag='erase')
+                dpg.add_button(label='add rectangle', callback=set_action_clb, tag='draw_rect')
+                dpg.add_colormap_slider(tag='height_input')
+                dpg.bind_colormap(dpg.last_item(), 'buildings_colormap')
+
+            with dpg.child_window(no_scrollbar=True, no_scroll_with_mouse=True, menubar=False, border=True, tag='map_child_window'):
+                dpg.add_child_window(tag='drawing_frame', show=False, width=0, height=0)
+
+    dpg.bind_item_theme('drawing_frame', item_theme)
+    dpg.bind_item_theme('map_child_window', 'map_image_theme')
+
+def construct_emission_point_settings_ui():
+    xoffset = 100
+    width = 300
+    with dpg.window(tag='emission_settings_window', show=False, autosize=True):
+        dpg.add_button(label='delete', tag='delete_emission_point', callback=delete_emission_point_clb)
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('value')
+            dpg.add_input_double(default_value=0, tag='emission_point_value', format='%.10f', callback=set_emission_point_value_clb)
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('begin')
+            dpg.add_input_double(default_value=0, tag='emission_point_begin', format='%.10f', callback=set_emission_point_value_clb)
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('xpos')
+            dpg.add_text('', tag='emission_point_xpos')
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('ypos')
+            dpg.add_text('', tag='emission_point_ypos')
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('zpos')
+            dpg.add_input_double(default_value=0, tag='emission_point_zpos', format='%.10f', callback=set_emission_point_value_clb)
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('sx')
+            dpg.add_input_double(default_value=0, tag='emission_point_sx', format='%.10f', callback=set_emission_point_value_clb)
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('sy')
+            dpg.add_input_double(default_value=0, tag='emission_point_sy', format='%.10f', callback=set_emission_point_value_clb)
+        with dpg.group(horizontal=True, xoffset=xoffset, width=width):
+            dpg.add_text('sz')
+            dpg.add_input_double(default_value=0, tag='emission_point_sz', format='%.10f', callback=set_emission_point_value_clb)
+
+def construct_ui():
+    construct_map_open_dialog()
+    construct_map_save_as_dialog()
+    construct_map_edit_window()
+    construct_emission_point_settings_ui()
+    construct_map_open_layer_dialog()
\ No newline at end of file
diff --git a/paths.py b/paths.py
new file mode 100644
index 0000000000000000000000000000000000000000..ebb9cf724d6f49b379435f20faafb17b32634fdc
--- /dev/null
+++ b/paths.py
@@ -0,0 +1,5 @@
+import os
+
+FONT_PATH = os.path.join('fonts', 'Montserrat-Medium.ttf')
+ICON_PATH = os.path.join('icons', 'icon.ico')
+CONFIG_PARSER_EXECUTABLE = "config-parser/parser-cli"
\ No newline at end of file
diff --git a/remote_run.py b/remote_run.py
index 6e49ed3bc00a3f8d5d76b4294f4643b9338ed72e..76a91d022e822996bfa11d12133dfb1263220430 100644
--- a/remote_run.py
+++ b/remote_run.py
@@ -1,158 +1,171 @@
-import threading
-import os
-import re
-import time
+from uuid import uuid4
 
+from paramiko import SSHClient, AutoAddPolicy
 from scp import SCPClient
-import dearpygui.dearpygui as dpg
-import paramiko
 
-from utils import show_status_text
+class Connection:
+    def __init__(self, host, username, password=None, id_rsa_path=None):
+        self.host = host
+        self.client = SSHClient()
+        self.transport = None
 
+        self.username = username
+        self.password = password
+        self.id_rsa_path = id_rsa_path
 
-HOST = 'geophyslab.srcc.msu.ru'
-URBAN_LES_REPOS = (
-    'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git',
-    'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git',
-    'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git'
-)
-
-def build_model(server_username, server_password, gitlab_username, gitlab_password, repos=URBAN_LES_REPOS):
-    try:
-        ssh = paramiko.SSHClient()
-        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-        ssh.connect(HOST, username=server_username, password=server_password)
-
-        commands = [
-            'mkdir run',
-            'cd ./run'
-        ]
-
-        for repo in repos:
-            commands.append(repo.format(username=gitlab_username, password=gitlab_password))
-        
-        commands += [
-            'cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/',
-            'chmod +x cpall.sh',
-            './cpall.sh ../../code',
-            'cd ../../code',
-            'make -B MACHINE=local COMPILER=gnu -j 23',
-        ]
-
-        commands = '; '.join(commands)
-
-        show_status_text(f'building model')
-        dpg.show_item('loading')
-        x, y = dpg.get_item_pos('status_text')
-        dpg.set_item_pos('loading', (x - 40, y))
-
-        stdin, stdout, stderr = ssh.exec_command(commands)
-        
-        with open('log.txt', 'w') as file:
-            file.write(stdout.read().decode())
-
-        ssh.close()
-
-        show_status_text('')
-        dpg.hide_item('loading')
-        
-        show_status_text('model builded:) log written to log.txt.')
+        self.connect()
     
-    except Exception as e:
-        show_status_text(f'error: {str(e)}')
-
-def make_run_command():
-    command = f'mpirun -np {int(dpg.get_value('-np'))} ./nsenx'
-    if dpg.get_value('-arch') != 'cpu':
-        command += f' -arch {dpg.get_value('-arch')}'
-    if dpg.get_value('use-udump'):
-        command += f' -udump {dpg.get_value('-udump')}'
-    if dpg.get_value('use-model-output'):
-        command += f' -model-output {dpg.get_value('-model-output')}'
-    return command
-
-def run_on_lab_server(username, password):
-    try:
-        show_status_text('model running...')
-        dpg.show_item('progress_bar')
-        ssh = paramiko.SSHClient()
-        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-        ssh.connect(HOST, username=username, password=password)
-        
-        scp = SCPClient(ssh.get_transport())
-        
-        scp.put('tmp.txt', 'run/code/config.txt')
-
-        command = make_run_command()
-
-        commands = [
-            'cd ./run',
-            'cd ./code',
-            command
-        ]
-
+    def connect(self):
+        try:
+            self.client.set_missing_host_key_policy(AutoAddPolicy())
+            if self.password is not None:
+                self.client.connect(self.host, username=self.username, password=self.password)
+            elif self.id_rsa_path is not None:
+                self.client.connect(self.host, username=self.username, key_filename=self.id_rsa_path)
+            else:
+                self.client.connect(self.host, username=self.username)
+        except Exception as e:
+            raise e
+
+    def exec_command(self, command):
+        try:
+            stdin, stdout, stderr = self.client.exec_command(command)
+            return stdout
+        except Exception:
+            try:
+                self.connect()
+                stdin, stdout, stderr = self.client.exec_command(command)
+                return stdout
+            except Exception as e:
+                raise e
+
+    def upload(self, local_filename, server_filename):
+        if not self.transport:
+            self.transport = SCPClient(self.client.get_transport())
+
+        self.transport.put(local_filename, server_filename)
+
+    def download(self, server_filename, local_filename):
+        if not self.transport:
+            self.transport = SCPClient(self.client.get_transport())
+
+        self.transport.get(server_filename, local_filename)
+
+    def close(self):
+        self.client.close()
+
+class BuildingSession:
+    def __init__(self, connection, build_commands, executable_file_path, gitlab_username=None, gitlab_password=None, uuid=None):
+        self.conn = connection
+        if uuid is not None:
+            self.uuid = uuid
+        else:
+            self.uuid = str(uuid4())
+        self.build_commands = build_commands
+        self.executable_file_path = f'{self.uuid}/{executable_file_path}'
+
+        commands = [f'mkdir {self.uuid}', f'cd {self.uuid}'] + self.build_commands
         commands = '; '.join(commands)
-
-        log = ''
-
-        stdin, stdout, stderr = ssh.exec_command(commands)
-
-        while not stdout.channel.exit_status_ready():
-            if stdout.channel.recv_ready():
-                output = stdout.channel.recv(512).decode()
-                update_progress(output)
-                log += '\n\n' + output
-            # time.sleep(1)
-
-        output = stdout.read().decode()
-        dpg.set_value('progress_bar', 1.)
-
-        log += '\n\n' + output
-        
-        with open('log.txt', 'w') as file:
-            file.write(log)
         
-        scp.close()
-        ssh.close()
-        
-        dpg.hide_item('progress_bar')
-        show_status_text('success:) log written to log.txt.')
-    
-    except Exception as e:
-        show_status_text(f'error: {str(e)}')
-
-def update_progress_building_model():
-    show_status_text(f'building model')
-    dpg.show_item('loading')
-    x, y = dpg.get_item_pos('status_text')
-    dpg.set_item_pos('loading', (x - 40, y))
-
-def update_progress(output):
-    if not re.findall(r'([0-9]+)%', output):
-        progress = 0.0
-    else:
-        progress = int(re.findall(r'([0-9]+)%', output)[-1]) / 100
-    dpg.set_value('progress_bar', progress)
-
-def start_remote_execution(server_username, server_password):
-    threading.Thread(target=run_on_lab_server, args=(server_username, server_password)).start()
-
-def start_model_build(server_username, server_password, gitlab_username, gitlab_password):
-    threading.Thread(target=build_model, args=(server_username, server_password, gitlab_username, gitlab_password)).start()
-
-def get_server_output_structure(username, password):
-    try:
-        ssh = paramiko.SSHClient()
-        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-        ssh.connect(HOST, username=username, password=password)
-
-        stdin, stdout, stderr = ssh.exec_command('cd ./run/code/output; find . -type f')
-
-        output = stdout.read().decode()
-        
-        ssh.close()
-
-        return output
-    
-    except Exception as e:
-        show_status_text(f'error: {str(e)}')
\ No newline at end of file
+        if gitlab_username and gitlab_password:
+            self.stdout = self.conn.exec_command(commands.format(username=gitlab_username, password=gitlab_password))
+        else:
+            self.stdout = self.conn.exec_command(commands)
+
+    def building(self):
+        return not self.stdout.channel.exit_status_ready()
+
+    def output(self):
+        if self.stdout.channel.recv_ready():
+            return self.stdout.channel.recv(512).decode()
+        return ''
+
+class RunningSession:
+    def __init__(self, connection, config_file_path, run_commands, executable_file_path, uuid=None):
+        self.conn = connection
+        if uuid is not None:
+            self.uuid = uuid
+        else:
+            self.uuid = str(uuid4())
+        self.config_file_path = config_file_path
+        self.run_commands = run_commands
+        self.executable_file_path = executable_file_path
+
+        self.conn.exec_command(f'mkdir {self.uuid}; cp {self.executable_file_path} {self.uuid}/nsenx')
+        self.conn.upload(self.config_file_path, f'{self.uuid}/config.txt')
+        commands = [f'cd {self.uuid}'] + self.run_commands
+
+        self.stdout = self.conn.exec_command('; '.join(commands))
+
+    def running(self):
+        return not self.stdout.channel.exit_status_ready()
+
+    def output(self):
+        if self.stdout.channel.recv_ready():
+            return self.stdout.channel.recv(512).decode()
+        return ''
+
+if __name__ == '__main__':
+    from dotenv import load_dotenv
+    import time
+    import os
+
+    load_dotenv()
+
+    gitlab_login = os.getenv('GITLAB_LOGIN', '')
+    gitlab_password = os.getenv('GITLAB_PASS', '')
+    server_login = os.getenv('SERVER_LOGIN', '')
+    server_password = os.getenv('SERVER_PASS', '')
+
+    BUILD_COMMANDS = [
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git',
+        'cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/',
+        'chmod +x cpall.sh',
+        './cpall.sh ../../code',
+        'cd ../../code',
+        'make -B MACHINE=local COMPILER=gnu -j 23'
+    ]
+
+    LOM_BUILD = [
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git',
+        'mkdir build',
+        'cd build',
+        'module load intel/2019.5 openmpi/4.0.1-icc cmake/3.19.8',
+        'cmake -DCMAKE_CONFIG_MODULE=lomonosov_pascal_ompi -DCOMPILER=intel -S ../nse-gabls1-urban-les/ -B ./',
+        'make'
+    ]
+
+    LAB_HOST = 'geophyslab.srcc.msu.ru'
+    LOMONOSOV_HOST = 'lomonosov2.parallel.ru'
+
+    conn = Connection(LOMONOSOV_HOST, 'gashchuk2011_2043', id_rsa_path='D:/lab/id_rsa')
+
+    building_session = BuildingSession(conn, LOM_BUILD, 'code/nsenx', gitlab_login, gitlab_password)
+
+    while building_session.building():
+        print(building_session.output())
+        time.sleep(2)
+
+    # conn = Connection(LAB_HOST, server_login, server_password)
+
+    # building_session = BuildingSession(conn, BUILD_COMMANDS, 'code/nsenx', gitlab_login, gitlab_password)
+
+    # print(building_session.uuid)
+
+    # while building_session.building():
+    #     print('building')
+    #     print(building_session.output())
+    #     time.sleep(2)
+
+    # running_session = RunningSession(conn, '../config-ex.txt', ['mpirun -np 8 ./nsenx'], '4e26cb1f-187b-4462-9794-5e8cc3844a1f/code/nsenx')#building_session.executable_file_path)
+
+    # print(running_session.uuid)
+
+    # while running_session.running():
+    #     print('running')
+    #     print(running_session.output())
+    #     time.sleep(2)
diff --git a/remote_run_callbacks.py b/remote_run_callbacks.py
new file mode 100644
index 0000000000000000000000000000000000000000..87cdd2cb97bf612d99efa5437d9f763e585b383e
--- /dev/null
+++ b/remote_run_callbacks.py
@@ -0,0 +1,250 @@
+import os
+import re
+import threading
+
+from socket import error
+from uuid import uuid4
+
+import dearpygui.dearpygui as dpg
+from paramiko.ssh_exception import BadHostKeyException, AuthenticationException, UnableToAuthenticate, NoValidConnectionsError, SSHException
+
+from utils import get_config_file_path, get_config, update_config_values, get_run_window_value, set_run_window_value
+from remote_run import Connection, BuildingSession, RunningSession
+from remote_run_constants import *
+from style_constants import FONT_SIZE
+from dynamic_ui import construct_model_output_structure_ui
+from config import dump_config
+from style import get_accent_color
+
+def update_run_model_window():
+    builds = get_run_window_value('builds')
+    connections = get_run_window_value('connections')
+
+    machine = LABEL_TO_MACHINE[dpg.get_value('machine')]
+    model = LABEL_TO_MODEL[dpg.get_value('model')]
+
+    if (machine, model) in builds:
+        dpg.hide_item('credentials')
+    elif machine in connections:
+        dpg.show_item('gitlab_group')
+        dpg.hide_item('lab_server_group')
+        dpg.hide_item('lomonosov_server_group')
+    else:
+        if machine == LAB:
+            dpg.show_item('gitlab_group')
+            dpg.show_item('lab_server_group')
+            dpg.hide_item('lomonosov_server_group')
+        elif machine == LOMONOSOV:
+            dpg.show_item('gitlab_group')
+            dpg.hide_item('lab_server_group')
+            dpg.show_item('lomonosov_server_group')
+
+def update_run_model_window_clb(s, a, u):
+    update_run_model_window()
+
+def show_run_model_window_clb(s, a, u):
+    if get_config_file_path():
+        update_config_values()
+        dump_config(get_config(), get_config_file_path())
+
+    dpg.set_value('config_file_path', get_config_file_path())
+
+    update_run_model_window()
+
+    dpg.show_item('run_model_window')
+
+def add_running_session_ui(running_session_id, machine, model):
+    with dpg.table_row(parent='models_table', tag=running_session_id):
+        dpg.add_text(MODEL_TO_LABEL[model], tag=f'{running_session_id}_model')
+        dpg.add_text(MACHINE_TO_LABEL[machine], tag=f'{running_session_id}_machine')
+        dpg.add_text('building', color=list(get_accent_color()[:3]) + [255], tag=f'{running_session_id}_status')
+        with dpg.group(horizontal=False):
+            dpg.add_spacer(height=0)
+            dpg.add_progress_bar(label='progress', default_value=0.0, tag=f'{running_session_id}_progress_bar', height=FONT_SIZE, width=150)
+        dpg.add_button(label='download output', enabled=False, tag=f'{running_session_id}_download_button', callback=show_download_output_window_clb, user_data=running_session_id)
+    dpg.bind_item_theme(f'{running_session_id}_progress_bar', 'progress_bar_theme')
+
+def make_execute_command():
+    np = int(dpg.get_value('-np'))
+    arch = dpg.get_value('-arch')
+    udump = dpg.get_value('-udump')
+    model_output = dpg.get_value('-model-output')
+
+    command = f'mpirun -np {np} ./nsenx -arch {arch}'
+    if dpg.get_value('use-udump'):
+        command += f' -udump {dpg.get_value('-udump')}'
+    if dpg.get_value('use-model-output'):
+        command += f' -model-output {dpg.get_value('-model-output')}'
+
+    return command
+
+def update_progress(running_session_id, output):
+    if not re.findall(r'([0-9]+)%', output):
+        progress = dpg.get_value(f'{running_session_id}_progress_bar')
+    else:
+        progress = int(re.findall(r'([0-9]+)%', output)[-1]) / 100
+    dpg.set_value(f'{running_session_id}_progress_bar', progress)
+
+def run_model():
+    dpg.hide_item('run_model_window')
+
+    config_file_path = dpg.get_value('config_file_path')
+
+    if not os.path.exists(config_file_path):
+        show_message(f'no {config_file_path} file :(')
+        return
+
+    builds = get_run_window_value('builds')
+    connections = get_run_window_value('connections')
+
+    machine = LABEL_TO_MACHINE[dpg.get_value('machine')]
+    model = LABEL_TO_MODEL[dpg.get_value('model')]
+
+    if machine not in connections:
+        if machine == LAB:
+            username = dpg.get_value('server_username')
+            password = dpg.get_value('server_password')
+        elif machine == LOMONOSOV:
+            username = dpg.get_value('lomonosov_username')
+            if 'ID_RSA_PATH' in os.environ:
+                id_rsa_path = os.environ['ID_RSA_PATH']
+            else:
+                id_rsa_path = None
+        try:
+            if machine == LAB:
+                conn = Connection(MACHINE_TO_HOST[machine], username, password)
+            elif machine == LOMONOSOV:
+                if id_rsa_path:
+                    conn = Connection(MACHINE_TO_HOST[machine], username, id_rsa_path=id_rsa_path)
+                else:
+                    conn = Connection(MACHINE_TO_HOST[machine], username)
+        except BadHostKeyException:
+            show_message('server’s host key could not be verified\ntry again')
+            return
+        except AuthenticationException:
+            show_message('wrong username or password')
+            return
+        except error:
+            show_message('check your internet connection :)')
+            return
+        except NoValidConnectionsError:
+            show_message('something wrong with the server ¯\\_(ツ)_/¯')
+            return
+        except SSHException:
+            show_message('i have no idea what happend\nplease contact @maryshca')
+            return
+        connections[machine] = conn
+        set_run_window_value('connections', connections)
+    else:
+        conn = connections[machine]
+
+    running_session_id = str(uuid4())
+
+    if machine == LOMONOSOV:
+        running_session_id = '~/_scratch/' + running_session_id
+
+    add_running_session_ui(running_session_id, machine, model)
+
+    if (machine, model) not in builds:
+        dpg.hide_item(f'{running_session_id}_progress_bar')
+
+        gitlab_username = dpg.get_value('gitlab_username')
+        gitlab_password = dpg.get_value('gitlab_password')
+        build_commands = BUILD_COMMANDS[(machine, model)]
+        exec_file_path = EXECUTABLE_FILE_PATH[(machine, model)]
+        building_session = BuildingSession(conn, build_commands, exec_file_path, gitlab_username, gitlab_password)
+
+        while building_session.building():
+            output = building_session.output()
+            update_progress(running_session_id, output)
+
+        executable_file_path = building_session.executable_file_path
+
+        builds[(machine, model)] = executable_file_path
+        set_run_window_value('builds', builds)
+
+        dpg.show_item(f'{running_session_id}_progress_bar')
+    else:
+        executable_file_path = builds[(machine, model)]
+
+    dpg.set_value(f'{running_session_id}_status', 'running')
+
+    if machine == LOMONOSOV:
+        run_commands = RUN_COMMANDS[(machine, model)] # + make_run_sh_for_slurm() + ['sbatch ./run.sh']
+    else:
+        run_commands = RUN_COMMANDS[(machine, model)] + [make_execute_command()]
+
+    running_session = RunningSession(conn, config_file_path, run_commands, executable_file_path, running_session_id)
+
+    if machine == LOMONOSOV:
+        output = running_session.output()
+        sbatch_id = re.findall(r'([0-1]+)', output)[0]
+        return
+
+    while running_session.running():
+        output = running_session.output()
+        update_progress(running_session_id, output)
+
+    dpg.set_value(f'{running_session_id}_status', 'successful run')
+    dpg.set_value(f'{running_session_id}_progress_bar', 1.0)
+    dpg.enable_item(f'{running_session_id}_download_button')
+
+def run_model_clb():
+    threading.Thread(target=run_model).start()
+
+def show_download_output_window_clb(s, a, running_session_id):
+    delete_item_children_and_clear_aliases('output_header')
+
+    machine = LABEL_TO_MACHINE[dpg.get_value(f'{running_session_id}_machine')]
+
+    conn = get_run_window_value('connections')[machine]
+    stdout = conn.exec_command(f'cd ./{running_session_id}/output; find . -type f')
+    output = stdout.read().decode()
+    dirs, filepaths = parse_dirs(output)
+
+    construct_model_output_structure_ui(filepaths, dirs)
+
+    dpg.set_item_user_data('model_output_window', (dirs, filepaths, running_session_id))
+
+    dpg.show_item('model_output_window')
+
+def open_download_to_folder_dialog_clb():
+    dpg.show_item('download_to_folder_dialog')
+
+def open_config_file_path_dialog_clb():
+    dpg.show_item('config_file_path_dialog')
+
+def set_download_output_to_folder_clb(s, a, u):
+    dpg.set_value('download_output_to_folder', a['file_path_name'])
+
+def set_config_file_path_clb(s, a, u):
+    dpg.set_value('config_file_path', a['file_path_name'])
+
+def download_output():
+    dpg.hide_item('model_output_window')
+    dirs, filepaths, running_session_id = dpg.get_item_user_data('model_output_window')
+
+    download = []
+    for filepath in filepaths:
+        if dpg.get_value(filepath):
+            download.append(filepath)
+
+    machine = LABEL_TO_MACHINE[dpg.get_value(f'{running_session_id}_machine')]
+
+    download_to_folder = dpg.get_value('download_output_to_folder')
+
+    dpg.set_value(f'{running_session_id}_status', 'downloading')
+
+    prefix = os.path.join(download_to_folder)
+        
+    construct_folder_structure(dirs, prefix=prefix)
+
+    conn = get_run_window_value('connections')[machine]
+    
+    for filepath in download:
+        conn.download(f'{running_session_id}/output/{filepath}', os.path.join(prefix, filepath))
+
+    dpg.set_value(f'{running_session_id}_status', 'downloaded')
+
+def download_output_clb(s, a, u):
+    threading.Thread(target=download_output).start()
\ No newline at end of file
diff --git a/remote_run_constants.py b/remote_run_constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..a52caec7cc4e9864a9d1e27bfbd9456592e8720e
--- /dev/null
+++ b/remote_run_constants.py
@@ -0,0 +1,69 @@
+LAB = 'geophyslab.srcc.msu.ru'
+LOMONOSOV = 'lomonosov2.parallel.ru'
+
+LAB_HOST = 'geophyslab.srcc.msu.ru'
+LOMONOSOV_HOST = 'lomonosov2.parallel.ru'
+
+MACHINES = [LAB, LOMONOSOV]
+
+MACHINE_TO_HOST = {
+    LAB: LAB_HOST,
+    LOMONOSOV: LOMONOSOV_HOST
+}
+
+URBAN_LES = 'urban-les'
+
+MODELS = [URBAN_LES]
+
+BUILD_COMMANDS = {
+    (LAB, URBAN_LES): [
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git',
+        'cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/',
+        'chmod +x cpall.sh',
+        './cpall.sh ../../code',
+        'cd ../../code',
+        'make -B MACHINE=local COMPILER=gnu -j 23'
+    ],
+    (LOMONOSOV, URBAN_LES): [
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git',
+        'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git',
+        'mkdir build',
+        'cd build',
+        'module load intel/2019.5 openmpi/4.0.1-icc cmake/3.19.8',
+        'cmake -DCMAKE_CONFIG_MODULE=lomonosov_pascal_ompi -DCOMPILER=intel -S ../nse-gabls1-urban-les/ -B ./',
+        'make'
+    ],
+}
+
+EXECUTABLE_FILE_PATH = {
+    (LAB, URBAN_LES): 'code/nsenx',
+    (LOMONOSOV, URBAN_LES): 'build/nse-gabls1-urban-les'
+}
+
+RUN_COMMANDS = {
+    (LAB, URBAN_LES): [],
+    (LOMONOSOV, URBAN_LES): [
+        'module load intel/2019.5 openmpi/4.0.1-icc slurm',
+        'sbatch -p compute -n 1 ompi ./nsenx'
+    ]
+}
+
+RUN_SH_LINES = ['#!/bin/sh',
+    '#SBATCH --job-name=nsenx',
+    '#SBATCH -p pascal',
+    '#SBATCH --ntasks=8',
+    '## -- setting CPUs per task [skipped]',
+    '##SBATCH --cpus-per-task=1',
+    '#SBATCH --time=01:00:00',
+    '## --- setting number of OMP threads [skipped]',
+    '#export OMP_NUM_THREADS=\\$SLURM_CPUS_PER_TASK',
+]
+
+LABEL_TO_MACHINE = {'lab server': LAB, 'lomonosov 2': LOMONOSOV}
+LABEL_TO_MODEL = {'urban les': URBAN_LES}
+
+MACHINE_TO_LABEL = {LAB: 'lab server', LOMONOSOV: 'lomonosov 2'}
+MODEL_TO_LABEL = {URBAN_LES: 'urban les'}
\ No newline at end of file
diff --git a/remote_run_ui.py b/remote_run_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb4123a8015cfad724fef67716999a8a4b0ac7ca
--- /dev/null
+++ b/remote_run_ui.py
@@ -0,0 +1,109 @@
+import dearpygui.dearpygui as dpg
+
+import os
+
+from remote_run_constants import *
+from remote_run_callbacks import *
+
+def construct_remote_run_window():
+    user_data = {
+        'connections': dict(),
+        'builds': dict()
+    }
+
+    with dpg.window(
+                label='remote run',
+                tag='run_window',
+                horizontal_scrollbar=True,
+                show=False,
+                width=1000,
+                height=600,
+                pos=[20, 60],
+                user_data=user_data):
+        with dpg.menu_bar():
+            dpg.add_menu_item(label='run', callback=show_run_model_window_clb, tag='run')
+
+        with dpg.table(header_row=True, row_background=False, borders_outerH=True, borders_innerH=True, borders_innerV=False, borders_outerV=False, delay_search=True, tag='models_table'):
+            dpg.add_table_column(label='Type')
+            dpg.add_table_column(label='Running on')
+            dpg.add_table_column(label='Status')
+            dpg.add_table_column(label='Progress')
+            dpg.add_table_column(label='')
+
+    with dpg.theme(tag='progress_bar_theme'):
+        with dpg.theme_component(dpg.mvAll):
+            dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (255, 255, 255, 100), category=dpg.mvThemeCat_Core)
+
+def construct_run_model_window():
+    xoffset = 400
+    width = 300
+    with dpg.window(label='remote run settings', show=False, tag='run_model_window', autosize=True, pos=[50, 50]):
+        with dpg.group(horizontal=True, xoffset=xoffset, tag='machines_group'):
+            dpg.add_text('choose machine')
+            dpg.add_radio_button(items=('lab server', 'lomonosov 2'), default_value='lab server', callback=update_run_model_window_clb, tag='machine', horizontal=True)
+        with dpg.group(horizontal=True, xoffset=xoffset, tag='models_group'):
+            dpg.add_text('choose model')
+            dpg.add_radio_button(items=('urban les',), callback=update_run_model_window_clb, default_value='urban les', tag='model', horizontal=True)
+
+        with dpg.child_window(label='credentials', tag='credentials', auto_resize_y=True):
+            with dpg.group(tag='gitlab_group'):
+                dpg.add_input_text(label='gitlab username', tag='gitlab_username', password=False, default_value=os.getenv('GITLAB_LOGIN', ''), width=width)
+                dpg.add_input_text(label='gitlab password', tag='gitlab_password', password=True, default_value=os.getenv('GITLAB_PASS', ''), width=width)
+
+            with dpg.group(tag='lab_server_group'):
+                dpg.add_input_text(label='server username', tag='server_username', password=False, default_value=os.getenv('SERVER_LOGIN', ''), width=width)
+                dpg.add_input_text(label='server password', tag='server_password', password=True, default_value=os.getenv('SERVER_PASS', ''), width=width)
+
+            with dpg.group(tag='lomonosov_server_group'):
+                dpg.add_input_text(label='lomonosov username', tag='lomonosov_username', default_value=os.getenv('LOMONOSOV_LOGIN', ''), width=width)
+
+        with dpg.group(horizontal=True, xoffset=xoffset, tag='config_file_path_group'):
+            dpg.add_text('config file path')
+            with dpg.group(horizontal=True):
+                dpg.add_input_text(default_value='', tag='config_file_path', width=width)
+                dpg.add_button(label='...', callback=open_config_file_path_dialog_clb)
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('number of processes for mpi run')
+            dpg.add_input_int(default_value=8, width=width, min_value=1, max_value=200, min_clamped=True, max_clamped=True, tag='-np')
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('arch')
+            dpg.add_radio_button(items=('gpu', 'cpu', 'mix'), label='arch', default_value='cpu', tag='-arch', horizontal=True)
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('use dump')
+            dpg.add_checkbox(default_value=False, tag='use-udump', callback=lambda s, a, u: dpg.show_item('-udump-group') if a else dpg.hide_item('-udump-group'))
+        with dpg.group(horizontal=True, xoffset=xoffset, tag='-udump-group', show=False):
+            dpg.add_text('dump from this control points')
+            dpg.add_input_int(default_value=1, width=width, min_value=1, max_value=200, min_clamped=True, max_clamped=True, tag='-udump', enabled=False)
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('write model stdout to file')
+            dpg.add_checkbox(default_value=False, tag='use-model-stdout', callback=lambda s, a, u: dpg.show_item('-model-stdout-group') if a else dpg.hide_item('-model-stdout-group'))
+        with dpg.group(horizontal=True, xoffset=xoffset, tag='-model-stdout-group', show=False):
+            dpg.add_text('filename')
+            dpg.add_input_text(default_value='log.txt', tag='-model-stdout', width=width, enabled=False)
+
+        dpg.add_button(label='run', callback=run_model_clb)
+
+def construct_select_download_folder_dialog():
+    with dpg.file_dialog(directory_selector=True, modal=True, show=False, callback=set_download_output_to_folder_clb, tag='download_to_folder_dialog', width=500, height=400):
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_select_config_file_path_dialog():
+    with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=set_config_file_path_clb, tag='config_file_path_dialog', width=500, height=400):
+        dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_model_output_window():
+    with dpg.window(label='select model output to download', show=False, tag='model_output_window', width=600, height=600, pos=[50, 50]):
+        dpg.add_child_window(height=500, tag='model_output_child_window')
+        with dpg.group(horizontal=True):
+            dpg.add_text('download to')
+            dpg.add_input_text(default_value='output', tag='download_output_to_folder')
+            dpg.add_button(label='...', callback=open_download_to_folder_dialog_clb)
+        dpg.add_button(label='download selected files', callback=download_output_clb)
+
+def construct_ui():
+    construct_remote_run_window()
+    construct_run_model_window()
+    construct_model_output_window()
+    construct_select_download_folder_dialog()
+    construct_select_config_file_path_dialog()
\ No newline at end of file
diff --git a/static_ui.py b/static_ui.py
index 8704c2fc9006977b5973e33f5c6a8b1d530c57bc..e06c8470983a22ea5d0a9c5976627294ce5e3f6b 100644
--- a/static_ui.py
+++ b/static_ui.py
@@ -28,9 +28,14 @@ def construct_main_window_ui():
 
             with dpg.menu(label='run'):
                 dpg.add_menu_item(label='edit remote run default values', callback=lambda: dpg.show_item('remote_run_settings_window'))
-                dpg.add_menu_item(label='build model on lab server', callback=show_get_credentials_modal_clb, user_data='build_model')
-                dpg.add_menu_item(label='run on lab server', callback=show_get_credentials_modal_clb, user_data='run_on_lab_server')
-                dpg.add_menu_item(label='download output', callback=show_get_credentials_modal_clb, user_data='download_output')
+                with dpg.menu(label='lab server'):
+                    dpg.add_menu_item(label='build model', callback=show_get_credentials_modal_clb, user_data='build_model_lab_server', tag='build_model_lab_server')
+                    dpg.add_menu_item(label='execute model', callback=show_get_credentials_modal_clb, enabled=False, user_data='execute_model_lab_server', tag='execute_model_lab_server')
+                    dpg.add_menu_item(label='download output', callback=show_get_credentials_modal_clb, enabled=False, user_data='download_output_lab_server', tag='download_output_lab_server')
+                with dpg.menu(label='lomonosov cluster'):
+                    dpg.add_menu_item(label='build model', callback=show_get_credentials_modal_clb, user_data='build_model_lomonosov_cluster', tag='build_model_lomonosov_cluster')
+                    dpg.add_menu_item(label='execute model', callback=show_get_credentials_modal_clb, enabled=False, user_data='execute_model_lomonosov_cluster', tag='execute_model_lomonosov_cluster')
+                    dpg.add_menu_item(label='download output', callback=show_get_credentials_modal_clb, enabled=False, user_data='download_output_lomonosov_cluster', tag='download_output_lomonosov_cluster')
 
             with dpg.menu(label='view'):
                 with dpg.menu(label='ui', tag='ui'):
diff --git a/style.py b/style.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5862bbaa0785bab6e9664540a52c6ef3ee1cddc
--- /dev/null
+++ b/style.py
@@ -0,0 +1,154 @@
+import os
+
+import dearpygui.dearpygui as dpg
+
+from style_constants import *
+from paths import FONT_PATH
+
+def apply_alpha(color, divisor=2):
+    r, g, b, a = color
+    return (r, g, b, a / divisor)
+
+def add_colors_to_theme(colors, theme):
+    APPLY_ALPHA = (
+        dpg.mvThemeCol_ButtonHovered,
+        dpg.mvThemeCol_HeaderHovered,
+        dpg.mvThemeCol_ResizeGripHovered,
+        dpg.mvThemeCol_Tab,
+        dpg.mvThemeCol_ScrollbarGrabHovered
+    )
+    
+    COLOR_TYPE_TO_DPG_ITEM = {
+        'PRIMARY' : (
+            dpg.mvThemeCol_WindowBg,
+            dpg.mvThemeCol_ChildBg,
+            dpg.mvThemeCol_TableHeaderBg,
+            dpg.mvThemeCol_TableBorderLight,
+            dpg.mvThemeCol_TableRowBg,
+            dpg.mvThemeCol_PopupBg
+        ),
+        'SECONDARY' : (
+            dpg.mvThemeCol_TabHovered,
+            dpg.mvThemeCol_TabUnfocusedActive,
+            dpg.mvThemeCol_Button,
+            dpg.mvThemeCol_Header,
+            dpg.mvThemeCol_FrameBg,
+            dpg.mvThemeCol_FrameBgHovered,
+            dpg.mvThemeCol_FrameBgActive,
+            dpg.mvThemeCol_MenuBarBg,
+            dpg.mvThemeCol_ScrollbarBg,
+            dpg.mvThemeCol_TableRowBgAlt
+        ),
+        'ACCENT' : (
+            dpg.mvThemeCol_TableBorderStrong,
+            dpg.mvThemeCol_Border,
+            dpg.mvThemeCol_CheckMark,
+            dpg.mvThemeCol_SliderGrab,
+            dpg.mvThemeCol_SliderGrabActive,
+            dpg.mvThemeCol_ScrollbarGrab,
+            dpg.mvThemeCol_ScrollbarGrabHovered,
+            dpg.mvThemeCol_TabActive,
+            dpg.mvThemeCol_TabUnfocused,
+            dpg.mvThemeCol_Tab,
+            dpg.mvThemeCol_ButtonActive,
+            dpg.mvThemeCol_ButtonHovered,
+            dpg.mvThemeCol_HeaderHovered,
+            dpg.mvThemeCol_HeaderActive,
+            dpg.mvThemeCol_Separator,
+            dpg.mvThemeCol_SeparatorHovered,
+            dpg.mvThemeCol_SeparatorActive,
+            dpg.mvThemeCol_TitleBg,
+            dpg.mvThemeCol_TitleBgActive,
+            dpg.mvThemeCol_TitleBgCollapsed,
+            dpg.mvThemeCol_ResizeGrip,
+            dpg.mvThemeCol_ResizeGripHovered,
+            dpg.mvThemeCol_ResizeGripActive,
+            dpg.mvThemeCol_TextSelectedBg
+        )
+    }
+
+    with dpg.theme_component(dpg.mvAll, parent=theme):
+        for color_type in COLOR_TYPE_TO_DPG_ITEM:
+            for item in COLOR_TYPE_TO_DPG_ITEM[color_type]:
+                color = colors['enabled'][color_type]
+                if item in APPLY_ALPHA:
+                    color = apply_alpha(color)
+                dpg.add_theme_color(item, color)
+    with dpg.theme_component(dpg.mvMenuItem, parent=theme, enabled_state=False):
+        for color_type in COLOR_TYPE_TO_DPG_ITEM:
+            for item in COLOR_TYPE_TO_DPG_ITEM[color_type]:
+                color = colors['disabled'][color_type]
+                if item in APPLY_ALPHA:
+                    color = apply_alpha(color)
+                dpg.add_theme_color(item, color)
+    with dpg.theme_component(dpg.mvButton, parent=theme, enabled_state=False):
+        for color_type in COLOR_TYPE_TO_DPG_ITEM:
+            for item in COLOR_TYPE_TO_DPG_ITEM[color_type]:
+                color = colors['disabled'][color_type]
+                if item in APPLY_ALPHA:
+                    color = apply_alpha(color)
+                dpg.add_theme_color(item, color)
+        dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, colors['disabled']['SECONDARY'])
+
+def add_styles_to_theme(theme):
+    rounding = (
+        dpg.mvStyleVar_ChildRounding,
+        dpg.mvStyleVar_WindowRounding,
+        dpg.mvStyleVar_FrameRounding,
+        dpg.mvStyleVar_TabRounding,
+        dpg.mvStyleVar_GrabRounding,
+        dpg.mvStyleVar_ScrollbarRounding,
+        dpg.mvStyleVar_PopupRounding
+    )
+
+    spacing = (
+        dpg.mvStyleVar_ItemSpacing,
+        dpg.mvStyleVar_ItemInnerSpacing,
+        dpg.mvStyleVar_WindowPadding,
+        dpg.mvStyleVar_FramePadding,
+        dpg.mvStyleVar_CellPadding
+    )
+
+    with dpg.theme_component(dpg.mvAll, parent=theme):
+        for item in rounding:
+            dpg.add_theme_style(item, ROUNDING, category=dpg.mvThemeCat_Core)
+        
+        for item in spacing:
+            dpg.add_theme_style(item, SPACE, SPACE, category=dpg.mvThemeCat_Core)
+
+        dpg.add_theme_style(dpg.mvStyleVar_ChildBorderSize, BORDER, category=dpg.mvThemeCat_Core)
+        dpg.add_theme_style(dpg.mvStyleVar_WindowBorderSize, BORDER, category=dpg.mvThemeCat_Core)
+        dpg.add_theme_style(dpg.mvStyleVar_FrameBorderSize, 0, category=dpg.mvThemeCat_Core)
+
+
+def setup_fonts():
+    cyrillic_capitals_start = 0x00C0
+    cyrillic_capitals_end = 0x00DF
+    cyrillic_smalls_end = 0x00FF
+    unicode_cyrillic_capital_A = 0x0410
+    alphabet_length = cyrillic_capitals_start - cyrillic_capitals_end + 1
+    with dpg.font_registry():
+        with dpg.font(FONT_PATH, FONT_SIZE, default_font=True) as montserrat_font:
+            dpg.add_font_range_hint(dpg.mvFontRangeHint_Default)
+            dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic)
+            for i in range(alphabet_length):
+                dpg.add_char_remap(cyrillic_capitals_start + i, unicode_cyrillic_capital_A + i)
+                dpg.add_char_remap(cyrillic_capitals_start + i + alphabet_length, unicode_cyrillic_capital_A + i + alphabet_length)
+    dpg.bind_font(montserrat_font)
+
+def apply_theme(theme_name):
+    theme = dpg.add_theme()
+    add_styles_to_theme(theme)
+    add_colors_to_theme(THEMES[theme_name], theme)
+    dpg.bind_theme(theme)
+
+def get_current_theme():
+    for child in dpg.get_item_children('theme', 1):
+        if dpg.get_value(child):
+            return dpg.get_item_alias(child)
+
+def setup_themes():
+    apply_theme(get_current_theme())
+
+def get_accent_color():
+    return THEMES[get_current_theme()]['enabled']['ACCENT']
\ No newline at end of file
diff --git a/style_constants.py b/style_constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..f587445319f3b5cf4efbb41ba02ea90cde6650c6
--- /dev/null
+++ b/style_constants.py
@@ -0,0 +1,48 @@
+BLUE = {
+    'enabled': {
+        'TEXT': (255, 255, 255, 255),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (0, 119, 200, 153)
+    },
+    'disabled': {
+        'TEXT': (255, 255, 255, 150),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (0, 119, 200, 153)
+    },
+}
+
+ORANGE = {
+    'enabled': {
+        'TEXT': (255, 255, 255, 255),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (251, 133, 0, 153)
+    },
+    'disabled': {
+        'TEXT': (255, 255, 255, 150),
+        'PRIMARY': (37, 37, 38, 255),
+        'SECONDARY': (51, 51, 55, 255),
+        'ACCENT': (251, 133, 0, 153)
+    },
+}
+
+THEMES = {
+    'blue': BLUE,
+    'orange': ORANGE
+}
+
+FONT_SIZE = 24
+ACCENT_COLOR = (0, 119, 200, 100)
+ROUNDING = 10
+SPACE = 10
+INDENT = 40
+BORDER = 3
+
+TABS_UI = 'tabs_ui'
+COLLAPSING_HEADERS_UI = 'collapsing_headers_ui'
+WIDGET_WIDTH = 300
+XOFFSET = 300
+LCZS = ['compact_high_rise', 'compact_low_rise', 'compact_mid_rise', 'heavy_industry', 'large_low_rise', 'lightweight_low_rise', 'open_high_rise', 'open_low_rise', 'open_mid_rise', 'sparsley_build']
+UIS = [TABS_UI, COLLAPSING_HEADERS_UI]
\ No newline at end of file
diff --git a/styles.py b/styles.py
index d8a8bbd6f4b72a30da77a2218254c5c2a2bab05b..dd72209443adbf3bb5aef9483124548a930d2137 100644
--- a/styles.py
+++ b/styles.py
@@ -15,10 +15,17 @@ def add_colors_to_theme(colors, theme):
     with dpg.theme_component(dpg.mvAll, parent=theme):
         for color_type in COLOR_TYPE_TO_DPG_ITEM:
             for item in COLOR_TYPE_TO_DPG_ITEM[color_type]:
-                color = colors[color_type]
+                color = colors['enabled'][color_type]
                 if item in APPLY_ALPHA:
                     color = apply_alpha(color)
-                dpg.add_theme_color(item, color, category=dpg.mvThemeCat_Core)
+                dpg.add_theme_color(item, color)
+    with dpg.theme_component(dpg.mvMenuItem, parent=theme, enabled_state=False):
+        for color_type in COLOR_TYPE_TO_DPG_ITEM:
+            for item in COLOR_TYPE_TO_DPG_ITEM[color_type]:
+                color = colors['disabled'][color_type]
+                if item in APPLY_ALPHA:
+                    color = apply_alpha(color)
+                dpg.add_theme_color(item, color)
 
 def add_styles_to_theme(theme):
     rounding = (
diff --git a/utils.py b/utils.py
index baf43fab13fb50b3590883d79356e95daa513e93..8b8b286b718c75f372f4cc303a193984e9699cf4 100644
--- a/utils.py
+++ b/utils.py
@@ -1,13 +1,26 @@
 import re
 import threading
+import os
 
 import numpy as np
 import dearpygui.dearpygui as dpg
 
-from constants import *
+from style_constants import SPACE
+
+def construct_folder_structure(folder_paths, prefix):
+    for folder_path in folder_paths:
+        full_path = os.path.join(prefix, folder_path)
+        os.makedirs(full_path, exist_ok=True)
+
+def update_config_values():
+    config = get_config()
+    for path in config:
+        if dpg.does_item_exist(f'{get_config_file_path()}.{path}'):
+            config[path].value = dpg.get_value(f'{get_config_file_path()}.{path}')
+    set_main_window_value('config', config)
 
 def parse_dirs(s):
-    filepaths = set(map(lambda x: x[2:], s.strip('\n').split('\n')[1:]))
+    filepaths = set(map(lambda x: x[2:], s.strip('\n').split('\n')))
     dirs = set()
     for path in filepaths:
         lst = path.split('/')
@@ -41,6 +54,14 @@ def set_main_window_value(key, value):
 def get_main_window_value(key):
     return dpg.get_item_user_data('main').get(key, None)
 
+def set_run_window_value(key, value):
+    data = dpg.get_item_user_data('run_window')
+    data[key] = value
+    dpg.set_item_user_data('run_window', data)
+
+def get_run_window_value(key):
+    return dpg.get_item_user_data('run_window').get(key, None)
+
 def get_height_map_file_path():
     return dpg.get_item_user_data('map_window')['height_map_file_path']
 
@@ -61,9 +82,14 @@ def set_map_window_value(key, value):
 def get_map_window_value(key):
     return dpg.get_item_user_data('map_window').get(key, None)
 
-def show_status_text(message):
-    dpg.set_item_pos('status_text', (dpg.get_item_width('main') - dpg.get_text_size(message)[0] - 2 * SPACE, dpg.get_item_pos('status_text')[1]))
-    dpg.set_value('status_text', message)
+def show_message(message):
+    dpg.set_value('message', message)
+    dpg.show_item('message_window')
+
+def show_loading_indicator():
+    dpg.show_item('loading')
+    x, y = dpg.get_item_pos('status_text')
+    dpg.set_item_pos('loading', (x - 40, y))
 
 def get_emission_points():
     points = dict()