Built-in settings dialog opened from a panel gear button
- New spawn_settings action + '≡ Inställningar' button in the default panel (≡ instead of ⚙ which has ambiguous character width): opens a singleton settings window - Rows: theme (cycles built-in + themes/*.toml), panel (cycles panels/*.toml), shadow and scrollbar toggles — keyboard (arrows/ Enter/Esc) and mouse (click selects, click again activates) - Shows where config.toml, themes/ and panels/ live - Changes apply immediately and persist to config.toml via toml_edit; the hot-reload watcher picks up the file writes for panels - README and --help updated with switcher/snap/copy-mode/theme docs - Integration suite: 30/30 (gear click, dialog opens, shadow toggle persisted to config.toml) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
172
src/app.rs
172
src/app.rs
@@ -301,6 +301,11 @@ pub enum WindowContent {
|
||||
input: String,
|
||||
cursor_pos: usize,
|
||||
},
|
||||
/// Inbyggd inställningsdialog (tema, panel, skugga, scrollbar)
|
||||
Settings {
|
||||
/// Markerad rad (0=tema, 1=panel, 2=skugga, 3=scrollbar)
|
||||
selected: usize,
|
||||
},
|
||||
PopupDialog {
|
||||
message: String,
|
||||
buttons: Vec<String>,
|
||||
@@ -709,6 +714,7 @@ impl App {
|
||||
WindowContent::Terminal { alive, .. } => *alive,
|
||||
WindowContent::RunDialog { .. } => true,
|
||||
WindowContent::PopupDialog { .. } => true,
|
||||
WindowContent::Settings { .. } => true,
|
||||
});
|
||||
let alive_ids: Vec<usize> = self.windows.iter().map(|w| w.id).collect();
|
||||
self.focus_history.retain(|h| alive_ids.contains(h));
|
||||
@@ -832,6 +838,16 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
// 2a2. Inställningsdialogen hanterar sina egna tangenter
|
||||
if let Some(id) = self.focused_id {
|
||||
let is_settings = self.windows.iter()
|
||||
.any(|w| w.id == id && matches!(w.content, WindowContent::Settings { .. }));
|
||||
if is_settings {
|
||||
self.handle_settings_key(id, key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2b. PopupDialog hanterar sina egna tangenter
|
||||
if let Some(id) = self.focused_id {
|
||||
let is_popup = self
|
||||
@@ -1275,6 +1291,34 @@ impl App {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4b. Klick i inställningsdialogen: välj rad, aktivera vid andra klicket
|
||||
let settings_hit = self
|
||||
.windows
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|w| w.in_content(col, row) && matches!(w.content, WindowContent::Settings { .. }))
|
||||
.map(|w| (w.id, w.content_rect()));
|
||||
if let Some((id, cr)) = settings_hit {
|
||||
self.focus_window(id);
|
||||
// rad 1..=4 i innehållet motsvarar inställningsrad 0..=3
|
||||
let rel = row.saturating_sub(cr.y);
|
||||
if (1..=4).contains(&rel) {
|
||||
let target = (rel - 1) as usize;
|
||||
let mut activate = false;
|
||||
if let Some(WindowContent::Settings { selected }) = self.settings_content(id) {
|
||||
if *selected == target {
|
||||
activate = true;
|
||||
} else {
|
||||
*selected = target;
|
||||
}
|
||||
}
|
||||
if activate {
|
||||
self.settings_activate(id, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Innehållsyta → fokus + vidarebefordra musklick till terminal
|
||||
let hit = self.windows.iter().rev().find(|w| w.in_content(col, row)).map(|w| {
|
||||
let (mode, encoding) = match &w.content {
|
||||
@@ -1429,6 +1473,7 @@ impl App {
|
||||
MenuAction::Submenu { .. } => {}
|
||||
MenuAction::WindowSwitcher => self.switcher_open_or_advance(),
|
||||
MenuAction::CopyMode => self.enter_copy_mode(),
|
||||
MenuAction::SpawnSettings => self.spawn_settings(),
|
||||
MenuAction::SnapLeft => self.snap_focused(true),
|
||||
MenuAction::SnapRight => self.snap_focused(false),
|
||||
MenuAction::Maximize => self.toggle_maximize_focused(),
|
||||
@@ -1657,6 +1702,132 @@ impl App {
|
||||
id
|
||||
}
|
||||
|
||||
/// Öppna inställningsdialogen (eller fokusera en redan öppen).
|
||||
fn spawn_settings(&mut self) {
|
||||
if let Some(w) = self.windows.iter().find(|w| matches!(w.content, WindowContent::Settings { .. })) {
|
||||
let id = w.id;
|
||||
self.focus_window(id);
|
||||
return;
|
||||
}
|
||||
let ca = self.content_area;
|
||||
let w = 62u16.min(ca.width.saturating_sub(2));
|
||||
let h = 13u16.min(ca.height.saturating_sub(2));
|
||||
let x = ca.x as i32 + ((ca.width.saturating_sub(w)) / 2) as i32;
|
||||
let y = ca.y as i32 + ((ca.height.saturating_sub(h)) / 2) as i32;
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.windows.push(FloatingWindow {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
content: WindowContent::Settings { selected: 0 },
|
||||
dragging: None,
|
||||
resizing: None,
|
||||
resizable: false,
|
||||
mouse_mode: vt100::MouseProtocolMode::None,
|
||||
mouse_encoding: vt100::MouseProtocolEncoding::Default,
|
||||
mouse_seq_carry: Vec::new(),
|
||||
selection: None,
|
||||
kitty_gfx_carry: Vec::new(),
|
||||
pending_graphics: Vec::new(),
|
||||
scrollback_total: 0,
|
||||
prev_geom: None,
|
||||
});
|
||||
self.focus_window(id);
|
||||
}
|
||||
|
||||
fn handle_settings_key(&mut self, id: usize, key: KeyEvent) {
|
||||
const ROWS: usize = 4;
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') => self.close_window(id),
|
||||
KeyCode::Up => {
|
||||
if let Some(WindowContent::Settings { selected }) = self.settings_content(id) {
|
||||
*selected = (*selected + ROWS - 1) % ROWS;
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Tab => {
|
||||
if let Some(WindowContent::Settings { selected }) = self.settings_content(id) {
|
||||
*selected = (*selected + 1) % ROWS;
|
||||
}
|
||||
}
|
||||
KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => self.settings_activate(id, true),
|
||||
KeyCode::Left => self.settings_activate(id, false),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn settings_content(&mut self, id: usize) -> Option<&mut WindowContent> {
|
||||
self.windows.iter_mut().find(|w| w.id == id).map(|w| &mut w.content)
|
||||
}
|
||||
|
||||
/// Lista *.toml i en katalog (relativa sökvägar, sorterade).
|
||||
fn list_toml(dir: &str) -> Vec<String> {
|
||||
let mut out: Vec<String> = std::fs::read_dir(dir)
|
||||
.map(|rd| {
|
||||
rd.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().is_some_and(|x| x == "toml"))
|
||||
.map(|e| format!("{}/{}", dir, e.file_name().to_string_lossy()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// Aktivera/cykla den markerade inställningsraden.
|
||||
/// Ändringar skrivs till config.toml — hot-reload plockar upp dem
|
||||
/// direkt, men tema/flaggor appliceras även omedelbart i minnet.
|
||||
fn settings_activate(&mut self, id: usize, forward: bool) {
|
||||
let Some(WindowContent::Settings { selected }) = self.settings_content(id) else { return };
|
||||
let row = *selected;
|
||||
match row {
|
||||
0 => {
|
||||
// Tema: cykla inbyggt + themes/*.toml
|
||||
let mut options: Vec<Option<String>> = vec![None];
|
||||
options.extend(Self::list_toml("themes").into_iter().map(Some));
|
||||
let cur = options
|
||||
.iter()
|
||||
.position(|o| o.as_deref() == self.config.theme.as_deref())
|
||||
.unwrap_or(0);
|
||||
let next = if forward {
|
||||
(cur + 1) % options.len()
|
||||
} else {
|
||||
(cur + options.len() - 1) % options.len()
|
||||
};
|
||||
self.config.theme = options[next].clone();
|
||||
self.theme = Theme::load_or_default(self.config.theme.as_deref());
|
||||
let _ = Config::save_top_level_str("config.toml", "theme", self.config.theme.as_deref());
|
||||
}
|
||||
1 => {
|
||||
// Panel: cykla panels/*.toml för panel 0
|
||||
let options = Self::list_toml("panels");
|
||||
if options.is_empty() || self.config.panels.is_empty() {
|
||||
return;
|
||||
}
|
||||
let cur_file = self.config.panels[0].file.clone().unwrap_or_default();
|
||||
let cur = options.iter().position(|o| *o == cur_file).unwrap_or(0);
|
||||
let next = if forward {
|
||||
(cur + 1) % options.len()
|
||||
} else {
|
||||
(cur + options.len() - 1) % options.len()
|
||||
};
|
||||
let _ = Config::save_panel_file("config.toml", 0, &options[next]);
|
||||
// panel-innehållet läses om av hot-reloaden
|
||||
}
|
||||
2 => {
|
||||
self.config.shadow = !self.config.shadow;
|
||||
let _ = Config::save_top_level_bool("config.toml", "shadow", self.config.shadow);
|
||||
}
|
||||
3 => {
|
||||
self.config.show_scrollbar = !self.config.show_scrollbar;
|
||||
let _ = Config::save_top_level_bool("config.toml", "show_scrollbar", self.config.show_scrollbar);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gå in i copy-mode för det fokuserade terminalfönstret.
|
||||
fn enter_copy_mode(&mut self) {
|
||||
let Some(id) = self.focused_id else { return };
|
||||
@@ -2232,6 +2403,7 @@ fn action_display(action: &MenuAction) -> &'static str {
|
||||
MenuAction::SpawnRunDialog => "Kommandodialog",
|
||||
MenuAction::WindowSwitcher => "Fönsterväxlare",
|
||||
MenuAction::CopyMode => "Copy-mode",
|
||||
MenuAction::SpawnSettings => "Inställningar",
|
||||
MenuAction::SnapLeft => "Fäst vänster",
|
||||
MenuAction::SnapRight => "Fäst höger",
|
||||
MenuAction::Maximize => "Maximera/återställ",
|
||||
|
||||
@@ -125,6 +125,8 @@ pub enum MenuAction {
|
||||
Maximize,
|
||||
/// Copy-mode: bläddra/söka i fokuserat fönsters historik
|
||||
CopyMode,
|
||||
/// Öppnar den inbyggda inställningsdialogen
|
||||
SpawnSettings,
|
||||
/// Gör ingenting – används för display-only rader i menyer
|
||||
NoOp,
|
||||
}
|
||||
@@ -271,6 +273,44 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sätt/ta bort en toppnivå-strängnyckel i config-filen (TOML).
|
||||
pub fn save_top_level_str(path: &str, key: &str, val: Option<&str>) -> anyhow::Result<()> {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
let mut doc: toml_edit::DocumentMut = content.parse().unwrap_or_default();
|
||||
match val {
|
||||
Some(v) => {
|
||||
doc[key] = toml_edit::value(v);
|
||||
}
|
||||
None => {
|
||||
doc.remove(key);
|
||||
}
|
||||
}
|
||||
fs::write(path, doc.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sätt en toppnivå-boolnyckel i config-filen (TOML).
|
||||
pub fn save_top_level_bool(path: &str, key: &str, val: bool) -> anyhow::Result<()> {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
let mut doc: toml_edit::DocumentMut = content.parse().unwrap_or_default();
|
||||
doc[key] = toml_edit::value(val);
|
||||
fs::write(path, doc.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sätt fil-referensen för panel nummer `index` i config-filen.
|
||||
pub fn save_panel_file(path: &str, index: usize, file: &str) -> anyhow::Result<()> {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
let mut doc: toml_edit::DocumentMut = content.parse().unwrap_or_default();
|
||||
if let Some(arr) = doc.get_mut("panel").and_then(|p| p.as_array_of_tables_mut()) {
|
||||
if let Some(tbl) = arr.get_mut(index) {
|
||||
tbl["file"] = toml_edit::value(file);
|
||||
}
|
||||
}
|
||||
fs::write(path, doc.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uppdatera background_image i config-filen (TOML)
|
||||
pub fn save_background(path: &str, image_path: Option<&str>) -> anyhow::Result<()> {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -97,8 +97,18 @@ KONFIGURATION (./config.toml + panels/*.toml):
|
||||
TANGENTER (standard, ändras via [[keybind]] i config.toml):
|
||||
ctrl+space Kommandodialog — skriv t.ex. `htop` och tryck Enter
|
||||
alt+enter Ny terminal
|
||||
alt+tab/alt+w Fönsterväxlare (MRU) — Tab stegar, Enter väljer
|
||||
alt+left/right Fäst fönster som vänster/höger halvskärm
|
||||
alt+up Maximera/återställ (även dubbelklick på titelraden)
|
||||
alt+c Copy-mode: bläddra/söka i historiken (/ n N q)
|
||||
ctrl+q Avsluta (från en ansluten klient: kopplar bara ner den)
|
||||
|
||||
TEMA & INSTÄLLNINGAR:
|
||||
theme = \"themes/dark.toml\" i config.toml väljer tema (egna teman =
|
||||
egna TOML-filer). Kugghjulet ≡ i panelen öppnar inbyggda
|
||||
inställningar (tema, panel, skugga, scrollbar). config.toml,
|
||||
panel- och temafiler hot-reloadas när de ändras.
|
||||
|
||||
MUS:
|
||||
Dra titelraden = flytta fönster Dra kant/hörn = ändra storlek
|
||||
Scroll = historik (skal) eller vidarebefordras (appar med mus-stöd)
|
||||
@@ -491,6 +501,7 @@ fn handle_ipc_event(
|
||||
WindowContent::PopupDialog { message, .. } => {
|
||||
format!("[Popup: {}]", &message[..message.len().min(20)])
|
||||
}
|
||||
WindowContent::Settings { .. } => "[Inställningar]".to_string(),
|
||||
};
|
||||
Some(WindowInfo {
|
||||
id: w.id,
|
||||
|
||||
@@ -77,6 +77,7 @@ fn render_switcher(frame: &mut Frame, app: &App, sw: &crate::app::SwitcherState)
|
||||
}
|
||||
WindowContent::RunDialog { .. } => "Kommandodialog".to_string(),
|
||||
WindowContent::PopupDialog { .. } => "Popup".to_string(),
|
||||
WindowContent::Settings { .. } => "Inställningar".to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| format!("fönster {}", id))
|
||||
})
|
||||
@@ -298,6 +299,7 @@ fn render_window(frame: &mut Frame, window: &FloatingWindow, app: &App) {
|
||||
WindowContent::Terminal { title, .. } => format!(" {} ", title),
|
||||
WindowContent::RunDialog { .. } => " Tui-run ".to_string(),
|
||||
WindowContent::PopupDialog { .. } => " TUI-WM ".to_string(),
|
||||
WindowContent::Settings { .. } => " Inställningar ".to_string(),
|
||||
};
|
||||
|
||||
// Diskret skugga nedanför/till höger om fönstret (inte när en
|
||||
@@ -386,6 +388,9 @@ fn render_window(frame: &mut Frame, window: &FloatingWindow, app: &App) {
|
||||
WindowContent::PopupDialog { .. } => {
|
||||
render_popup_dialog(frame, window, focused_id, th);
|
||||
}
|
||||
WindowContent::Settings { selected } => {
|
||||
render_settings_content(frame, cr, *selected, app, th);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy-mode: statusrad + markering av sökträffar
|
||||
@@ -506,6 +511,71 @@ fn highlight_matches(frame: &mut Frame, rect: Rect, query: &str, style: Style) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Innehållet i inställningsdialogen: interaktiva rader + info-sektion.
|
||||
fn render_settings_content(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
selected: usize,
|
||||
app: &App,
|
||||
th: &crate::theme::Theme,
|
||||
) {
|
||||
let theme_name = app.config.theme.as_deref().unwrap_or("(inbyggt)");
|
||||
let panel_name = app
|
||||
.config
|
||||
.panels
|
||||
.first()
|
||||
.and_then(|p| p.file.as_deref())
|
||||
.unwrap_or("(inline)");
|
||||
let onoff = |b: bool| if b { "[x] på " } else { "[ ] av " };
|
||||
let rows: Vec<(String, String)> = vec![
|
||||
("Tema".to_string(), format!("‹ {} ›", theme_name)),
|
||||
("Panel".to_string(), format!("‹ {} ›", panel_name)),
|
||||
("Skugga".to_string(), onoff(app.config.shadow).to_string()),
|
||||
("Scrollbar".to_string(), onoff(app.config.show_scrollbar).to_string()),
|
||||
];
|
||||
|
||||
let mut y = area.y + 1;
|
||||
for (i, (label, value)) in rows.iter().enumerate() {
|
||||
if y >= area.y + area.height {
|
||||
break;
|
||||
}
|
||||
let style = if i == selected {
|
||||
th.hover_highlight()
|
||||
} else {
|
||||
Style::default().fg(th.text).bg(Color::Black)
|
||||
};
|
||||
let line = format!(" {:<11}{}", format!("{}:", label), value);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(format!("{:<width$}", line, width = area.width as usize), style)),
|
||||
Rect::new(area.x, y, area.width, 1),
|
||||
);
|
||||
y += 1;
|
||||
}
|
||||
|
||||
// Info-sektion: var filerna finns
|
||||
let config_path = std::fs::canonicalize("config.toml")
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "config.toml".to_string());
|
||||
let info = [
|
||||
String::new(),
|
||||
format!(" Config: {}", config_path),
|
||||
" Teman: ./themes/*.toml".to_string(),
|
||||
" Paneler: ./panels/*.toml".to_string(),
|
||||
String::new(),
|
||||
" ↑↓ välj ←/→/Enter ändra Esc stäng".to_string(),
|
||||
];
|
||||
for line in info.iter() {
|
||||
if y >= area.y + area.height {
|
||||
break;
|
||||
}
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(line.clone(), Style::default().fg(th.hint))),
|
||||
Rect::new(area.x, y, area.width, 1),
|
||||
);
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_popup_dialog(
|
||||
frame: &mut Frame,
|
||||
window: &FloatingWindow,
|
||||
|
||||
Reference in New Issue
Block a user