Implement settings dialog and custom menu item management

This commit is contained in:
2026-03-28 03:48:41 +01:00
parent e6fdfa1aa3
commit b5c18d493c
6 changed files with 550 additions and 39 deletions

20
build/linux/config.json Normal file
View File

@@ -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"
}
]
}

Binary file not shown.

View File

@@ -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<usize>,
pub rename_original: Option<PathBuf>,
// Settings editor state
pub settings_selected_row: usize, // which row is highlighted in settings
pub settings_editing_field: Option<SettingsField>, // 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<usize> = 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);

View File

@@ -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<String>,
#[serde(default)]
pub text_editor: String,
#[serde(default)]
pub custom_menu_items: Vec<CustomMenuItem>,
}
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<PathBuf> {
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"))
}

View File

@@ -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<B: Backend>(terminal: &mut Terminal<B>, 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;

229
src/ui.rs
View File

@@ -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!(" {:<w$} {:<w$} {:<6}", "Namn", "Kommando ($[path])", "Typ", w = col_w);
f.render_widget(
Paragraph::new(header).style(hint_s),
Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1),
);
row_y += 1;
// Custom item rows (row index 4+)
let custom_count = app.config.custom_menu_items.len();
for (i, ci) in app.config.custom_menu_items.iter().enumerate() {
if row_y >= 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!(" {:<w$} {:<w$} {:<6}", name_col, cmd_col, ci.applies_to.label(), w = col_w);
f.render_widget(
Paragraph::new(" 📄 Ny fil ").style(sub_item0_style),
Rect::new(sub_inner.x, sub_inner.y, sub_inner.width, 1),
Paragraph::new(row_text).style(row_style),
Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1),
);
row_y += 1;
}
if custom_count == 0 {
f.render_widget(
Paragraph::new(" 📁 Ny mapp ").style(sub_item1_style),
Rect::new(sub_inner.x, sub_inner.y + 1, sub_inner.width, 1),
Paragraph::new(" (inga anpassade val - tryck n for att lagga till)").style(hint_s),
Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1),
);
row_y += 1;
}
// Action buttons row
if row_y < inner.y + inner.height.saturating_sub(2) {
let is_btn_sel = sel == 4 + custom_count;
let btn_s = if is_btn_sel { sel_bg } else { Style::default().fg(Color::Green) };
f.render_widget(
Paragraph::new("[+ Ny] [Ta bort markerad]").style(btn_s),
Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1),
);
row_y += 1;
}
// Save button row
if row_y < inner.y + inner.height.saturating_sub(1) {
let is_save_sel = sel == 5 + custom_count;
let save_s = if is_save_sel {
Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
};
f.render_widget(
Paragraph::new("[ Spara installningar ]").style(save_s),
Rect::new(inner.x + 1, row_y, inner.width.saturating_sub(2), 1),
);
}
}