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:
15
README.md
15
README.md
@@ -6,11 +6,15 @@ En terminal-baserad fönsterhanterare med flytande fönster, full mussuport och
|
|||||||
|
|
||||||
- Flytande terminaler med fri positionering och storleksändring
|
- Flytande terminaler med fri positionering och storleksändring
|
||||||
- Full mussuport: vänster/höger/mitten-klick, scroll, drag, ANSI-muse-protokoll vidarebefordras till appar
|
- Full mussuport: vänster/höger/mitten-klick, scroll, drag, ANSI-muse-protokoll vidarebefordras till appar
|
||||||
|
- Scrollback-historik med scrollbar + copy-mode med sökning (`alt+c`)
|
||||||
|
- Fönsterväxlare i MRU-ordning (`alt+tab`/`alt+w`)
|
||||||
|
- Snap till halvskärm (`alt+left`/`alt+right` eller dra mot kanten) och maximera (`alt+up`/dubbelklick på titelraden)
|
||||||
|
- Teman i egna TOML-filer (`themes/`) + inbyggd inställningsdialog (kugghjulet `≡` i panelen)
|
||||||
|
- Hot-reload: config.toml, panel- och temafiler laddas om live när de ändras
|
||||||
- Panel (topp/botten) med klickbara knappar och dynamiska status-widgets
|
- Panel (topp/botten) med klickbara knappar och dynamiska status-widgets
|
||||||
- Dropdown-menyer med undermenyer
|
- Dropdown-menyer med undermenyer
|
||||||
- Körbar kommandodialog (`ctrl+space`)
|
- Körbar kommandodialog (`ctrl+space`)
|
||||||
- Dynamisk omrendering vid terminalstorlek-ändringar
|
- SSH-kompatibel: daemon + display-klienter med full mus-forwarding
|
||||||
- SSH-kompatibel (fungerar via vanliga ANSI escape-koder)
|
|
||||||
- Kompilerar till fristående binärer för Linux och Windows
|
- Kompilerar till fristående binärer för Linux och Windows
|
||||||
|
|
||||||
## Teknikstack
|
## Teknikstack
|
||||||
@@ -85,6 +89,13 @@ Toppnivå-nycklar i `config.toml`:
|
|||||||
| `show_scrollbar` | `true` | Scrollbar på terminalfönstrens högerkant |
|
| `show_scrollbar` | `true` | Scrollbar på terminalfönstrens högerkant |
|
||||||
| `background_image` | – | Bakgrundsbild via Kitty graphics |
|
| `background_image` | – | Bakgrundsbild via Kitty graphics |
|
||||||
| `terminal_bg_color` | genomskinlig | Terminalfönstrens bakgrundsfärg |
|
| `terminal_bg_color` | genomskinlig | Terminalfönstrens bakgrundsfärg |
|
||||||
|
| `theme` | inbyggt | Temafil, t.ex. `"themes/dark.toml"` — se `themes/` för exempel |
|
||||||
|
| `shadow` | `true` | Skugga under/till höger om fönster |
|
||||||
|
|
||||||
|
Alla dessa (plus panel- och temafiler) **hot-reloadas** — spara filen så
|
||||||
|
uppdateras TUI-WM inom en sekund, utan omstart. Kugghjulet `≡` i panelen
|
||||||
|
öppnar den inbyggda inställningsdialogen som ändrar tema/panel/skugga/
|
||||||
|
scrollbar och sparar till config.toml.
|
||||||
|
|
||||||
Konfigurationen delas upp i två nivåer:
|
Konfigurationen delas upp i två nivåer:
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ action = { type = "spawn_terminal" }
|
|||||||
label = "mouse-test"
|
label = "mouse-test"
|
||||||
action = { type = "run_program", command = "testapps/mouse-test/target/release/mouse-test" }
|
action = { type = "run_program", command = "testapps/mouse-test/target/release/mouse-test" }
|
||||||
|
|
||||||
|
# Inbyggda inställningar (tema/panel/skugga/scrollbar).
|
||||||
|
# ≡ används i stället för ⚙ eftersom kugghjuls-emojin har tvetydig
|
||||||
|
# teckenbredd och förskjuter panelraden i vissa terminaler.
|
||||||
|
[[item]]
|
||||||
|
label = "≡ Inställningar"
|
||||||
|
action = { type = "spawn_settings" }
|
||||||
|
|
||||||
[[item]]
|
[[item]]
|
||||||
label = "Exit"
|
label = "Exit"
|
||||||
action = { type = "exit" }
|
action = { type = "exit" }
|
||||||
|
|||||||
172
src/app.rs
172
src/app.rs
@@ -301,6 +301,11 @@ pub enum WindowContent {
|
|||||||
input: String,
|
input: String,
|
||||||
cursor_pos: usize,
|
cursor_pos: usize,
|
||||||
},
|
},
|
||||||
|
/// Inbyggd inställningsdialog (tema, panel, skugga, scrollbar)
|
||||||
|
Settings {
|
||||||
|
/// Markerad rad (0=tema, 1=panel, 2=skugga, 3=scrollbar)
|
||||||
|
selected: usize,
|
||||||
|
},
|
||||||
PopupDialog {
|
PopupDialog {
|
||||||
message: String,
|
message: String,
|
||||||
buttons: Vec<String>,
|
buttons: Vec<String>,
|
||||||
@@ -709,6 +714,7 @@ impl App {
|
|||||||
WindowContent::Terminal { alive, .. } => *alive,
|
WindowContent::Terminal { alive, .. } => *alive,
|
||||||
WindowContent::RunDialog { .. } => true,
|
WindowContent::RunDialog { .. } => true,
|
||||||
WindowContent::PopupDialog { .. } => true,
|
WindowContent::PopupDialog { .. } => true,
|
||||||
|
WindowContent::Settings { .. } => true,
|
||||||
});
|
});
|
||||||
let alive_ids: Vec<usize> = self.windows.iter().map(|w| w.id).collect();
|
let alive_ids: Vec<usize> = self.windows.iter().map(|w| w.id).collect();
|
||||||
self.focus_history.retain(|h| alive_ids.contains(h));
|
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
|
// 2b. PopupDialog hanterar sina egna tangenter
|
||||||
if let Some(id) = self.focused_id {
|
if let Some(id) = self.focused_id {
|
||||||
let is_popup = self
|
let is_popup = self
|
||||||
@@ -1275,6 +1291,34 @@ impl App {
|
|||||||
return;
|
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
|
// 5. Innehållsyta → fokus + vidarebefordra musklick till terminal
|
||||||
let hit = self.windows.iter().rev().find(|w| w.in_content(col, row)).map(|w| {
|
let hit = self.windows.iter().rev().find(|w| w.in_content(col, row)).map(|w| {
|
||||||
let (mode, encoding) = match &w.content {
|
let (mode, encoding) = match &w.content {
|
||||||
@@ -1429,6 +1473,7 @@ impl App {
|
|||||||
MenuAction::Submenu { .. } => {}
|
MenuAction::Submenu { .. } => {}
|
||||||
MenuAction::WindowSwitcher => self.switcher_open_or_advance(),
|
MenuAction::WindowSwitcher => self.switcher_open_or_advance(),
|
||||||
MenuAction::CopyMode => self.enter_copy_mode(),
|
MenuAction::CopyMode => self.enter_copy_mode(),
|
||||||
|
MenuAction::SpawnSettings => self.spawn_settings(),
|
||||||
MenuAction::SnapLeft => self.snap_focused(true),
|
MenuAction::SnapLeft => self.snap_focused(true),
|
||||||
MenuAction::SnapRight => self.snap_focused(false),
|
MenuAction::SnapRight => self.snap_focused(false),
|
||||||
MenuAction::Maximize => self.toggle_maximize_focused(),
|
MenuAction::Maximize => self.toggle_maximize_focused(),
|
||||||
@@ -1657,6 +1702,132 @@ impl App {
|
|||||||
id
|
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.
|
/// Gå in i copy-mode för det fokuserade terminalfönstret.
|
||||||
fn enter_copy_mode(&mut self) {
|
fn enter_copy_mode(&mut self) {
|
||||||
let Some(id) = self.focused_id else { return };
|
let Some(id) = self.focused_id else { return };
|
||||||
@@ -2232,6 +2403,7 @@ fn action_display(action: &MenuAction) -> &'static str {
|
|||||||
MenuAction::SpawnRunDialog => "Kommandodialog",
|
MenuAction::SpawnRunDialog => "Kommandodialog",
|
||||||
MenuAction::WindowSwitcher => "Fönsterväxlare",
|
MenuAction::WindowSwitcher => "Fönsterväxlare",
|
||||||
MenuAction::CopyMode => "Copy-mode",
|
MenuAction::CopyMode => "Copy-mode",
|
||||||
|
MenuAction::SpawnSettings => "Inställningar",
|
||||||
MenuAction::SnapLeft => "Fäst vänster",
|
MenuAction::SnapLeft => "Fäst vänster",
|
||||||
MenuAction::SnapRight => "Fäst höger",
|
MenuAction::SnapRight => "Fäst höger",
|
||||||
MenuAction::Maximize => "Maximera/återställ",
|
MenuAction::Maximize => "Maximera/återställ",
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ pub enum MenuAction {
|
|||||||
Maximize,
|
Maximize,
|
||||||
/// Copy-mode: bläddra/söka i fokuserat fönsters historik
|
/// Copy-mode: bläddra/söka i fokuserat fönsters historik
|
||||||
CopyMode,
|
CopyMode,
|
||||||
|
/// Öppnar den inbyggda inställningsdialogen
|
||||||
|
SpawnSettings,
|
||||||
/// Gör ingenting – används för display-only rader i menyer
|
/// Gör ingenting – används för display-only rader i menyer
|
||||||
NoOp,
|
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)
|
/// Uppdatera background_image i config-filen (TOML)
|
||||||
pub fn save_background(path: &str, image_path: Option<&str>) -> anyhow::Result<()> {
|
pub fn save_background(path: &str, image_path: Option<&str>) -> anyhow::Result<()> {
|
||||||
let content = fs::read_to_string(path).unwrap_or_default();
|
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):
|
TANGENTER (standard, ändras via [[keybind]] i config.toml):
|
||||||
ctrl+space Kommandodialog — skriv t.ex. `htop` och tryck Enter
|
ctrl+space Kommandodialog — skriv t.ex. `htop` och tryck Enter
|
||||||
alt+enter Ny terminal
|
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)
|
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:
|
MUS:
|
||||||
Dra titelraden = flytta fönster Dra kant/hörn = ändra storlek
|
Dra titelraden = flytta fönster Dra kant/hörn = ändra storlek
|
||||||
Scroll = historik (skal) eller vidarebefordras (appar med mus-stöd)
|
Scroll = historik (skal) eller vidarebefordras (appar med mus-stöd)
|
||||||
@@ -491,6 +501,7 @@ fn handle_ipc_event(
|
|||||||
WindowContent::PopupDialog { message, .. } => {
|
WindowContent::PopupDialog { message, .. } => {
|
||||||
format!("[Popup: {}]", &message[..message.len().min(20)])
|
format!("[Popup: {}]", &message[..message.len().min(20)])
|
||||||
}
|
}
|
||||||
|
WindowContent::Settings { .. } => "[Inställningar]".to_string(),
|
||||||
};
|
};
|
||||||
Some(WindowInfo {
|
Some(WindowInfo {
|
||||||
id: w.id,
|
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::RunDialog { .. } => "Kommandodialog".to_string(),
|
||||||
WindowContent::PopupDialog { .. } => "Popup".to_string(),
|
WindowContent::PopupDialog { .. } => "Popup".to_string(),
|
||||||
|
WindowContent::Settings { .. } => "Inställningar".to_string(),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| format!("fönster {}", id))
|
.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::Terminal { title, .. } => format!(" {} ", title),
|
||||||
WindowContent::RunDialog { .. } => " Tui-run ".to_string(),
|
WindowContent::RunDialog { .. } => " Tui-run ".to_string(),
|
||||||
WindowContent::PopupDialog { .. } => " TUI-WM ".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
|
// 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 { .. } => {
|
WindowContent::PopupDialog { .. } => {
|
||||||
render_popup_dialog(frame, window, focused_id, th);
|
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
|
// 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(
|
fn render_popup_dialog(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
window: &FloatingWindow,
|
window: &FloatingWindow,
|
||||||
|
|||||||
@@ -348,6 +348,27 @@ def main():
|
|||||||
check("hot-reload plockar upp panel-ändring", "HOTRELOAD" in text,
|
check("hot-reload plockar upp panel-ändring", "HOTRELOAD" in text,
|
||||||
f"frame-tail: {text[:150]!r}")
|
f"frame-tail: {text[:150]!r}")
|
||||||
|
|
||||||
|
# ── 4h. Inbyggda inställningar via kugghjulet i panelen ──────────
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="Inställningar")
|
||||||
|
row0 = text[:100]
|
||||||
|
gx = row0.find("Inställningar")
|
||||||
|
check("kugghjulet syns i panelen", gx > 0)
|
||||||
|
disp.send(mouse_event({"Down": "Left"}, gx, 0))
|
||||||
|
disp.send(mouse_event({"Up": "Left"}, gx, 0))
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="Tema:")
|
||||||
|
check("inställningsdialogen öppnas från panelen", "Tema:" in text
|
||||||
|
and "Skugga" in text and "config.toml" in text)
|
||||||
|
# Ned två rader till Skugga och toggla av
|
||||||
|
disp.send(key_event(code="Down"))
|
||||||
|
disp.send(key_event(code="Down"))
|
||||||
|
disp.send(key_event(code="Enter"))
|
||||||
|
time.sleep(0.5)
|
||||||
|
cfg_now = open(os.path.join(cfgdir, "config.toml")).read()
|
||||||
|
check("skugg-toggle sparas till config.toml", "shadow = false" in cfg_now,
|
||||||
|
f"config innehåller: {[l for l in cfg_now.splitlines() if 'shadow' in l]!r}")
|
||||||
|
disp.send(key_event(code="Esc"))
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
# ── 5. Popup + popup_result via Enter ───────────────────────────
|
# ── 5. Popup + popup_result via Enter ───────────────────────────
|
||||||
app.send({
|
app.send({
|
||||||
"type": "spawn_popup", "message": "Integrationstest?",
|
"type": "spawn_popup", "message": "Integrationstest?",
|
||||||
|
|||||||
Reference in New Issue
Block a user