Themes as standalone config files + configurable shadow + dim unfocused + resize content-loss fix
- Theme is now a runtime struct loaded from its own TOML file (theme = "themes/dark.toml" in config.toml, like panels), all fields optional with built-in defaults; ships themes/dark.toml and themes/light.toml - shadow = true/false in config.toml, shadow_color per theme - dim_unfocused (per theme): unfocused window content is dimmed - BUGFIX: resizing a window no longer erases its content and history — the vt100 parser was recreated on every resize; now uses screen_mut().set_size() which preserves the grid and scrollback - New integration check: window content survives mouse resize (20/20) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
11
src/app.rs
11
src/app.rs
@@ -1,4 +1,5 @@
|
|||||||
use crate::config::{Config, KeybindScope, MenuAction, MenuItem, PanelPosition};
|
use crate::config::{Config, KeybindScope, MenuAction, MenuItem, PanelPosition};
|
||||||
|
use crate::theme::Theme;
|
||||||
use crate::pty::PtyTerminal;
|
use crate::pty::PtyTerminal;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind};
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -100,6 +101,8 @@ pub enum AppIpcOut {
|
|||||||
pub struct App {
|
pub struct App {
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
|
/// Aktivt tema (laddas från config.theme, hot-reloadas med config)
|
||||||
|
pub theme: Theme,
|
||||||
|
|
||||||
// Layout – uppdateras varje frame
|
// Layout – uppdateras varje frame
|
||||||
pub panel_rects: Vec<Rect>,
|
pub panel_rects: Vec<Rect>,
|
||||||
@@ -439,6 +442,8 @@ impl App {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let theme = Theme::load_or_default(config.theme.as_deref());
|
||||||
|
|
||||||
App {
|
App {
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
config,
|
config,
|
||||||
@@ -458,6 +463,7 @@ impl App {
|
|||||||
status_states,
|
status_states,
|
||||||
parsed_keybinds,
|
parsed_keybinds,
|
||||||
tui_wm_btn_rects: Vec::new(),
|
tui_wm_btn_rects: Vec::new(),
|
||||||
|
theme,
|
||||||
hovered_tui_btn: false,
|
hovered_tui_btn: false,
|
||||||
socket_path: None,
|
socket_path: None,
|
||||||
ipc_out: Vec::new(),
|
ipc_out: Vec::new(),
|
||||||
@@ -598,8 +604,9 @@ impl App {
|
|||||||
if cur_rows != new_rows || cur_cols != new_cols {
|
if cur_rows != new_rows || cur_cols != new_cols {
|
||||||
crate::log::log(&format!("PTY resize: {}x{} -> {}x{} (mouse mode was {:?})", cur_cols, cur_rows, new_cols, new_rows, window.mouse_mode));
|
crate::log::log(&format!("PTY resize: {}x{} -> {}x{} (mouse mode was {:?})", cur_cols, cur_rows, new_cols, new_rows, window.mouse_mode));
|
||||||
let _ = pty.resize(new_rows, new_cols);
|
let _ = pty.resize(new_rows, new_cols);
|
||||||
*parser = vt100::Parser::new(new_rows, new_cols, self.config.scrollback_lines);
|
// set_size behåller skärminnehåll och scrollback-historik —
|
||||||
// OBS: window.mouse_mode / window.mouse_encoding berörs inte — överlever restet.
|
// att återskapa parsern här raderade allt vid varje resize.
|
||||||
|
parser.screen_mut().set_size(new_rows, new_cols);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ pub struct Config {
|
|||||||
/// Standard-shell för nya terminaler. Om None → $SHELL eller /bin/bash.
|
/// Standard-shell för nya terminaler. Om None → $SHELL eller /bin/bash.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub default_shell: Option<String>,
|
pub default_shell: Option<String>,
|
||||||
|
/// Sökväg till temafil (t.ex. "themes/dark.toml"). Om None → inbyggt tema.
|
||||||
|
#[serde(default)]
|
||||||
|
pub theme: Option<String>,
|
||||||
|
/// Rita skugga under/till höger om fönster.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub shadow: bool,
|
||||||
/// Antal raders scrollback-historik per virtuell terminal.
|
/// Antal raders scrollback-historik per virtuell terminal.
|
||||||
#[serde(default = "default_scrollback_lines")]
|
#[serde(default = "default_scrollback_lines")]
|
||||||
pub scrollback_lines: usize,
|
pub scrollback_lines: usize,
|
||||||
@@ -207,6 +213,8 @@ impl Config {
|
|||||||
background_image: None,
|
background_image: None,
|
||||||
terminal_bg_color: None,
|
terminal_bg_color: None,
|
||||||
default_shell: None,
|
default_shell: None,
|
||||||
|
theme: None,
|
||||||
|
shadow: true,
|
||||||
scrollback_lines: default_scrollback_lines(),
|
scrollback_lines: default_scrollback_lines(),
|
||||||
show_scrollbar: default_true(),
|
show_scrollbar: default_true(),
|
||||||
default_window_size: default_window_size(),
|
default_window_size: default_window_size(),
|
||||||
|
|||||||
162
src/render.rs
162
src/render.rs
@@ -1,5 +1,4 @@
|
|||||||
use crate::app::{App, DropdownState, FloatingWindow, ResizeEdge, WindowContent};
|
use crate::app::{App, DropdownState, FloatingWindow, ResizeEdge, WindowContent};
|
||||||
use crate::theme;
|
|
||||||
use crate::config::StatusAlign;
|
use crate::config::StatusAlign;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
@@ -16,7 +15,7 @@ pub fn render(frame: &mut Frame, app: &App) {
|
|||||||
let bg_color = if app.config.background_image.is_some() {
|
let bg_color = if app.config.background_image.is_some() {
|
||||||
Color::Reset
|
Color::Reset
|
||||||
} else {
|
} else {
|
||||||
theme::DESKTOP_BG
|
app.theme.desktop_bg
|
||||||
};
|
};
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Block::default().style(Style::default().bg(bg_color)),
|
Block::default().style(Style::default().bg(bg_color)),
|
||||||
@@ -24,18 +23,8 @@ pub fn render(frame: &mut Frame, app: &App) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Flytande fönster – sista i listan renderas överst
|
// Flytande fönster – sista i listan renderas överst
|
||||||
let terminal_bg = app.config.terminal_bg_color.as_deref()
|
|
||||||
.and_then(parse_color)
|
|
||||||
.unwrap_or(Color::Reset);
|
|
||||||
for window in &app.windows {
|
for window in &app.windows {
|
||||||
let resize_hover = app
|
render_window(frame, window, app);
|
||||||
.hovered_resize
|
|
||||||
.filter(|(id, _)| *id == window.id)
|
|
||||||
.map(|(_, e)| e);
|
|
||||||
render_window(
|
|
||||||
frame, window, app.focused_id, app.hovered_window_close, resize_hover,
|
|
||||||
terminal_bg, bg_color, app.config.show_scrollbar,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paneler – alltid ovanpå fönster
|
// Paneler – alltid ovanpå fönster
|
||||||
@@ -55,13 +44,14 @@ pub fn render(frame: &mut Frame, app: &App) {
|
|||||||
status_states,
|
status_states,
|
||||||
app.focused_id.is_none(),
|
app.focused_id.is_none(),
|
||||||
app.hovered_tui_btn,
|
app.hovered_tui_btn,
|
||||||
|
&app.theme,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dropdown – allra överst
|
// Dropdown – allra överst
|
||||||
if let Some(dd) = &app.dropdown {
|
if let Some(dd) = &app.dropdown {
|
||||||
render_dropdown(frame, dd);
|
render_dropdown(frame, dd, &app.theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,18 +67,19 @@ fn render_panel(
|
|||||||
status_states: &[crate::app::StatusWidgetState],
|
status_states: &[crate::app::StatusWidgetState],
|
||||||
desktop_focused: bool,
|
desktop_focused: bool,
|
||||||
hovered_tui: bool,
|
hovered_tui: bool,
|
||||||
|
th: &crate::theme::Theme,
|
||||||
) {
|
) {
|
||||||
// Panelens bakgrund
|
// Panelens bakgrund
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Block::default().style(Style::default().bg(theme::PANEL_BG)),
|
Block::default().style(Style::default().bg(th.panel_bg)),
|
||||||
rect,
|
rect,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Logotyp – klickbar meny-knapp med hover-highlight
|
// Logotyp – klickbar meny-knapp med hover-highlight
|
||||||
let logo_style = if hovered_tui {
|
let logo_style = if hovered_tui {
|
||||||
theme::hover_highlight()
|
th.hover_highlight()
|
||||||
} else {
|
} else {
|
||||||
theme::logo()
|
th.logo()
|
||||||
};
|
};
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled(" TUI-WM ", logo_style)),
|
Paragraph::new(Span::styled(" TUI-WM ", logo_style)),
|
||||||
@@ -96,9 +87,9 @@ fn render_panel(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fokus-indikator: grön ● = skrivbord fokuserat, grå ● = fönster fokuserat
|
// Fokus-indikator: grön ● = skrivbord fokuserat, grå ● = fönster fokuserat
|
||||||
let dot_color = if desktop_focused { theme::OK } else { theme::HINT };
|
let dot_color = if desktop_focused { th.ok } else { th.hint };
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled("●", Style::default().fg(dot_color).bg(theme::PANEL_BG))),
|
Paragraph::new(Span::styled("●", Style::default().fg(dot_color).bg(th.panel_bg))),
|
||||||
Rect::new(rect.x + 10, rect.y, 1, 1),
|
Rect::new(rect.x + 10, rect.y, 1, 1),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,9 +98,9 @@ fn render_panel(
|
|||||||
let Some(&ir) = item_rects.get(ii) else { continue };
|
let Some(&ir) = item_rects.get(ii) else { continue };
|
||||||
let is_hovered = hovered == Some((panel_idx, ii));
|
let is_hovered = hovered == Some((panel_idx, ii));
|
||||||
let style = if is_hovered {
|
let style = if is_hovered {
|
||||||
theme::hover_highlight()
|
th.hover_highlight()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme::TEXT).bg(theme::PANEL_BG)
|
Style::default().fg(th.text).bg(th.panel_bg)
|
||||||
};
|
};
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled(format!(" {} ", item.label), style)),
|
Paragraph::new(Span::styled(format!(" {} ", item.label), style)),
|
||||||
@@ -135,7 +126,7 @@ fn render_panel(
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled(
|
Paragraph::new(Span::styled(
|
||||||
padded,
|
padded,
|
||||||
theme::panel_item(),
|
th.panel_item(),
|
||||||
)),
|
)),
|
||||||
draw_rect,
|
draw_rect,
|
||||||
);
|
);
|
||||||
@@ -154,7 +145,7 @@ fn render_panel(
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled(
|
Paragraph::new(Span::styled(
|
||||||
padded,
|
padded,
|
||||||
theme::panel_item(),
|
th.panel_item(),
|
||||||
)),
|
)),
|
||||||
draw_rect,
|
draw_rect,
|
||||||
);
|
);
|
||||||
@@ -162,23 +153,23 @@ fn render_panel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_dropdown(frame: &mut Frame, dd: &DropdownState) {
|
fn render_dropdown(frame: &mut Frame, dd: &DropdownState, th: &crate::theme::Theme) {
|
||||||
let border_rect = Rect::new(dd.rect.x, dd.rect.y, dd.rect.width, dd.rect.height + 2);
|
let border_rect = Rect::new(dd.rect.x, dd.rect.y, dd.rect.width, dd.rect.height + 2);
|
||||||
frame.render_widget(Clear, border_rect);
|
frame.render_widget(Clear, border_rect);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(Style::default().fg(theme::ACCENT))
|
.border_style(Style::default().fg(th.accent))
|
||||||
.style(Style::default().bg(theme::MENU_BG)),
|
.style(Style::default().bg(th.menu_bg)),
|
||||||
border_rect,
|
border_rect,
|
||||||
);
|
);
|
||||||
for (i, (item, &ir)) in dd.items.iter().zip(dd.item_rects.iter()).enumerate() {
|
for (i, (item, &ir)) in dd.items.iter().zip(dd.item_rects.iter()).enumerate() {
|
||||||
let is_hovered = dd.hovered == Some(i);
|
let is_hovered = dd.hovered == Some(i);
|
||||||
let style = if is_hovered {
|
let style = if is_hovered {
|
||||||
theme::hover_highlight()
|
th.hover_highlight()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme::TEXT).bg(theme::MENU_BG)
|
Style::default().fg(th.text).bg(th.menu_bg)
|
||||||
};
|
};
|
||||||
// ir pekar redan på den faktiska raden innanför kanten
|
// ir pekar redan på den faktiska raden innanför kanten
|
||||||
let draw_rect = Rect::new(ir.x + 1, ir.y, ir.width.saturating_sub(2), 1);
|
let draw_rect = Rect::new(ir.x + 1, ir.y, ir.width.saturating_sub(2), 1);
|
||||||
@@ -189,17 +180,26 @@ fn render_dropdown(frame: &mut Frame, dd: &DropdownState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
fn render_window(frame: &mut Frame, window: &FloatingWindow, app: &App) {
|
||||||
fn render_window(
|
let th = &app.theme;
|
||||||
frame: &mut Frame,
|
let focused_id = app.focused_id;
|
||||||
window: &FloatingWindow,
|
let hovered_close = app.hovered_window_close;
|
||||||
focused_id: Option<usize>,
|
let resize_hover = app
|
||||||
hovered_close: Option<usize>,
|
.hovered_resize
|
||||||
resize_hover: Option<ResizeEdge>,
|
.filter(|(id, _)| *id == window.id)
|
||||||
terminal_bg: Color,
|
.map(|(_, e)| e);
|
||||||
outer_bg: Color,
|
let terminal_bg = app
|
||||||
show_scrollbar: bool,
|
.config
|
||||||
) {
|
.terminal_bg_color
|
||||||
|
.as_deref()
|
||||||
|
.and_then(crate::theme::parse_color)
|
||||||
|
.unwrap_or(Color::Reset);
|
||||||
|
let outer_bg = if app.config.background_image.is_some() {
|
||||||
|
Color::Reset
|
||||||
|
} else {
|
||||||
|
th.desktop_bg
|
||||||
|
};
|
||||||
|
let show_scrollbar = app.config.show_scrollbar;
|
||||||
let full_rect = window.rect().intersection(frame.area());
|
let full_rect = window.rect().intersection(frame.area());
|
||||||
if full_rect.width < 4 || full_rect.height < 2 {
|
if full_rect.width < 4 || full_rect.height < 2 {
|
||||||
return;
|
return;
|
||||||
@@ -208,7 +208,7 @@ fn render_window(
|
|||||||
let is_focused = focused_id == Some(window.id);
|
let is_focused = focused_id == Some(window.id);
|
||||||
let is_resizing = window.resizing.is_some();
|
let is_resizing = window.resizing.is_some();
|
||||||
|
|
||||||
let border_style = theme::window_border(is_focused, is_resizing, resize_hover.is_some());
|
let border_style = th.window_border(is_focused, is_resizing, resize_hover.is_some());
|
||||||
|
|
||||||
let title_text = match &window.content {
|
let title_text = match &window.content {
|
||||||
WindowContent::Terminal { title, .. } => format!(" {} ", title),
|
WindowContent::Terminal { title, .. } => format!(" {} ", title),
|
||||||
@@ -218,8 +218,8 @@ fn render_window(
|
|||||||
|
|
||||||
// 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
|
||||||
// bakgrundsbild används — då ska skrivbordet synas igenom).
|
// bakgrundsbild används — då ska skrivbordet synas igenom).
|
||||||
if outer_bg != Color::Reset {
|
if app.config.shadow && outer_bg != Color::Reset {
|
||||||
render_shadow(frame, full_rect);
|
render_shadow(frame, full_rect, th.shadow_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.render_widget(Clear, full_rect);
|
frame.render_widget(Clear, full_rect);
|
||||||
@@ -242,7 +242,7 @@ fn render_window(
|
|||||||
// Titeln renderas som ett "chip" infällt i ramen: ┤ titel ├
|
// Titeln renderas som ett "chip" infällt i ramen: ┤ titel ├
|
||||||
let title_line = Line::from(vec![
|
let title_line = Line::from(vec![
|
||||||
Span::styled("┤", border_style),
|
Span::styled("┤", border_style),
|
||||||
Span::styled(title_text.clone(), theme::window_title(is_focused)),
|
Span::styled(title_text.clone(), th.window_title(is_focused)),
|
||||||
Span::styled("├", border_style),
|
Span::styled("├", border_style),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -259,9 +259,9 @@ fn render_window(
|
|||||||
if cb.x + 3 <= full_rect.x + full_rect.width {
|
if cb.x + 3 <= full_rect.x + full_rect.width {
|
||||||
let close_hov = hovered_close == Some(window.id);
|
let close_hov = hovered_close == Some(window.id);
|
||||||
let close_style = if close_hov {
|
let close_style = if close_hov {
|
||||||
Style::default().fg(theme::TEXT).bg(theme::DANGER).add_modifier(Modifier::BOLD)
|
Style::default().fg(th.text).bg(th.danger).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme::DANGER)
|
Style::default().fg(th.danger)
|
||||||
};
|
};
|
||||||
let close_line = Line::from(vec![
|
let close_line = Line::from(vec![
|
||||||
Span::styled("┤", border_style),
|
Span::styled("┤", border_style),
|
||||||
@@ -284,22 +284,23 @@ fn render_window(
|
|||||||
match &window.content {
|
match &window.content {
|
||||||
WindowContent::Terminal { parser, alive, .. } => {
|
WindowContent::Terminal { parser, alive, .. } => {
|
||||||
if *alive {
|
if *alive {
|
||||||
render_terminal(frame, parser.screen(), cr, window.selection.as_ref(), cr, terminal_bg);
|
let dim = th.dim_unfocused && !is_focused;
|
||||||
|
render_terminal(frame, parser.screen(), cr, window.selection.as_ref(), cr, terminal_bg, dim);
|
||||||
} else {
|
} else {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled(
|
Paragraph::new(Span::styled(
|
||||||
" [Avslutad] ",
|
" [Avslutad] ",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(th.hint),
|
||||||
)),
|
)),
|
||||||
cr,
|
cr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowContent::RunDialog { input, cursor_pos } => {
|
WindowContent::RunDialog { input, cursor_pos } => {
|
||||||
render_run_dialog_content(frame, input, *cursor_pos, cr);
|
render_run_dialog_content(frame, input, *cursor_pos, cr, th);
|
||||||
}
|
}
|
||||||
WindowContent::PopupDialog { .. } => {
|
WindowContent::PopupDialog { .. } => {
|
||||||
render_popup_dialog(frame, window, focused_id);
|
render_popup_dialog(frame, window, focused_id, th);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +317,7 @@ fn render_window(
|
|||||||
.track_symbol(Some("│"))
|
.track_symbol(Some("│"))
|
||||||
.thumb_symbol("█")
|
.thumb_symbol("█")
|
||||||
.track_style(border_style)
|
.track_style(border_style)
|
||||||
.thumb_style(Style::default().fg(theme::ACCENT));
|
.thumb_style(Style::default().fg(th.accent));
|
||||||
frame.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
sb,
|
sb,
|
||||||
full_rect.inner(ratatui::layout::Margin { vertical: 1, horizontal: 0 }),
|
full_rect.inner(ratatui::layout::Margin { vertical: 1, horizontal: 0 }),
|
||||||
@@ -330,13 +331,13 @@ fn render_window(
|
|||||||
/// mörkare bakgrund + dämpad text, så fönstren ser ut att sväva över
|
/// mörkare bakgrund + dämpad text, så fönstren ser ut att sväva över
|
||||||
/// skrivbordet. Muterar cellerna direkt så att underliggande innehåll
|
/// skrivbordet. Muterar cellerna direkt så att underliggande innehåll
|
||||||
/// (andra fönster/skrivbordet) syns kvar — bara mörkare.
|
/// (andra fönster/skrivbordet) syns kvar — bara mörkare.
|
||||||
fn render_shadow(frame: &mut Frame, rect: Rect) {
|
fn render_shadow(frame: &mut Frame, rect: Rect, shadow_bg: Color) {
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
let buf = frame.buffer_mut();
|
let buf = frame.buffer_mut();
|
||||||
let mut shade = |x: u16, y: u16| {
|
let mut shade = |x: u16, y: u16| {
|
||||||
if x < area.width && y < area.height {
|
if x < area.width && y < area.height {
|
||||||
if let Some(cell) = buf.cell_mut((x, y)) {
|
if let Some(cell) = buf.cell_mut((x, y)) {
|
||||||
cell.set_bg(theme::SHADOW_BG);
|
cell.set_bg(shadow_bg);
|
||||||
cell.modifier.insert(Modifier::DIM);
|
cell.modifier.insert(Modifier::DIM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,12 +356,13 @@ fn render_popup_dialog(
|
|||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
window: &FloatingWindow,
|
window: &FloatingWindow,
|
||||||
focused_id: Option<usize>,
|
focused_id: Option<usize>,
|
||||||
|
th: &crate::theme::Theme,
|
||||||
) {
|
) {
|
||||||
let is_focused = focused_id == Some(window.id);
|
let is_focused = focused_id == Some(window.id);
|
||||||
let border_style = if is_focused {
|
let border_style = if is_focused {
|
||||||
Style::default().fg(theme::ACCENT)
|
Style::default().fg(th.accent)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme::HINT)
|
Style::default().fg(th.hint)
|
||||||
};
|
};
|
||||||
|
|
||||||
let WindowContent::PopupDialog { message, buttons, selected, .. } = &window.content else {
|
let WindowContent::PopupDialog { message, buttons, selected, .. } = &window.content else {
|
||||||
@@ -374,8 +376,8 @@ fn render_popup_dialog(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(border_style)
|
.border_style(border_style)
|
||||||
.title(Span::styled(" TUI-WM ", theme::logo()))
|
.title(Span::styled(" TUI-WM ", th.logo()))
|
||||||
.style(Style::default().bg(theme::DESKTOP_BG)),
|
.style(Style::default().bg(th.desktop_bg)),
|
||||||
rect,
|
rect,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -386,7 +388,7 @@ fn render_popup_dialog(
|
|||||||
Paragraph::new(message.as_str())
|
Paragraph::new(message.as_str())
|
||||||
.wrap(Wrap { trim: true })
|
.wrap(Wrap { trim: true })
|
||||||
.alignment(ratatui::layout::Alignment::Center)
|
.alignment(ratatui::layout::Alignment::Center)
|
||||||
.style(Style::default().fg(Color::White).bg(Color::Indexed(235))),
|
.style(Style::default().fg(th.text).bg(th.desktop_bg)),
|
||||||
Rect::new(content.x, content.y, content.width, msg_h),
|
Rect::new(content.x, content.y, content.width, msg_h),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -401,9 +403,9 @@ fn render_popup_dialog(
|
|||||||
for (i, label) in labels.iter().enumerate() {
|
for (i, label) in labels.iter().enumerate() {
|
||||||
let w = label.len() as u16;
|
let w = label.len() as u16;
|
||||||
let style = if i == *selected {
|
let style = if i == *selected {
|
||||||
theme::hover_highlight()
|
th.hover_highlight()
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme::TEXT).bg(theme::PANEL_BG)
|
Style::default().fg(th.text).bg(th.panel_bg)
|
||||||
};
|
};
|
||||||
if btn_x + w <= rect.x + rect.width {
|
if btn_x + w <= rect.x + rect.width {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -448,7 +450,7 @@ fn render_resize_hints(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_run_dialog_content(frame: &mut Frame, input: &str, cursor_pos: usize, area: Rect) {
|
fn render_run_dialog_content(frame: &mut Frame, input: &str, cursor_pos: usize, area: Rect, th: &crate::theme::Theme) {
|
||||||
if area.height == 0 {
|
if area.height == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -465,10 +467,10 @@ fn render_run_dialog_content(frame: &mut Frame, input: &str, cursor_pos: usize,
|
|||||||
};
|
};
|
||||||
|
|
||||||
let input_line = Line::from(vec![
|
let input_line = Line::from(vec![
|
||||||
Span::styled(prefix, Style::default().fg(Color::Cyan)),
|
Span::styled(prefix, Style::default().fg(th.accent)),
|
||||||
Span::styled(before_cursor, Style::default().fg(Color::White)),
|
Span::styled(before_cursor, Style::default().fg(th.text)),
|
||||||
Span::styled(&at_cursor, Style::default().fg(Color::Black).bg(Color::White).add_modifier(Modifier::BOLD)),
|
Span::styled(&at_cursor, Style::default().fg(Color::Black).bg(Color::White).add_modifier(Modifier::BOLD)),
|
||||||
Span::styled(after_cursor, Style::default().fg(Color::White)),
|
Span::styled(after_cursor, Style::default().fg(th.text)),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(input_line),
|
Paragraph::new(input_line),
|
||||||
@@ -481,13 +483,14 @@ fn render_run_dialog_content(frame: &mut Frame, input: &str, cursor_pos: usize,
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Span::styled(
|
Paragraph::new(Span::styled(
|
||||||
" Enter=Kör Esc=Avbryt",
|
" Enter=Kör Esc=Avbryt",
|
||||||
Style::default().fg(Color::Indexed(242)),
|
Style::default().fg(th.border_dim),
|
||||||
)),
|
)),
|
||||||
Rect::new(area.x, area.y + 2, area.width, 1),
|
Rect::new(area.x, area.y + 2, area.width, 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_terminal(
|
fn render_terminal(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
screen: &vt100::Screen,
|
screen: &vt100::Screen,
|
||||||
@@ -495,6 +498,7 @@ fn render_terminal(
|
|||||||
selection: Option<&crate::app::Selection>,
|
selection: Option<&crate::app::Selection>,
|
||||||
_content_rect: Rect,
|
_content_rect: Rect,
|
||||||
terminal_bg: Color,
|
terminal_bg: Color,
|
||||||
|
dim: bool,
|
||||||
) {
|
) {
|
||||||
let (screen_rows, screen_cols) = screen.size();
|
let (screen_rows, screen_cols) = screen.size();
|
||||||
let rows = (area.height as usize).min(screen_rows as usize);
|
let rows = (area.height as usize).min(screen_rows as usize);
|
||||||
@@ -541,6 +545,9 @@ fn render_terminal(
|
|||||||
if cell.underline() {
|
if cell.underline() {
|
||||||
st = st.add_modifier(Modifier::UNDERLINED);
|
st = st.add_modifier(Modifier::UNDERLINED);
|
||||||
}
|
}
|
||||||
|
if dim {
|
||||||
|
st = st.add_modifier(Modifier::DIM);
|
||||||
|
}
|
||||||
if is_selected {
|
if is_selected {
|
||||||
st = Style::default()
|
st = Style::default()
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
@@ -590,29 +597,4 @@ fn vt_color(c: vt100::Color) -> Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_color(s: &str) -> Option<Color> {
|
|
||||||
let s = s.trim();
|
|
||||||
if let Some(hex) = s.strip_prefix('#') {
|
|
||||||
if hex.len() == 6 {
|
|
||||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
|
||||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
|
||||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
|
||||||
return Some(Color::Rgb(r, g, b));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Ok(n) = s.parse::<u8>() {
|
|
||||||
return Some(Color::Indexed(n));
|
|
||||||
}
|
|
||||||
match s.to_lowercase().as_str() {
|
|
||||||
"black" => Some(Color::Black),
|
|
||||||
"red" => Some(Color::Red),
|
|
||||||
"green" => Some(Color::Green),
|
|
||||||
"yellow" => Some(Color::Yellow),
|
|
||||||
"blue" => Some(Color::Blue),
|
|
||||||
"magenta" => Some(Color::Magenta),
|
|
||||||
"cyan" => Some(Color::Cyan),
|
|
||||||
"white" => Some(Color::White),
|
|
||||||
"reset" | "transparent" => Some(Color::Reset),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
279
src/theme.rs
279
src/theme.rs
@@ -1,77 +1,240 @@
|
|||||||
//! Centralt tema för TUI-WM.
|
//! Tema för TUI-WM.
|
||||||
//!
|
//!
|
||||||
//! ALLA färger och återkommande stilar definieras här — ändra utseendet
|
//! Ett tema definieras i en egen TOML-fil (t.ex. `themes/dark.toml`) och
|
||||||
//! på hela fönsterhanteraren från en enda fil. render.rs ska inte
|
//! väljs i `config.toml` med `theme = "themes/dark.toml"` — precis som
|
||||||
//! hårdkoda `Color::`-literaler för sådant som temat täcker.
|
//! paneler pekar på sina egna filer. Alla fält är valfria; utelämnade
|
||||||
|
//! fält faller tillbaka på standardtemat.
|
||||||
|
//!
|
||||||
|
//! Färger anges som strängar: färgnamn ("cyan"), index ("235") eller
|
||||||
|
//! hex ("#1e1e2e").
|
||||||
|
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
// ─── Palett ──────────────────────────────────────────────────────────────
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
/// Fokusmarkering (fönsterram, logga, markerade menyrader)
|
pub struct Theme {
|
||||||
pub const ACCENT: Color = Color::Cyan;
|
/// Fokusmarkering (fönsterram, logga, markerade menyrader)
|
||||||
/// Pågående storleksändring
|
pub accent: Color,
|
||||||
pub const RESIZE: Color = Color::Yellow;
|
/// Pågående storleksändring
|
||||||
/// Hover över fönsterkant (inför resize)
|
pub resize: Color,
|
||||||
pub const RESIZE_HOVER: Color = Color::Indexed(220);
|
/// Hover över fönsterkant (inför resize)
|
||||||
/// Ofokuserad fönsterram
|
pub resize_hover: Color,
|
||||||
pub const BORDER_DIM: Color = Color::Indexed(242);
|
/// Ofokuserad fönsterram
|
||||||
/// Skrivbordsbakgrund (när ingen bakgrundsbild används)
|
pub border_dim: Color,
|
||||||
pub const DESKTOP_BG: Color = Color::Indexed(235);
|
/// Skrivbordsbakgrund (när ingen bakgrundsbild används)
|
||||||
/// Panelens bakgrund
|
pub desktop_bg: Color,
|
||||||
pub const PANEL_BG: Color = Color::Indexed(238);
|
/// Panelens bakgrund
|
||||||
/// Dropdown-menyns bakgrund
|
pub panel_bg: Color,
|
||||||
pub const MENU_BG: Color = Color::Indexed(236);
|
/// Dropdown-menyns bakgrund
|
||||||
/// Normal text
|
pub menu_bg: Color,
|
||||||
pub const TEXT: Color = Color::White;
|
/// Normal text
|
||||||
/// Panel-text (något nedtonad)
|
pub text: Color,
|
||||||
pub const PANEL_TEXT: Color = Color::Indexed(252);
|
/// Panel-text (något nedtonad)
|
||||||
/// Hints och inaktiva indikatorer
|
pub panel_text: Color,
|
||||||
pub const HINT: Color = Color::Indexed(240);
|
/// Hints och inaktiva indikatorer
|
||||||
/// Positiv indikator (skrivbordsfokus-punkten)
|
pub hint: Color,
|
||||||
pub const OK: Color = Color::Green;
|
/// Positiv indikator (skrivbordsfokus-punkten)
|
||||||
/// Destruktivt (stäng-knappen)
|
pub ok: Color,
|
||||||
pub const DANGER: Color = Color::Red;
|
/// Destruktivt (stäng-knappen)
|
||||||
|
pub danger: Color,
|
||||||
|
/// Fönsterskuggans bakgrundsfärg
|
||||||
|
pub shadow_color: Color,
|
||||||
|
/// Dämpa innehållet i ofokuserade fönster
|
||||||
|
pub dim_unfocused: bool,
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Stilar ──────────────────────────────────────────────────────────────
|
impl Default for Theme {
|
||||||
/// Ramstil för ett fönster utifrån dess tillstånd.
|
fn default() -> Self {
|
||||||
pub fn window_border(focused: bool, resizing: bool, resize_hover: bool) -> Style {
|
Theme {
|
||||||
if resizing {
|
accent: Color::Cyan,
|
||||||
Style::default().fg(RESIZE)
|
resize: Color::Yellow,
|
||||||
} else if resize_hover {
|
resize_hover: Color::Indexed(220),
|
||||||
Style::default().fg(RESIZE_HOVER)
|
border_dim: Color::Indexed(242),
|
||||||
} else if focused {
|
desktop_bg: Color::Indexed(235),
|
||||||
Style::default().fg(ACCENT)
|
panel_bg: Color::Indexed(238),
|
||||||
} else {
|
menu_bg: Color::Indexed(236),
|
||||||
Style::default().fg(BORDER_DIM)
|
text: Color::White,
|
||||||
|
panel_text: Color::Indexed(252),
|
||||||
|
hint: Color::Indexed(240),
|
||||||
|
ok: Color::Green,
|
||||||
|
danger: Color::Red,
|
||||||
|
shadow_color: Color::Indexed(233),
|
||||||
|
dim_unfocused: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Markerad rad/knapp (hover): inverterad accent.
|
/// Rå temafil — alla fält valfria strängar som parsas till färger.
|
||||||
pub fn hover_highlight() -> Style {
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct ThemeFile {
|
||||||
|
pub accent: Option<String>,
|
||||||
|
pub resize: Option<String>,
|
||||||
|
pub resize_hover: Option<String>,
|
||||||
|
pub border_dim: Option<String>,
|
||||||
|
pub desktop_bg: Option<String>,
|
||||||
|
pub panel_bg: Option<String>,
|
||||||
|
pub menu_bg: Option<String>,
|
||||||
|
pub text: Option<String>,
|
||||||
|
pub panel_text: Option<String>,
|
||||||
|
pub hint: Option<String>,
|
||||||
|
pub ok: Option<String>,
|
||||||
|
pub danger: Option<String>,
|
||||||
|
pub shadow_color: Option<String>,
|
||||||
|
pub dim_unfocused: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
/// Ladda ett tema från fil. Fält som saknas (eller inte kan parsas)
|
||||||
|
/// behåller standardtemats värde.
|
||||||
|
pub fn load(path: &str) -> anyhow::Result<Theme> {
|
||||||
|
let content = std::fs::read_to_string(path)?;
|
||||||
|
let file: ThemeFile = toml::from_str(&content)?;
|
||||||
|
Ok(Theme::from_file(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_file(f: ThemeFile) -> Theme {
|
||||||
|
let d = Theme::default();
|
||||||
|
let c = |s: Option<String>, fallback: Color| {
|
||||||
|
s.as_deref().and_then(parse_color).unwrap_or(fallback)
|
||||||
|
};
|
||||||
|
Theme {
|
||||||
|
accent: c(f.accent, d.accent),
|
||||||
|
resize: c(f.resize, d.resize),
|
||||||
|
resize_hover: c(f.resize_hover, d.resize_hover),
|
||||||
|
border_dim: c(f.border_dim, d.border_dim),
|
||||||
|
desktop_bg: c(f.desktop_bg, d.desktop_bg),
|
||||||
|
panel_bg: c(f.panel_bg, d.panel_bg),
|
||||||
|
menu_bg: c(f.menu_bg, d.menu_bg),
|
||||||
|
text: c(f.text, d.text),
|
||||||
|
panel_text: c(f.panel_text, d.panel_text),
|
||||||
|
hint: c(f.hint, d.hint),
|
||||||
|
ok: c(f.ok, d.ok),
|
||||||
|
danger: c(f.danger, d.danger),
|
||||||
|
shadow_color: c(f.shadow_color, d.shadow_color),
|
||||||
|
dim_unfocused: f.dim_unfocused.unwrap_or(d.dim_unfocused),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ladda temat som konfigurationen pekar på, annars standardtemat.
|
||||||
|
pub fn load_or_default(path: Option<&str>) -> Theme {
|
||||||
|
match path {
|
||||||
|
None => Theme::default(),
|
||||||
|
Some(p) => match Theme::load(p) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Varning: kunde inte ladda tema {}: {}", p, e);
|
||||||
|
Theme::default()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stilar ───────────────────────────────────────────────────────────
|
||||||
|
/// Ramstil för ett fönster utifrån dess tillstånd.
|
||||||
|
pub fn window_border(&self, focused: bool, resizing: bool, resize_hover: bool) -> Style {
|
||||||
|
if resizing {
|
||||||
|
Style::default().fg(self.resize)
|
||||||
|
} else if resize_hover {
|
||||||
|
Style::default().fg(self.resize_hover)
|
||||||
|
} else if focused {
|
||||||
|
Style::default().fg(self.accent)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(self.border_dim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Markerad rad/knapp (hover): inverterad accent.
|
||||||
|
pub fn hover_highlight(&self) -> Style {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
.bg(ACCENT)
|
.bg(self.accent)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TUI-WM-loggan i panelen.
|
/// TUI-WM-loggan i panelen.
|
||||||
pub fn logo() -> Style {
|
pub fn logo(&self) -> Style {
|
||||||
Style::default().fg(ACCENT).add_modifier(Modifier::BOLD)
|
Style::default().fg(self.accent).add_modifier(Modifier::BOLD)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Panel-objekt (menyknappar) i normalläge.
|
/// Panel-objekt (menyknappar, status-widgets) i normalläge.
|
||||||
pub fn panel_item() -> Style {
|
pub fn panel_item(&self) -> Style {
|
||||||
Style::default().fg(PANEL_TEXT).bg(PANEL_BG)
|
Style::default().fg(self.panel_text).bg(self.panel_bg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fönstertitel — accentfärgad när fönstret har fokus.
|
/// Fönstertitel — accentfärgad när fönstret har fokus.
|
||||||
pub fn window_title(focused: bool) -> Style {
|
pub fn window_title(&self, focused: bool) -> Style {
|
||||||
if focused {
|
if focused {
|
||||||
Style::default().fg(ACCENT).add_modifier(Modifier::BOLD)
|
Style::default().fg(self.accent).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(HINT)
|
Style::default().fg(self.hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bakgrundsfärg för fönsterskuggan (mörkare än skrivbordet).
|
/// Tolka en färgsträng: färgnamn, "0"–"255" (indexed) eller "#RRGGBB".
|
||||||
pub const SHADOW_BG: Color = Color::Indexed(233);
|
pub fn parse_color(s: &str) -> Option<Color> {
|
||||||
|
let s = s.trim();
|
||||||
|
if let Some(hex) = s.strip_prefix('#') {
|
||||||
|
if hex.len() == 6 {
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||||
|
return Some(Color::Rgb(r, g, b));
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Ok(idx) = s.parse::<u8>() {
|
||||||
|
return Some(Color::Indexed(idx));
|
||||||
|
}
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"black" => Some(Color::Black),
|
||||||
|
"red" => Some(Color::Red),
|
||||||
|
"green" => Some(Color::Green),
|
||||||
|
"yellow" => Some(Color::Yellow),
|
||||||
|
"blue" => Some(Color::Blue),
|
||||||
|
"magenta" => Some(Color::Magenta),
|
||||||
|
"cyan" => Some(Color::Cyan),
|
||||||
|
"gray" | "grey" => Some(Color::Gray),
|
||||||
|
"darkgray" | "darkgrey" => Some(Color::DarkGray),
|
||||||
|
"lightred" => Some(Color::LightRed),
|
||||||
|
"lightgreen" => Some(Color::LightGreen),
|
||||||
|
"lightyellow" => Some(Color::LightYellow),
|
||||||
|
"lightblue" => Some(Color::LightBlue),
|
||||||
|
"lightmagenta" => Some(Color::LightMagenta),
|
||||||
|
"lightcyan" => Some(Color::LightCyan),
|
||||||
|
"white" => Some(Color::White),
|
||||||
|
"reset" | "transparent" => Some(Color::Reset),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn theme_file_overrides_defaults() {
|
||||||
|
let f: ThemeFile = toml::from_str(
|
||||||
|
r##"
|
||||||
|
accent = "#ff0000"
|
||||||
|
desktop_bg = "17"
|
||||||
|
dim_unfocused = false
|
||||||
|
"##,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let t = Theme::from_file(f);
|
||||||
|
assert_eq!(t.accent, Color::Rgb(255, 0, 0));
|
||||||
|
assert_eq!(t.desktop_bg, Color::Indexed(17));
|
||||||
|
assert!(!t.dim_unfocused);
|
||||||
|
// Ej satta fält behåller default
|
||||||
|
assert_eq!(t.panel_bg, Theme::default().panel_bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_color_variants() {
|
||||||
|
assert_eq!(parse_color("cyan"), Some(Color::Cyan));
|
||||||
|
assert_eq!(parse_color("235"), Some(Color::Indexed(235)));
|
||||||
|
assert_eq!(parse_color("#1e1e2e"), Some(Color::Rgb(0x1e, 0x1e, 0x2e)));
|
||||||
|
assert_eq!(parse_color("blorp"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -213,6 +213,27 @@ def main():
|
|||||||
f"frame-text: {text[-200:]!r}")
|
f"frame-text: {text[-200:]!r}")
|
||||||
app.send({"type": "close_window", "window_id": emoji_open["id"]})
|
app.send({"type": "close_window", "window_id": emoji_open["id"]})
|
||||||
|
|
||||||
|
# ── 4b2. Resize får inte radera fönstrets innehåll ───────────────
|
||||||
|
app.send({"type": "spawn_window",
|
||||||
|
"command": "printf 'RESIZE-KEEP\\n'; exec cat",
|
||||||
|
"request_id": "w-resize"})
|
||||||
|
rz_open = app.recv_until("window_opened")
|
||||||
|
disp.frame_text(timeout=5.0, contains="RESIZE-KEEP")
|
||||||
|
app.send({"type": "list_windows", "request_id": "lr"})
|
||||||
|
wlr = app.recv_until("window_list")
|
||||||
|
rw = next(w for w in wlr["windows"] if w["id"] == rz_open["id"])
|
||||||
|
# dra i nedre högra hörnet: krymp fönstret 4 kolumner/2 rader
|
||||||
|
corner_x = rw["x"] + rw["width"] - 1
|
||||||
|
corner_y = rw["y"] + rw["height"] - 1
|
||||||
|
disp.send(mouse_event({"Down": "Left"}, corner_x, corner_y))
|
||||||
|
disp.send(mouse_event({"Drag": "Left"}, corner_x - 4, corner_y - 2))
|
||||||
|
disp.send(mouse_event({"Up": "Left"}, corner_x - 4, corner_y - 2))
|
||||||
|
time.sleep(0.5)
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="RESIZE-KEEP")
|
||||||
|
check("innehållet överlever fönster-resize", "RESIZE-KEEP" in text,
|
||||||
|
f"frame-tail: {text[-200:]!r}")
|
||||||
|
app.send({"type": "close_window", "window_id": rz_open["id"]})
|
||||||
|
|
||||||
# ── 4c. Scrollback: mushjul utan mus-tracking scrollar historiken ─
|
# ── 4c. Scrollback: mushjul utan mus-tracking scrollar historiken ─
|
||||||
app.send({"type": "spawn_window",
|
app.send({"type": "spawn_window",
|
||||||
"command": "seq 1 200; exec cat",
|
"command": "seq 1 200; exec cat",
|
||||||
|
|||||||
19
themes/dark.toml
Normal file
19
themes/dark.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# TUI-WM standardtema (mörkt).
|
||||||
|
# Välj tema i config.toml: theme = "themes/dark.toml"
|
||||||
|
# Alla fält är valfria — utelämnade fält får inbyggda standardvärden.
|
||||||
|
# Färger: namn ("cyan"), index ("235") eller hex ("#1e1e2e").
|
||||||
|
|
||||||
|
accent = "cyan" # fokusram, logga, markeringar
|
||||||
|
resize = "yellow" # ram under pågående storleksändring
|
||||||
|
resize_hover = "220" # ram vid hover över kant
|
||||||
|
border_dim = "242" # ofokuserad fönsterram
|
||||||
|
desktop_bg = "235" # skrivbordsbakgrund
|
||||||
|
panel_bg = "238" # panelens bakgrund
|
||||||
|
menu_bg = "236" # dropdown-menyns bakgrund
|
||||||
|
text = "white"
|
||||||
|
panel_text = "252"
|
||||||
|
hint = "240"
|
||||||
|
ok = "green"
|
||||||
|
danger = "red"
|
||||||
|
shadow_color = "233" # fönsterskuggans färg
|
||||||
|
dim_unfocused = true # dämpa innehåll i ofokuserade fönster
|
||||||
17
themes/light.toml
Normal file
17
themes/light.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# TUI-WM ljust tema — exempel på eget tema.
|
||||||
|
# Välj i config.toml: theme = "themes/light.toml"
|
||||||
|
|
||||||
|
accent = "blue"
|
||||||
|
resize = "166"
|
||||||
|
resize_hover = "208"
|
||||||
|
border_dim = "249"
|
||||||
|
desktop_bg = "253"
|
||||||
|
panel_bg = "251"
|
||||||
|
menu_bg = "254"
|
||||||
|
text = "black"
|
||||||
|
panel_text = "236"
|
||||||
|
hint = "245"
|
||||||
|
ok = "28"
|
||||||
|
danger = "124"
|
||||||
|
shadow_color = "248"
|
||||||
|
dim_unfocused = true
|
||||||
Reference in New Issue
Block a user