Implement settings dialog and custom menu item management
This commit is contained in:
20
build/linux/config.json
Normal file
20
build/linux/config.json
Normal 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.
49
src/app.rs
49
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<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);
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
|
||||
245
src/events.rs
245
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<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
229
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!(" {:<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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user