diff --git a/build/linux/config.json b/build/linux/config.json new file mode 100644 index 0000000..ce4c12a --- /dev/null +++ b/build/linux/config.json @@ -0,0 +1,20 @@ +{ + "favorites": [ + "/home/brasse", + "/home/brasse/test", + "/home" + ], + "text_editor": "msedit", + "custom_menu_items": [ + { + "name": "Open in Vs Code", + "command": "code $[path]", + "applies_to": "Both" + }, + { + "name": "Nytt kommando", + "command": "echo $[path]", + "applies_to": "Both" + } + ] +} \ No newline at end of file diff --git a/build/linux/tui-fm b/build/linux/tui-fm index 203ffdb..97a6cd3 100755 Binary files a/build/linux/tui-fm and b/build/linux/tui-fm differ diff --git a/src/app.rs b/src/app.rs index f2c4dba..c556419 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::config::Config; +use crate::config::{AppliesTo, Config, CustomMenuItem}; use crate::file_ops::{self, FileEntry}; #[derive(Debug, Clone, PartialEq)] @@ -36,6 +36,7 @@ pub enum ContextMenuItem { RemoveFavorite(String), Separator, NewSubmenu, + Custom(usize), // index into config.custom_menu_items } #[derive(Debug, Clone)] @@ -57,6 +58,7 @@ pub enum DialogMode { Properties(PropertiesInfo), Error(String), Confirm(String, ConfirmAction), + Settings, } #[derive(Debug, Clone, PartialEq)] @@ -94,6 +96,19 @@ pub struct App { pub last_click_entry: Option<(usize, std::time::Instant)>, pub sidebar_selected: Option, pub rename_original: Option, + // Settings editor state + pub settings_selected_row: usize, // which row is highlighted in settings + pub settings_editing_field: Option, // which field is being edited + pub settings_editing_buf: String, // current text in the edited field + pub settings_custom_selected: usize, // which custom item row is selected +} + +// Which field inside the settings editor is being edited +#[derive(Debug, Clone, PartialEq)] +pub enum SettingsField { + TextEditor, + CustomName(usize), + CustomCommand(usize), } impl App { @@ -122,6 +137,10 @@ impl App { last_click_entry: None, sidebar_selected: None, rename_original: None, + settings_selected_row: 0, + settings_editing_field: None, + settings_editing_buf: String::new(), + settings_custom_selected: 0, } } @@ -365,6 +384,14 @@ impl App { let has_clipboard = self.clipboard.is_some(); let can_paste = has_clipboard && (self.selected_target_dir().is_some() || true); + // Determine if selected items are files/dirs for custom item filtering + let selected_is_dir = self.selected_indices.iter().all(|&i| { + self.entries.get(i).map(|e| e.is_dir()).unwrap_or(false) + }); + let selected_is_file = self.selected_indices.iter().all(|&i| { + self.entries.get(i).map(|e| !e.is_dir()).unwrap_or(false) + }); + let mut items = Vec::new(); if is_sidebar_fav { @@ -386,6 +413,26 @@ impl App { items.push(ContextMenuItem::Separator); items.push(ContextMenuItem::Delete); } + // Custom items + let custom_applicable: Vec = self.config.custom_menu_items + .iter() + .enumerate() + .filter(|(_, ci)| { + if !has_selection { return matches!(ci.applies_to, AppliesTo::Both); } + match ci.applies_to { + AppliesTo::File => selected_is_file, + AppliesTo::Dir => selected_is_dir, + AppliesTo::Both => true, + } + }) + .map(|(i, _)| i) + .collect(); + if !custom_applicable.is_empty() { + items.push(ContextMenuItem::Separator); + for i in custom_applicable { + items.push(ContextMenuItem::Custom(i)); + } + } items.push(ContextMenuItem::Separator); items.push(ContextMenuItem::NewSubmenu); items.push(ContextMenuItem::AddFavorite); diff --git a/src/config.rs b/src/config.rs index eaa8ca2..9cede57 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,59 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum AppliesTo { + File, + Dir, + Both, +} + +impl AppliesTo { + pub fn label(&self) -> &'static str { + match self { + AppliesTo::File => "Fil", + AppliesTo::Dir => "Mapp", + AppliesTo::Both => "Båda", + } + } + pub fn cycle(&self) -> Self { + match self { + AppliesTo::File => AppliesTo::Dir, + AppliesTo::Dir => AppliesTo::Both, + AppliesTo::Both => AppliesTo::File, + } + } +} + +impl Default for AppliesTo { + fn default() -> Self { AppliesTo::Both } +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct CustomMenuItem { + pub name: String, + pub command: String, + pub applies_to: AppliesTo, +} + #[derive(Serialize, Deserialize, Default, Clone)] pub struct Config { pub favorites: Vec, + #[serde(default)] + pub text_editor: String, + #[serde(default)] + pub custom_menu_items: Vec, } impl Config { + /// Preferred path: config.json next to the running binary. + /// Falls back to ~/.config/tui-fm/config.json if the exe path can't be determined. fn config_path() -> Option { + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + return Some(dir.join("config.json")); + } + } dirs::config_dir().map(|d| d.join("tui-fm").join("config.json")) } diff --git a/src/events.rs b/src/events.rs index baa5bfb..cd16765 100644 --- a/src/events.rs +++ b/src/events.rs @@ -6,7 +6,8 @@ use crossterm::event::{ }; use ratatui::{backend::Backend, Terminal}; -use crate::app::{App, ConfirmAction, ContextMenuItem, DialogMode, Focus}; +use crate::app::{App, ConfirmAction, ContextMenuItem, DialogMode, Focus, SettingsField}; +use crate::config::{AppliesTo, CustomMenuItem}; use crate::ui; pub fn run_app(terminal: &mut Terminal, app: &mut App) -> io::Result<()> { @@ -100,6 +101,10 @@ fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) -> bool { } return false; } + DialogMode::Settings => { + handle_settings_key(app, key); + return false; + } DialogMode::None => {} } @@ -310,11 +315,199 @@ fn execute_context_menu(app: &mut App) { // Toggle submenu - handled in UI } ContextMenuItem::Separator => {} + ContextMenuItem::Custom(idx) => { + execute_custom_item(app, idx); + } } } } } +fn execute_custom_item(app: &mut App, idx: usize) { + let Some(ci) = app.config.custom_menu_items.get(idx).cloned() else { return; }; + let path_str = app.selected_indices.first() + .and_then(|&i| app.entries.get(i)) + .map(|e| e.path.to_string_lossy().into_owned()) + .unwrap_or_else(|| app.current_path.to_string_lossy().into_owned()); + let cmd = ci.command.replace("$[path]", &path_str); + #[cfg(unix)] + let result = std::process::Command::new("sh").arg("-c").arg(&cmd).spawn(); + #[cfg(windows)] + let result = std::process::Command::new("cmd").arg("/C").arg(&cmd).spawn(); + match result { + Ok(_) => app.set_status(format!("Kör: {}", ci.name)), + Err(e) => app.set_status(format!("Fel: {}", e)), + } +} + +fn handle_settings_key(app: &mut App, key: crossterm::event::KeyEvent) { + if let Some(field) = app.settings_editing_field.clone() { + match key.code { + KeyCode::Esc => { + app.settings_editing_field = None; + app.settings_editing_buf.clear(); + } + KeyCode::Enter => { + let buf = app.settings_editing_buf.clone(); + match field { + SettingsField::TextEditor => { app.config.text_editor = buf; } + SettingsField::CustomName(i) => { + if let Some(ci) = app.config.custom_menu_items.get_mut(i) { ci.name = buf; } + } + SettingsField::CustomCommand(i) => { + if let Some(ci) = app.config.custom_menu_items.get_mut(i) { ci.command = buf; } + } + } + app.config.save(); + app.settings_editing_field = None; + app.settings_editing_buf.clear(); + } + KeyCode::Backspace => { app.settings_editing_buf.pop(); } + KeyCode::Char(c) => { app.settings_editing_buf.push(c); } + _ => {} + } + return; + } + let custom_count = app.config.custom_menu_items.len(); + match key.code { + KeyCode::Esc => { app.dialog = DialogMode::None; } + KeyCode::Up => { if app.settings_selected_row > 0 { app.settings_selected_row -= 1; } } + KeyCode::Down => { + let max = 5 + custom_count; + if app.settings_selected_row < max { app.settings_selected_row += 1; } + } + KeyCode::Enter => { settings_activate(app); } + KeyCode::Char('n') => { + app.config.custom_menu_items.push(CustomMenuItem { + name: String::from("Nytt kommando"), + command: String::from("echo $[path]"), + applies_to: AppliesTo::Both, + }); + app.config.save(); + } + KeyCode::Char('d') | KeyCode::Delete => { + if app.settings_selected_row >= 4 { + let i = app.settings_selected_row - 4; + if i < custom_count { + app.config.custom_menu_items.remove(i); + app.config.save(); + if app.settings_selected_row > 4 { app.settings_selected_row -= 1; } + } + } + } + _ => {} + } +} + +fn settings_activate(app: &mut App) { + let row = app.settings_selected_row; + let custom_count = app.config.custom_menu_items.len(); + match row { + // Row 1 = editor value field + 1 => { + app.settings_editing_buf = app.config.text_editor.clone(); + app.settings_editing_field = Some(SettingsField::TextEditor); + } + r if r >= 4 && r < 4 + custom_count => { + // Cycle AppliesTo on Enter + let i = r - 4; + if let Some(ci) = app.config.custom_menu_items.get_mut(i) { + ci.applies_to = ci.applies_to.cycle(); + app.config.save(); + } + } + r if r == 4 + custom_count => { + // [+ Ny] button: add new item + app.config.custom_menu_items.push(CustomMenuItem { + name: String::from("Nytt kommando"), + command: String::from("echo $[path]"), + applies_to: AppliesTo::Both, + }); + app.config.save(); + } + r if r == 5 + custom_count => { + // [Spara] button + app.config.save(); + app.dialog = DialogMode::None; + } + _ => {} + } +} + +pub fn settings_click(app: &mut App, col: u16, row: u16, dlg: ratatui::layout::Rect) { + if col < dlg.x || col >= dlg.x + dlg.width { return; } + if row < dlg.y || row >= dlg.y + dlg.height { return; } + let inner_y = dlg.y + 1; // top border + let inner_x = dlg.x + 1; + let inner_w = dlg.width.saturating_sub(2); + if row < inner_y { return; } + let list_row = (row - inner_y) as usize; + let custom_count = app.config.custom_menu_items.len(); + + // Visual layout (list_row = offset from inner top): + // 0 : "Texteditor" label + // 1 : editor value field + // 2 : separator + // 3 : "Anpassade menyval" header + // 4 : column header (Namn / Kommando / Typ) – not clickable + // 5 .. 4+custom_count : custom items (keyboard sel = list_row - 1) + // 5+custom_count (or 6 if empty) : [+ Ny] [Ta bort] + // 6+custom_count (or 7 if empty) : [Spara] + let btn_list_row: usize = if custom_count == 0 { 6 } else { 5 + custom_count }; + let save_list_row: usize = btn_list_row + 1; + + if list_row == 1 { + app.settings_editing_buf = app.config.text_editor.clone(); + app.settings_editing_field = Some(SettingsField::TextEditor); + app.settings_selected_row = 1; + } else if list_row >= 5 && custom_count > 0 && list_row < 5 + custom_count { + let i = list_row - 5; + app.settings_selected_row = 4 + i; + app.settings_custom_selected = i; + let col_rel = col.saturating_sub(inner_x); + let name_w = inner_w / 3; + let cmd_w = inner_w / 3; + if col_rel < name_w { + if let Some(ci) = app.config.custom_menu_items.get(i) { + app.settings_editing_buf = ci.name.clone(); + app.settings_editing_field = Some(SettingsField::CustomName(i)); + } + } else if col_rel < name_w + cmd_w { + if let Some(ci) = app.config.custom_menu_items.get(i) { + app.settings_editing_buf = ci.command.clone(); + app.settings_editing_field = Some(SettingsField::CustomCommand(i)); + } + } else if let Some(ci) = app.config.custom_menu_items.get_mut(i) { + ci.applies_to = ci.applies_to.cycle(); + app.config.save(); + } + } else if list_row == btn_list_row { + app.settings_selected_row = 4 + custom_count; + let col_rel = col.saturating_sub(inner_x) as usize; + if col_rel < 8 { + // [+ Ny] + app.config.custom_menu_items.push(CustomMenuItem { + name: String::from("Nytt kommando"), + command: String::from("echo $[path]"), + applies_to: AppliesTo::Both, + }); + app.config.save(); + } else if col_rel < 26 { + // [Ta bort markerad] + let sel = app.settings_custom_selected; + if sel < app.config.custom_menu_items.len() { + app.config.custom_menu_items.remove(sel); + app.config.save(); + if app.settings_selected_row > 4 { app.settings_selected_row -= 1; } + } + } + } else if list_row == save_list_row { + app.settings_selected_row = 5 + custom_count; + app.config.save(); + app.dialog = DialogMode::None; + } +} + fn handle_mouse(app: &mut App, mouse: crossterm::event::MouseEvent) { match mouse.kind { MouseEventKind::ScrollDown => handle_scroll(app, mouse.column, mouse.row, 3), @@ -470,6 +663,31 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) { // Check path input area let pi = layout.path_input; + + // Check settings gear button (bottom-right corner of status bar) + let sb = layout.status_bar; + // Gear rendered at inner.x + inner.width - 4 = sb.x + sb.width - 5 + let gear_col = sb.x + sb.width.saturating_sub(5); + let gear_row = sb.y + 1; // inside the border + if row == gear_row && col >= gear_col && col < gear_col + 3 { + // If settings is already open and we click inside the dialog – handled by settings_click + if app.dialog == DialogMode::Settings { + let dlg = ui::settings_dialog_rect(sb.x + sb.width); + settings_click(app, col, row, dlg); + } else { + app.dialog = DialogMode::Settings; + } + return; + } + + // If settings dialog is open, route all clicks into it + if app.dialog == DialogMode::Settings { + let dlg = ui::settings_dialog_rect(sb.x + sb.width); + settings_click(app, col, row, dlg); + return; + } + + // Check path input area if row == pi.y && col >= pi.x && col < pi.x + pi.width { app.focus = Focus::PathInput; return; @@ -551,6 +769,31 @@ fn handle_right_click(app: &mut App, col: u16, row: u16) { } fn handle_mouse_move(app: &mut App, col: u16, row: u16) { + // Settings hover: highlight rows when mouse moves over the dialog + if app.dialog == DialogMode::Settings { + let layout = ui::compute_layout(app); + let dlg = ui::settings_dialog_rect(layout.status_bar.x + layout.status_bar.width); + if col > dlg.x && col < dlg.x + dlg.width.saturating_sub(1) + && row > dlg.y && row < dlg.y + dlg.height.saturating_sub(1) + { + let inner_y = dlg.y + 1; + let list_row = (row - inner_y) as usize; + let custom_count = app.config.custom_menu_items.len(); + let btn_lr = if custom_count == 0 { 6 } else { 5 + custom_count }; + let save_lr = btn_lr + 1; + if list_row == 1 { + app.settings_selected_row = 1; + } else if list_row >= 5 && custom_count > 0 && list_row < 5 + custom_count { + app.settings_selected_row = 4 + (list_row - 5); + } else if list_row == btn_lr { + app.settings_selected_row = 4 + custom_count; + } else if list_row == save_lr { + app.settings_selected_row = 5 + custom_count; + } + } + return; + } + const MENU_W: u16 = 24; const SUB_W: u16 = 18; diff --git a/src/ui.rs b/src/ui.rs index 7a89344..a595a9a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -83,6 +83,7 @@ pub fn draw(f: &mut Frame, app: &mut App) { DialogMode::Properties(info) => draw_properties(f, info.clone(), area), DialogMode::Error(msg) => draw_error_dialog(f, msg, area), DialogMode::Confirm(msg, _) => draw_confirm_dialog(f, msg, area), + DialogMode::Settings => draw_settings(f, app, layout.status_bar.x + layout.status_bar.width), DialogMode::None => {} } } @@ -318,9 +319,9 @@ fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) { hint_area, ); - // Right: path input + // Right: path input (leave 5 chars on the far right for gear button) let input_x = inner.x + hint_width + 1; - let input_width = inner.width.saturating_sub(hint_width + 1); + let input_width = inner.width.saturating_sub(hint_width + 1 + 5); if input_width < 4 { return; } let input_area = Rect::new(input_x, inner.y, input_width, inner.height); @@ -361,6 +362,18 @@ fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) { let para = Paragraph::new(Line::from(spans)); f.render_widget(para, input_area); + + // Gear button at bottom-right + let gear_x = inner.x + inner.width.saturating_sub(4); + let gear_style = if app.dialog == DialogMode::Settings { + Style::default().fg(Color::Black).bg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Yellow) + }; + f.render_widget( + Paragraph::new(" ⚙ ").style(gear_style), + Rect::new(gear_x, inner.y, 3, 1), + ); } fn scroll_input_text(text: &str, cursor: usize, avail: usize) -> (String, usize) { @@ -376,12 +389,11 @@ fn scroll_input_text(text: &str, cursor: usize, avail: usize) -> (String, usize) (slice.to_string(), cursor_in_display) } -fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { +fn draw_context_menu(f: &mut Frame, _app: &App, menu: crate::app::ContextMenu) { let items = &menu.items; if items.is_empty() { return; } - // Calculate menu dimensions - let menu_width: u16 = 24; + let menu_width: u16 = 26; let menu_height = items.len() as u16 + 2; let term_area = f.area(); @@ -389,7 +401,6 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { let y = menu.y.min(term_area.height.saturating_sub(menu_height)); let menu_area = Rect::new(x, y, menu_width, menu_height); - f.render_widget(Clear, menu_area); let block = Block::default() @@ -404,25 +415,42 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { let mut new_sub_row: u16 = 0; for (i, item) in items.iter().enumerate() { - let row = Rect::new(inner.x, inner.y + i as u16, inner.width, 1); + let row_rect = Rect::new(inner.x, inner.y + i as u16, inner.width, 1); let is_sel = i == menu.selected; - let (text, style) = match item { - ContextMenuItem::Copy => (" 📋 Kopiera ", Style::default()), - ContextMenuItem::Cut => (" ✂ Klipp ut ", Style::default()), - ContextMenuItem::Paste => (" 📌 Klistra in ", Style::default()), - ContextMenuItem::Rename => (" ✏ Byt namn ", Style::default()), - ContextMenuItem::Properties => (" ℹ Egenskaper ", Style::default()), - ContextMenuItem::Delete => (" 🗑 Ta bort ", Style::default().fg(Color::Red)), + let (owned_text, style): (String, Style) = match item { + ContextMenuItem::Copy => + (" \u{1F4CB} Kopiera ".into(), Style::default()), + ContextMenuItem::Cut => + (" \u{2702} Klipp ut ".into(), Style::default()), + ContextMenuItem::Paste => + (" \u{1F4CC} Klistra in ".into(), Style::default()), + ContextMenuItem::Rename => + (" \u{270F} Byt namn ".into(), Style::default()), + ContextMenuItem::Properties => + (" \u{2139} Egenskaper ".into(), Style::default()), + ContextMenuItem::Delete => + (" \u{1F5D1} Ta bort ".into(), Style::default().fg(Color::Red)), ContextMenuItem::NewSubmenu => { if is_sel { show_new_sub = true; new_sub_row = inner.y + i as u16; } - (" ➕ Ny ▶ ", Style::default().fg(Color::Green)) + (" + Ny > ".into(), Style::default().fg(Color::Green)) + } + ContextMenuItem::AddFavorite => + (" \u{2605} Lagg till fav ".into(), Style::default().fg(Color::Yellow)), + ContextMenuItem::RemoveFavorite(_) => + (" \u{2716} Ta bort favorit ".into(), Style::default().fg(Color::Red)), + ContextMenuItem::Separator => + (" -------------------- ".into(), Style::default().fg(Color::DarkGray)), + ContextMenuItem::NewFile => + (" \u{1F4C4} Ny fil ".into(), Style::default().fg(Color::Green)), + ContextMenuItem::NewDir => + (" \u{1F4C1} Ny mapp ".into(), Style::default().fg(Color::Green)), + ContextMenuItem::Custom(idx) => { + let name = _app.config.custom_menu_items.get(*idx) + .map(|ci| format!(" > {:<20}", ci.name)) + .unwrap_or_else(|| " > ?".into()); + (name, Style::default().fg(Color::LightMagenta)) } - ContextMenuItem::AddFavorite => (" ★ Lägg till fav ", Style::default().fg(Color::Yellow)), - ContextMenuItem::RemoveFavorite(_) => (" ✖ Ta bort favorit ", Style::default().fg(Color::Red)), - ContextMenuItem::Separator => (" ──────────────── ", Style::default().fg(Color::DarkGray)), - ContextMenuItem::NewFile => (" 📄 Ny fil ", Style::default().fg(Color::Green)), - ContextMenuItem::NewDir => (" 📁 Ny mapp ", Style::default().fg(Color::Green)), }; let bg = if is_sel && !matches!(item, ContextMenuItem::Separator) { @@ -430,42 +458,169 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { } else { style }; - - f.render_widget( - Paragraph::new(text).style(bg), - row, - ); + f.render_widget(Paragraph::new(owned_text.as_str()).style(bg), row_rect); } - // Draw submenu for "Ny" if selected if show_new_sub { let sub_x = x + menu_width; let sub_area = Rect::new(sub_x.min(term_area.width.saturating_sub(18)), new_sub_row, 18, 4); f.render_widget(Clear, sub_area); - let sub_block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(Color::Green)); let sub_inner = sub_block.inner(sub_area); f.render_widget(sub_block, sub_area); + let s0 = if menu.sub_selected == Some(0) { Style::default().bg(Color::Blue).fg(Color::White) } else { Style::default().fg(Color::White) }; + let s1 = if menu.sub_selected == Some(1) { Style::default().bg(Color::Blue).fg(Color::White) } else { Style::default().fg(Color::White) }; + f.render_widget(Paragraph::new(" \u{1F4C4} Ny fil ").style(s0), Rect::new(sub_inner.x, sub_inner.y, sub_inner.width, 1)); + f.render_widget(Paragraph::new(" \u{1F4C1} Ny mapp ").style(s1), Rect::new(sub_inner.x, sub_inner.y + 1, sub_inner.width, 1)); + } +} - let sub_item0_style = if menu.sub_selected == Some(0) { - Style::default().bg(Color::Blue).fg(Color::White) +/// Compute the settings dialog rect anchored to the bottom-right corner. +pub fn settings_dialog_rect(right_edge: u16) -> ratatui::layout::Rect { + let dlg_width: u16 = 70; + let dlg_height: u16 = 28; + let x = right_edge.saturating_sub(dlg_width + 1); + let (_, term_h) = crossterm::terminal::size().unwrap_or((120, 40)); + let y = term_h.saturating_sub(dlg_height + 3); + Rect::new(x, y, dlg_width, dlg_height) +} + +fn draw_settings(f: &mut Frame, app: &App, right_edge: u16) { + let area = settings_dialog_rect(right_edge); + let term_area = f.area(); + let area = Rect::new( + area.x.min(term_area.width.saturating_sub(area.width)), + area.y.min(term_area.height.saturating_sub(area.height)), + area.width.min(term_area.width), + area.height.min(term_area.height), + ); + + f.render_widget(Clear, area); + let block = Block::default() + .title(" \u{2699} Installningar (Esc=stang | n=ny | d=ta bort) ") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)); + let inner = block.inner(area); + f.render_widget(block, area); + + let sel = app.settings_selected_row; + let editing = &app.settings_editing_field; + let buf = &app.settings_editing_buf; + + let label_s = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD); + let value_s = Style::default().fg(Color::White); + let sel_bg = Style::default().bg(Color::DarkGray).fg(Color::White); + let hint_s = Style::default().fg(Color::DarkGray); + + let mut row_y = inner.y; + + // Row 0: TextEditor section label + f.render_widget( + Paragraph::new("Texteditor (standardapp for textfiler)").style(label_s), + Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1), + ); + row_y += 1; + + // Row 1: text editor value / edit field + let is_editing_editor = matches!(editing, Some(crate::app::SettingsField::TextEditor)); + let editor_style = if sel == 1 && !is_editing_editor { sel_bg } else { value_s }; + let editor_text = if is_editing_editor { + format!("{}_", buf) + } else if app.config.text_editor.is_empty() { + " (ej installtt - tryck Enter for att redigera)".into() + } else { + format!(" {}", app.config.text_editor) + }; + f.render_widget( + Paragraph::new(editor_text).style(editor_style), + Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1), + ); + row_y += 1; + + // Row 2: separator + f.render_widget( + Paragraph::new("-".repeat(inner.width.saturating_sub(2) as usize)).style(hint_s), + Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1), + ); + row_y += 1; + + // Row 3: Custom items header + f.render_widget( + Paragraph::new("Anpassade menyval (Enter=andra typ | klick=andra falt)").style(label_s), + Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1), + ); + row_y += 1; + + // Column header row + let col_w = ((inner.width.saturating_sub(6)) / 3) as usize; + let header = format!(" {:= inner.y + inner.height.saturating_sub(3) { break; } + let row_idx = 4 + i; + let is_row_sel = sel == row_idx; + + let name_str = if matches!(editing, Some(crate::app::SettingsField::CustomName(n)) if *n == i) { + format!("{}_", buf) } else { - Style::default().fg(Color::White) + ci.name.clone() }; - let sub_item1_style = if menu.sub_selected == Some(1) { - Style::default().bg(Color::Blue).fg(Color::White) + let cmd_str = if matches!(editing, Some(crate::app::SettingsField::CustomCommand(n)) if *n == i) { + format!("{}_", buf) } else { - Style::default().fg(Color::White) + ci.command.clone() }; + + let row_style = if is_row_sel { sel_bg } else { value_s }; + let name_col = truncate_str(&name_str, col_w); + let cmd_col = truncate_str(&cmd_str, col_w); + let row_text = format!(" {: