Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6a69e78ef | |||
| 87ff0e1b77 | |||
| 9d637a9378 |
@@ -51,12 +51,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
BODY="Rullande bygge från senaste master. Commit: ${{ github.sha }}. Arkitekturer: linux x64 + arm64 (Pi5)."
|
BASE="https://gitea.brasse-pc.eu/$REPO/releases/download/$TAG"
|
||||||
|
BODY=$(cat <<EOF
|
||||||
|
Rullande bygge från senaste master. Commit: ${{ github.sha }}. Arkitekturer: linux x64 + arm64 (Pi5).
|
||||||
|
|
||||||
|
## Installera / uppdatera
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
mkdir -p ~/.local/bin && curl -fsSL "$BASE/tui-wm-linux-\$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')" -o ~/.local/bin/tui-wm && chmod +x ~/.local/bin/tui-wm && ~/.local/bin/tui-wm --init
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Kör sedan \`tui-wm\` (se till att \`~/.local/bin\` finns i \$PATH).
|
||||||
|
\`--init\` skriver standardkonfiguration till ~/.config/tui-wm — hoppa över den vid uppdatering.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
rid=$(curl -s -X POST "$API/repos/$REPO/releases" \
|
rid=$(curl -s -X POST "$API/repos/$REPO/releases" \
|
||||||
-H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
-H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"$BODY\"}" | jq -r '.id // empty')
|
-d "$(jq -n --arg tag "$TAG" --arg body "$BODY" '{tag_name:$tag,name:$tag,body:$body}')" | jq -r '.id // empty')
|
||||||
[ -z "$rid" ] && rid=$(curl -s "$API/repos/$REPO/releases/tags/$TAG" \
|
if [ -z "$rid" ]; then
|
||||||
-H "Authorization: token $TOKEN" | jq -r '.id')
|
rid=$(curl -s "$API/repos/$REPO/releases/tags/$TAG" \
|
||||||
|
-H "Authorization: token $TOKEN" | jq -r '.id')
|
||||||
|
curl -s -X PATCH "$API/repos/$REPO/releases/$rid" \
|
||||||
|
-H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -n --arg body "$BODY" '{body:$body}')" -o /dev/null
|
||||||
|
fi
|
||||||
echo "release id: $rid"
|
echo "release id: $rid"
|
||||||
for f in tui-wm-linux-x64 tui-wm-linux-arm64 tui-wm-config.tar.gz checksums.txt; do
|
for f in tui-wm-linux-x64 tui-wm-linux-arm64 tui-wm-config.tar.gz checksums.txt; do
|
||||||
aid=$(curl -s "$API/repos/$REPO/releases/$rid/assets" \
|
aid=$(curl -s "$API/repos/$REPO/releases/$rid/assets" \
|
||||||
|
|||||||
38
Cargo.lock
generated
38
Cargo.lock
generated
@@ -451,12 +451,28 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fontdue"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"ttf-parser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -524,6 +540,17 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash 0.1.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -532,7 +559,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"foldhash",
|
"foldhash 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -543,7 +570,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"foldhash",
|
"foldhash 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1716,12 +1743,19 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttf-parser"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tui-wm"
|
name = "tui-wm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"fontdue",
|
||||||
"image",
|
"image",
|
||||||
"interprocess",
|
"interprocess",
|
||||||
"libc",
|
"libc",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ anyhow = "1"
|
|||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] }
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "webp"] }
|
||||||
interprocess = "2.4"
|
interprocess = "2.4"
|
||||||
unicode-width = "0.2"
|
unicode-width = "0.2"
|
||||||
|
fontdue = "0.9"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|||||||
BIN
assets/DejaVuSansMono.ttf
Normal file
BIN
assets/DejaVuSansMono.ttf
Normal file
Binary file not shown.
@@ -24,6 +24,12 @@ default_window_size = [82, 26]
|
|||||||
# Bakgrundsfärg för virtuella terminaler ("#RRGGBB", "0"-"255" eller färgnamn)
|
# Bakgrundsfärg för virtuella terminaler ("#RRGGBB", "0"-"255" eller färgnamn)
|
||||||
# terminal_bg_color = "#1a1a2e"
|
# terminal_bg_color = "#1a1a2e"
|
||||||
|
|
||||||
|
|
||||||
|
# Skärmbilder: katalog (relativ = under konfig-katalogen) och format
|
||||||
|
# ("text", "png" eller "both").
|
||||||
|
# screenshot_dir = "skärmbilder"
|
||||||
|
# screenshot_format = "both"
|
||||||
|
|
||||||
[[panel]]
|
[[panel]]
|
||||||
position = "top"
|
position = "top"
|
||||||
file = "panels/topbar.toml"
|
file = "panels/topbar.toml"
|
||||||
@@ -88,6 +94,13 @@ key = "alt+up"
|
|||||||
scope = "global"
|
scope = "global"
|
||||||
action = { type = "maximize" }
|
action = { type = "maximize" }
|
||||||
|
|
||||||
|
# Skärmbild av arbetsytan (sparas som text + PNG i skärmbilder/)
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+p"
|
||||||
|
scope = "global"
|
||||||
|
label = "Skärmbild"
|
||||||
|
action = { type = "screenshot" }
|
||||||
|
|
||||||
[[keybind]]
|
[[keybind]]
|
||||||
key = "alt+c"
|
key = "alt+c"
|
||||||
scope = "global"
|
scope = "global"
|
||||||
|
|||||||
13
config.toml
13
config.toml
@@ -22,6 +22,12 @@ background_image = "/home/brasse/Bilder/backrunder/pexels-veeterzy-38136.jpg"
|
|||||||
# Standardstorlek [bredd, höjd] för nya terminalfönster (default: [82, 26])
|
# Standardstorlek [bredd, höjd] för nya terminalfönster (default: [82, 26])
|
||||||
# default_window_size = [82, 26]
|
# default_window_size = [82, 26]
|
||||||
|
|
||||||
|
|
||||||
|
# Skärmbilder: katalog (relativ = under konfig-katalogen) och format
|
||||||
|
# ("text", "png" eller "both").
|
||||||
|
# screenshot_dir = "skärmbilder"
|
||||||
|
# screenshot_format = "both"
|
||||||
|
|
||||||
[[panel]]
|
[[panel]]
|
||||||
position = "top"
|
position = "top"
|
||||||
file = "panels/topbar.toml"
|
file = "panels/topbar.toml"
|
||||||
@@ -94,6 +100,13 @@ key = "alt+up"
|
|||||||
scope = "global"
|
scope = "global"
|
||||||
action = { type = "maximize" }
|
action = { type = "maximize" }
|
||||||
|
|
||||||
|
# Skärmbild av arbetsytan (sparas som text + PNG i skärmbilder/)
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+p"
|
||||||
|
scope = "global"
|
||||||
|
label = "Skärmbild"
|
||||||
|
action = { type = "screenshot" }
|
||||||
|
|
||||||
# Copy-mode: bläddra/söka i fokuserat fönsters historik med tangentbordet
|
# Copy-mode: bläddra/söka i fokuserat fönsters historik med tangentbordet
|
||||||
# (↑↓/PgUp/PgDn bläddrar, / söker, n/N hoppar mellan träffar, q stänger)
|
# (↑↓/PgUp/PgDn bläddrar, / söker, n/N hoppar mellan träffar, q stänger)
|
||||||
[[keybind]]
|
[[keybind]]
|
||||||
|
|||||||
7
dist/linux/config.toml
vendored
7
dist/linux/config.toml
vendored
@@ -88,6 +88,13 @@ key = "alt+up"
|
|||||||
scope = "global"
|
scope = "global"
|
||||||
action = { type = "maximize" }
|
action = { type = "maximize" }
|
||||||
|
|
||||||
|
# Skärmbild av arbetsytan (sparas som text + PNG i skärmbilder/)
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+p"
|
||||||
|
scope = "global"
|
||||||
|
label = "Skärmbild"
|
||||||
|
action = { type = "screenshot" }
|
||||||
|
|
||||||
[[keybind]]
|
[[keybind]]
|
||||||
key = "alt+c"
|
key = "alt+c"
|
||||||
scope = "global"
|
scope = "global"
|
||||||
|
|||||||
7
dist/win/config.toml
vendored
7
dist/win/config.toml
vendored
@@ -88,6 +88,13 @@ key = "alt+up"
|
|||||||
scope = "global"
|
scope = "global"
|
||||||
action = { type = "maximize" }
|
action = { type = "maximize" }
|
||||||
|
|
||||||
|
# Skärmbild av arbetsytan (sparas som text + PNG i skärmbilder/)
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+p"
|
||||||
|
scope = "global"
|
||||||
|
label = "Skärmbild"
|
||||||
|
action = { type = "screenshot" }
|
||||||
|
|
||||||
[[keybind]]
|
[[keybind]]
|
||||||
key = "alt+c"
|
key = "alt+c"
|
||||||
scope = "global"
|
scope = "global"
|
||||||
|
|||||||
45
docs/KNOWN-ISSUES.md
Normal file
45
docs/KNOWN-ISSUES.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Kända problem
|
||||||
|
|
||||||
|
## Spöktecken i vissa terminaler vid fönsterflytt
|
||||||
|
|
||||||
|
**Status:** öppen — behöver reproduktionsinfo från värdterminalen.
|
||||||
|
|
||||||
|
**Symptom:** när ett fönster (t.ex. TUI-FM) dras runt kan enstaka ikoner
|
||||||
|
och bokstäver "fastna" på skärmen — ofta från vänsterspalten i TUI-FM —
|
||||||
|
tills en annan ruta dras över dem.
|
||||||
|
|
||||||
|
**Analys hittills:**
|
||||||
|
|
||||||
|
- Kompositorn är verifierat korrekt: headless-frames (daemon-läget) är
|
||||||
|
pixelperfekta efter godtyckliga drag — varje rad exakt rätt bredd,
|
||||||
|
inga dubbletter (`tests/integration.py` + manuella tmux-tester).
|
||||||
|
- I tmux (strikt referensterminal) uppstår inga spöken, varken under
|
||||||
|
eller efter drag.
|
||||||
|
- Rotorsaksklass: terminalen/fonten och `unicode-width` är oense om
|
||||||
|
glyfbredder (typiskt emoji med tvetydig East-Asian-Width, t.ex. ⭐ ➕),
|
||||||
|
eller terminalens hantering av delvis överskrivna breda glyfer.
|
||||||
|
|
||||||
|
**Åtgärdade delar (som inte räckt fullt ut enligt rapport):**
|
||||||
|
|
||||||
|
1. Full omritning vid drag-släpp/stängning/snap (`needs_full_redraw`).
|
||||||
|
2. `SafeBackend`: explicit markörpositionering (`ESC[y;xH`) för varje
|
||||||
|
cell som skrivs — breddoenighet kan inte längre ackumuleras inom en
|
||||||
|
rad.
|
||||||
|
3. Skade-expansion för breda glyfer: när ett brett tecken ersätts av
|
||||||
|
ett smalt skrivs högercellen över explicit (ratatui-diffen ser den
|
||||||
|
inte eftersom båda buffertarna har space där).
|
||||||
|
|
||||||
|
**För att komma vidare behövs:**
|
||||||
|
|
||||||
|
- Terminalemulator + version (Konsole? Kitty? Alacritty? ...) och font.
|
||||||
|
- `echo $TERM` i terminalen där felet syns.
|
||||||
|
- En PNG-skärmbild via TUI-WM:s inbyggda `alt+p` när spöket syns
|
||||||
|
(visar kompositorns bild) **plus** en riktig skärmdump av terminalen
|
||||||
|
(visar vad terminalen faktiskt ritar) — skiljer de sig är felet
|
||||||
|
bekräftat terminal-sidigt.
|
||||||
|
- Test i en annan terminal (t.ex. kitty eller wezterm) för jämförelse.
|
||||||
|
|
||||||
|
**Möjliga nästa steg:** rendera om hela raden (inte bara cellen) när en
|
||||||
|
rad innehåller tecken med tvetydig bredd; alternativt heltäckande
|
||||||
|
frame-omskrivning med synkroniserade uppdateringar (DEC 2026) som i
|
||||||
|
klient-läget, där problemet inte kan uppstå.
|
||||||
219
src/app.rs
219
src/app.rs
@@ -111,6 +111,9 @@ pub struct App {
|
|||||||
last_title_click: Option<(usize, std::time::Instant)>,
|
last_title_click: Option<(usize, std::time::Instant)>,
|
||||||
/// Copy-mode: bläddra/söka i fokuserat fönsters historik med tangentbordet
|
/// Copy-mode: bläddra/söka i fokuserat fönsters historik med tangentbordet
|
||||||
pub copy_mode: Option<CopyMode>,
|
pub copy_mode: Option<CopyMode>,
|
||||||
|
/// Senaste layout-arean (hela skärmen inkl. paneler) — används av
|
||||||
|
/// skärmbilden för att rendera i rätt storlek.
|
||||||
|
pub last_area: Rect,
|
||||||
/// Begär full omritning av värdterminalen (rensar spök-tecken som
|
/// Begär full omritning av värdterminalen (rensar spök-tecken som
|
||||||
/// vissa terminaler lämnar kvar när breda glyfer delvis skrivs över)
|
/// vissa terminaler lämnar kvar när breda glyfer delvis skrivs över)
|
||||||
pub needs_full_redraw: bool,
|
pub needs_full_redraw: bool,
|
||||||
@@ -320,6 +323,8 @@ pub enum WindowContent {
|
|||||||
editing: Option<String>,
|
editing: Option<String>,
|
||||||
/// Associationsvyn är öppen: markerat index i listan
|
/// Associationsvyn är öppen: markerat index i listan
|
||||||
assoc: Option<usize>,
|
assoc: Option<usize>,
|
||||||
|
/// Tangentvyn är öppen: (markerat index, fångar ny tangent)
|
||||||
|
keys: Option<(usize, bool)>,
|
||||||
},
|
},
|
||||||
/// "Öppna med"-dialog: fråga vilken app som ska öppna en fil vars
|
/// "Öppna med"-dialog: fråga vilken app som ska öppna en fil vars
|
||||||
/// format saknar association.
|
/// format saknar association.
|
||||||
@@ -573,6 +578,7 @@ impl App {
|
|||||||
last_title_click: None,
|
last_title_click: None,
|
||||||
copy_mode: None,
|
copy_mode: None,
|
||||||
needs_full_redraw: false,
|
needs_full_redraw: false,
|
||||||
|
last_area: Rect::default(),
|
||||||
watch_sig,
|
watch_sig,
|
||||||
last_watch_check: None,
|
last_watch_check: None,
|
||||||
hovered_tui_btn: false,
|
hovered_tui_btn: false,
|
||||||
@@ -582,6 +588,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_layout(&mut self, area: Rect) {
|
pub fn update_layout(&mut self, area: Rect) {
|
||||||
|
self.last_area = area;
|
||||||
self.panel_rects.clear();
|
self.panel_rects.clear();
|
||||||
self.panel_item_rects.clear();
|
self.panel_item_rects.clear();
|
||||||
self.tui_wm_btn_rects.clear();
|
self.tui_wm_btn_rects.clear();
|
||||||
@@ -1354,7 +1361,7 @@ impl App {
|
|||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
// rad 1..=4 i innehållet motsvarar inställningsrad 0..=3
|
// rad 1..=4 i innehållet motsvarar inställningsrad 0..=3
|
||||||
let rel = row.saturating_sub(cr.y);
|
let rel = row.saturating_sub(cr.y);
|
||||||
if (1..=6).contains(&rel) {
|
if (1..=8).contains(&rel) {
|
||||||
let target = (rel - 1) as usize;
|
let target = (rel - 1) as usize;
|
||||||
let mut activate = false;
|
let mut activate = false;
|
||||||
if let Some(WindowContent::Settings { selected, .. }) = self.settings_content(id) {
|
if let Some(WindowContent::Settings { selected, .. }) = self.settings_content(id) {
|
||||||
@@ -1526,6 +1533,7 @@ impl App {
|
|||||||
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::SpawnSettings => self.spawn_settings(),
|
||||||
|
MenuAction::Screenshot => self.take_screenshot(),
|
||||||
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(),
|
||||||
@@ -1859,6 +1867,56 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ta en skärmbild av arbetsytan och spara enligt konfigurationen.
|
||||||
|
pub fn take_screenshot(&mut self) {
|
||||||
|
let area = self.last_area;
|
||||||
|
if area.width == 0 || area.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut tt = match ratatui::Terminal::new(
|
||||||
|
ratatui::backend::TestBackend::new(area.width, area.height),
|
||||||
|
) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if tt.draw(|f| crate::render::render(f, self)).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dir = self
|
||||||
|
.config
|
||||||
|
.screenshot_dir
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "skärmbilder".to_string());
|
||||||
|
let stamp = crate::log::timestamp().replace(' ', "_").replace(':', "");
|
||||||
|
let fmt = self.config.screenshot_format.clone();
|
||||||
|
match crate::screenshot::save(tt.backend().buffer(), &dir, &fmt, &stamp) {
|
||||||
|
Ok(s) => {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if let Some(p) = &s.text_path {
|
||||||
|
parts.push(p.display().to_string());
|
||||||
|
}
|
||||||
|
if let Some(p) = &s.png_path {
|
||||||
|
parts.push(p.display().to_string());
|
||||||
|
}
|
||||||
|
crate::log::log(&format!("skärmbild sparad: {}", parts.join(", ")));
|
||||||
|
self.spawn_popup_dialog(
|
||||||
|
format!("Skärmbild sparad:\n{}", parts.join("\n")),
|
||||||
|
vec!["OK".to_string()],
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.spawn_popup_dialog(
|
||||||
|
format!("Skärmbild misslyckades: {}", e),
|
||||||
|
vec!["OK".to_string()],
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Öppna inställningsdialogen (eller fokusera en redan öppen).
|
/// Öppna inställningsdialogen (eller fokusera en redan öppen).
|
||||||
fn spawn_settings(&mut self) {
|
fn spawn_settings(&mut self) {
|
||||||
if let Some(w) = self.windows.iter().find(|w| matches!(w.content, WindowContent::Settings { .. })) {
|
if let Some(w) = self.windows.iter().find(|w| matches!(w.content, WindowContent::Settings { .. })) {
|
||||||
@@ -1868,7 +1926,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
let ca = self.content_area;
|
let ca = self.content_area;
|
||||||
let w = 62u16.min(ca.width.saturating_sub(2));
|
let w = 62u16.min(ca.width.saturating_sub(2));
|
||||||
let h = 13u16.min(ca.height.saturating_sub(2));
|
let h = 17u16.min(ca.height.saturating_sub(2));
|
||||||
let x = ca.x as i32 + ((ca.width.saturating_sub(w)) / 2) as i32;
|
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 y = ca.y as i32 + ((ca.height.saturating_sub(h)) / 2) as i32;
|
||||||
let id = self.next_id;
|
let id = self.next_id;
|
||||||
@@ -1879,7 +1937,7 @@ impl App {
|
|||||||
y,
|
y,
|
||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
content: WindowContent::Settings { selected: 0, editing: None, assoc: None },
|
content: WindowContent::Settings { selected: 0, editing: None, assoc: None, keys: None },
|
||||||
dragging: None,
|
dragging: None,
|
||||||
resizing: None,
|
resizing: None,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
@@ -1897,7 +1955,72 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_settings_key(&mut self, id: usize, key: KeyEvent) {
|
fn handle_settings_key(&mut self, id: usize, key: KeyEvent) {
|
||||||
const ROWS: usize = 6;
|
const ROWS: usize = 8;
|
||||||
|
// Tangentvyn är öppen?
|
||||||
|
let keys_state = match self.settings_content(id) {
|
||||||
|
Some(WindowContent::Settings { keys, .. }) => *keys,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some((sel, capturing)) = keys_state {
|
||||||
|
let len = self.config.keybinds.len();
|
||||||
|
if capturing {
|
||||||
|
// Nästa tangenttryck blir den nya genvägen (Esc avbryter)
|
||||||
|
if key.code == KeyCode::Esc {
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = Some((sel, false));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(chord) = format_key_chord(key) {
|
||||||
|
if let Some(kb) = self.config.keybinds.get_mut(sel) {
|
||||||
|
kb.key = chord;
|
||||||
|
}
|
||||||
|
self.parsed_keybinds = Self::build_keybinds(&self.config);
|
||||||
|
let _ = Config::save_keybinds("config.toml", &self.config.keybinds);
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = Some((sel, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc | KeyCode::Char('q') => {
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = Some((sel.saturating_sub(1), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = Some(((sel + 1).min(len.saturating_sub(1)), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if len > 0 {
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = Some((sel, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Delete | KeyCode::Backspace => {
|
||||||
|
if sel < len {
|
||||||
|
self.config.keybinds.remove(sel);
|
||||||
|
self.parsed_keybinds = Self::build_keybinds(&self.config);
|
||||||
|
let _ = Config::save_keybinds("config.toml", &self.config.keybinds);
|
||||||
|
let new_len = self.config.keybinds.len();
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = Some((sel.min(new_len.saturating_sub(1)), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Associationsvyn är öppen?
|
// Associationsvyn är öppen?
|
||||||
let assoc_open = matches!(
|
let assoc_open = matches!(
|
||||||
self.settings_content(id),
|
self.settings_content(id),
|
||||||
@@ -1945,13 +2068,27 @@ impl App {
|
|||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let buf = editing.take().unwrap_or_default();
|
let buf = editing.take().unwrap_or_default();
|
||||||
let val = buf.trim().to_string();
|
let val = buf.trim().to_string();
|
||||||
self.config.default_editor =
|
let row = match self.settings_content(id) {
|
||||||
if val.is_empty() { None } else { Some(val) };
|
Some(WindowContent::Settings { selected, .. }) => *selected,
|
||||||
let _ = Config::save_top_level_str(
|
_ => 2,
|
||||||
"config.toml",
|
};
|
||||||
"default_editor",
|
if row == 3 {
|
||||||
self.config.default_editor.as_deref(),
|
self.config.screenshot_dir =
|
||||||
);
|
if val.is_empty() { None } else { Some(val) };
|
||||||
|
let _ = Config::save_top_level_str(
|
||||||
|
"config.toml",
|
||||||
|
"screenshot_dir",
|
||||||
|
self.config.screenshot_dir.as_deref(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.config.default_editor =
|
||||||
|
if val.is_empty() { None } else { Some(val) };
|
||||||
|
let _ = Config::save_top_level_str(
|
||||||
|
"config.toml",
|
||||||
|
"default_editor",
|
||||||
|
self.config.default_editor.as_deref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
*editing = None;
|
*editing = None;
|
||||||
@@ -2012,6 +2149,7 @@ impl App {
|
|||||||
/// direkt, men tema/flaggor appliceras även omedelbart i minnet.
|
/// direkt, men tema/flaggor appliceras även omedelbart i minnet.
|
||||||
fn settings_activate(&mut self, id: usize, forward: bool) {
|
fn settings_activate(&mut self, id: usize, forward: bool) {
|
||||||
let current_editor = self.config.default_editor.clone().unwrap_or_default();
|
let current_editor = self.config.default_editor.clone().unwrap_or_default();
|
||||||
|
let current_shotdir = self.config.screenshot_dir.clone().unwrap_or_default();
|
||||||
let Some(WindowContent::Settings { selected, editing, .. }) = self.settings_content(id) else { return };
|
let Some(WindowContent::Settings { selected, editing, .. }) = self.settings_content(id) else { return };
|
||||||
let row = *selected;
|
let row = *selected;
|
||||||
if row == 2 {
|
if row == 2 {
|
||||||
@@ -2019,13 +2157,25 @@ impl App {
|
|||||||
*editing = Some(current_editor);
|
*editing = Some(current_editor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if row == 5 {
|
if row == 3 {
|
||||||
|
// Skärmbildskatalog: börja textredigera värdet
|
||||||
|
*editing = Some(current_shotdir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if row == 6 {
|
||||||
// Associationer: öppna listvyn
|
// Associationer: öppna listvyn
|
||||||
if let Some(WindowContent::Settings { assoc, .. }) = self.settings_content(id) {
|
if let Some(WindowContent::Settings { assoc, .. }) = self.settings_content(id) {
|
||||||
*assoc = Some(0);
|
*assoc = Some(0);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if row == 7 {
|
||||||
|
// Tangenter: öppna keybind-vyn
|
||||||
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
|
*keys = Some((0, false));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
match row {
|
match row {
|
||||||
0 => {
|
0 => {
|
||||||
// Tema: cykla inbyggt + themes/*.toml
|
// Tema: cykla inbyggt + themes/*.toml
|
||||||
@@ -2060,11 +2210,11 @@ impl App {
|
|||||||
let _ = Config::save_panel_file("config.toml", 0, &options[next]);
|
let _ = Config::save_panel_file("config.toml", 0, &options[next]);
|
||||||
// panel-innehållet läses om av hot-reloaden
|
// panel-innehållet läses om av hot-reloaden
|
||||||
}
|
}
|
||||||
3 => {
|
4 => {
|
||||||
self.config.shadow = !self.config.shadow;
|
self.config.shadow = !self.config.shadow;
|
||||||
let _ = Config::save_top_level_bool("config.toml", "shadow", self.config.shadow);
|
let _ = Config::save_top_level_bool("config.toml", "shadow", self.config.shadow);
|
||||||
}
|
}
|
||||||
4 => {
|
5 => {
|
||||||
self.config.show_scrollbar = !self.config.show_scrollbar;
|
self.config.show_scrollbar = !self.config.show_scrollbar;
|
||||||
let _ = Config::save_top_level_bool("config.toml", "show_scrollbar", self.config.show_scrollbar);
|
let _ = Config::save_top_level_bool("config.toml", "show_scrollbar", self.config.show_scrollbar);
|
||||||
}
|
}
|
||||||
@@ -2626,6 +2776,41 @@ pub fn build_open_command(cmd: &str, path: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formatera ett tangenttryck som en keybind-sträng ("alt+w", "ctrl+f2"...).
|
||||||
|
/// Returnerar None för tangenter som inte kan bindas.
|
||||||
|
fn format_key_chord(key: KeyEvent) -> Option<String> {
|
||||||
|
let mut s = String::new();
|
||||||
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
s.push_str("ctrl+");
|
||||||
|
}
|
||||||
|
if key.modifiers.contains(KeyModifiers::ALT) {
|
||||||
|
s.push_str("alt+");
|
||||||
|
}
|
||||||
|
if key.modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
s.push_str("shift+");
|
||||||
|
}
|
||||||
|
let name = match key.code {
|
||||||
|
KeyCode::Char(' ') => "space".to_string(),
|
||||||
|
KeyCode::Char(c) => c.to_lowercase().to_string(),
|
||||||
|
KeyCode::Enter => "enter".to_string(),
|
||||||
|
KeyCode::Tab | KeyCode::BackTab => "tab".to_string(),
|
||||||
|
KeyCode::Backspace => "backspace".to_string(),
|
||||||
|
KeyCode::Delete => "delete".to_string(),
|
||||||
|
KeyCode::Up => "up".to_string(),
|
||||||
|
KeyCode::Down => "down".to_string(),
|
||||||
|
KeyCode::Left => "left".to_string(),
|
||||||
|
KeyCode::Right => "right".to_string(),
|
||||||
|
KeyCode::Home => "home".to_string(),
|
||||||
|
KeyCode::End => "end".to_string(),
|
||||||
|
KeyCode::PageUp => "pageup".to_string(),
|
||||||
|
KeyCode::PageDown => "pagedown".to_string(),
|
||||||
|
KeyCode::F(n) => format!("f{}", n),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
s.push_str(&name);
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
|
||||||
fn default_shell() -> String {
|
fn default_shell() -> String {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
// Försök hitta PowerShell i prioritetsordning: pwsh (Core) → powershell (inbyggd)
|
// Försök hitta PowerShell i prioritetsordning: pwsh (Core) → powershell (inbyggd)
|
||||||
@@ -2664,6 +2849,11 @@ fn run_command(command: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Publik variant för render-lagret.
|
||||||
|
pub fn action_display_pub(action: &MenuAction) -> &'static str {
|
||||||
|
action_display(action)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returnerar ett kortfattat namn för en MenuAction (används som fallback i keybind-menyn).
|
/// Returnerar ett kortfattat namn för en MenuAction (används som fallback i keybind-menyn).
|
||||||
fn action_display(action: &MenuAction) -> &'static str {
|
fn action_display(action: &MenuAction) -> &'static str {
|
||||||
match action {
|
match action {
|
||||||
@@ -2676,6 +2866,7 @@ fn action_display(action: &MenuAction) -> &'static str {
|
|||||||
MenuAction::WindowSwitcher => "Fönsterväxlare",
|
MenuAction::WindowSwitcher => "Fönsterväxlare",
|
||||||
MenuAction::CopyMode => "Copy-mode",
|
MenuAction::CopyMode => "Copy-mode",
|
||||||
MenuAction::SpawnSettings => "Inställningar",
|
MenuAction::SpawnSettings => "Inställningar",
|
||||||
|
MenuAction::Screenshot => "Skärmbild",
|
||||||
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",
|
||||||
|
|||||||
@@ -21,11 +21,20 @@ use std::io::{self, Write};
|
|||||||
|
|
||||||
pub struct SafeBackend<W: Write> {
|
pub struct SafeBackend<W: Write> {
|
||||||
writer: W,
|
writer: W,
|
||||||
|
/// Positioner där vi senast skrev ett brett (2-kolumns) tecken.
|
||||||
|
/// Behövs för att expandera ratatuis diff: när ett brett tecken
|
||||||
|
/// ersätts av ett smalt innehåller diffen bara vänstercellen —
|
||||||
|
/// högercellen är "space" i båda ratatui-buffertarna och skrivs
|
||||||
|
/// aldrig om, så terminalen kan lämna kvar glyfens högra halva.
|
||||||
|
wide_cells: std::collections::HashSet<(u16, u16)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: Write> SafeBackend<W> {
|
impl<W: Write> SafeBackend<W> {
|
||||||
pub fn new(writer: W) -> Self {
|
pub fn new(writer: W) -> Self {
|
||||||
SafeBackend { writer }
|
SafeBackend {
|
||||||
|
writer,
|
||||||
|
wide_cells: std::collections::HashSet::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,11 +78,15 @@ impl<W: Write> Backend for SafeBackend<W> {
|
|||||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||||
{
|
{
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
let cells: Vec<(u16, u16, &Cell)> = content.collect();
|
||||||
|
let in_diff: std::collections::HashSet<(u16, u16)> =
|
||||||
|
cells.iter().map(|(x, y, _)| (*x, *y)).collect();
|
||||||
|
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
let mut last_fg = None;
|
let mut last_fg = None;
|
||||||
let mut last_bg = None;
|
let mut last_bg = None;
|
||||||
let mut last_mod = None;
|
let mut last_mod = None;
|
||||||
for (x, y, cell) in content {
|
for (x, y, cell) in cells {
|
||||||
// Explicit position för varje cell — se modulkommentaren.
|
// Explicit position för varje cell — se modulkommentaren.
|
||||||
let _ = write!(buf, "\x1b[{};{}H", y + 1, x + 1);
|
let _ = write!(buf, "\x1b[{};{}H", y + 1, x + 1);
|
||||||
if last_mod != Some(cell.modifier) {
|
if last_mod != Some(cell.modifier) {
|
||||||
@@ -91,6 +104,23 @@ impl<W: Write> Backend for SafeBackend<W> {
|
|||||||
last_bg = Some(cell.bg);
|
last_bg = Some(cell.bg);
|
||||||
}
|
}
|
||||||
buf.push_str(cell.symbol());
|
buf.push_str(cell.symbol());
|
||||||
|
|
||||||
|
let width = unicode_width::UnicodeWidthStr::width(cell.symbol());
|
||||||
|
let was_wide = self.wide_cells.contains(&(x, y));
|
||||||
|
if width > 1 {
|
||||||
|
self.wide_cells.insert((x, y));
|
||||||
|
} else {
|
||||||
|
self.wide_cells.remove(&(x, y));
|
||||||
|
// Skade-expansion: ett brett tecken ersattes av ett smalt.
|
||||||
|
// Ratatui-diffen innehåller inte högercellen (space i båda
|
||||||
|
// buffertarna) men terminalen kan ha kvar glyfens högra
|
||||||
|
// halva där — skriv explicit över den med ett space i den
|
||||||
|
// nya cellens bakgrundsfärg.
|
||||||
|
if was_wide && !in_diff.contains(&(x + 1, y)) {
|
||||||
|
let _ = write!(buf, "\x1b[{};{}H ", y + 1, x + 2);
|
||||||
|
self.wide_cells.remove(&(x + 1, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buf.push_str("\x1b[0m");
|
buf.push_str("\x1b[0m");
|
||||||
self.writer.write_all(buf.as_bytes())
|
self.writer.write_all(buf.as_bytes())
|
||||||
@@ -117,6 +147,7 @@ impl<W: Write> Backend for SafeBackend<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self) -> io::Result<()> {
|
fn clear(&mut self) -> io::Result<()> {
|
||||||
|
self.wide_cells.clear();
|
||||||
self.writer.write_all(b"\x1b[2J\x1b[H")
|
self.writer.write_all(b"\x1b[2J\x1b[H")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone, Default)]
|
#[derive(Deserialize, Debug, Clone, Default)]
|
||||||
@@ -23,6 +23,12 @@ pub struct Config {
|
|||||||
/// Filassociationer: ändelse (gemener) → kommando för open_file.
|
/// Filassociationer: ändelse (gemener) → kommando för open_file.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub open_with: std::collections::HashMap<String, String>,
|
pub open_with: std::collections::HashMap<String, String>,
|
||||||
|
/// Katalog för skärmbilder (relativ = under konfig-katalogen).
|
||||||
|
#[serde(default)]
|
||||||
|
pub screenshot_dir: Option<String>,
|
||||||
|
/// Skärmbildsformat: "text", "png" eller "both".
|
||||||
|
#[serde(default = "default_screenshot_format")]
|
||||||
|
pub screenshot_format: String,
|
||||||
/// Rita skugga under/till höger om fönster.
|
/// Rita skugga under/till höger om fönster.
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub shadow: bool,
|
pub shadow: bool,
|
||||||
@@ -53,8 +59,12 @@ fn default_window_size() -> [u16; 2] {
|
|||||||
[82, 26]
|
[82, 26]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_screenshot_format() -> String {
|
||||||
|
"both".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// En tangentbords-genväg definierad i config
|
/// En tangentbords-genväg definierad i config
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub struct KeybindConfig {
|
pub struct KeybindConfig {
|
||||||
/// T.ex. "ctrl+space", "alt+f2", "ctrl+t"
|
/// T.ex. "ctrl+space", "alt+f2", "ctrl+t"
|
||||||
pub key: String,
|
pub key: String,
|
||||||
@@ -63,11 +73,11 @@ pub struct KeybindConfig {
|
|||||||
pub scope: KeybindScope,
|
pub scope: KeybindScope,
|
||||||
pub action: MenuAction,
|
pub action: MenuAction,
|
||||||
/// Visningsnamn som visas i TUI-WM-menyn (valfritt)
|
/// Visningsnamn som visas i TUI-WM-menyn (valfritt)
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum KeybindScope {
|
pub enum KeybindScope {
|
||||||
/// Aktiveras alltid, oavsett fokus
|
/// Aktiveras alltid, oavsett fokus
|
||||||
@@ -95,17 +105,18 @@ pub enum PanelPosition {
|
|||||||
Bottom,
|
Bottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub struct MenuItem {
|
pub struct MenuItem {
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub action: MenuAction,
|
pub action: MenuAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum MenuAction {
|
pub enum MenuAction {
|
||||||
Exit,
|
Exit,
|
||||||
SpawnTerminal {
|
SpawnTerminal {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
shell: Option<String>,
|
shell: Option<String>,
|
||||||
},
|
},
|
||||||
RunScript {
|
RunScript {
|
||||||
@@ -134,6 +145,8 @@ pub enum MenuAction {
|
|||||||
CopyMode,
|
CopyMode,
|
||||||
/// Öppnar den inbyggda inställningsdialogen
|
/// Öppnar den inbyggda inställningsdialogen
|
||||||
SpawnSettings,
|
SpawnSettings,
|
||||||
|
/// Tar en skärmbild av arbetsytan (text och/eller PNG)
|
||||||
|
Screenshot,
|
||||||
/// 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,
|
||||||
}
|
}
|
||||||
@@ -227,6 +240,12 @@ fn default_keybinds() -> Vec<KeybindConfig> {
|
|||||||
action: MenuAction::Maximize,
|
action: MenuAction::Maximize,
|
||||||
label: None,
|
label: None,
|
||||||
},
|
},
|
||||||
|
KeybindConfig {
|
||||||
|
key: "alt+p".to_string(),
|
||||||
|
scope: KeybindScope::Global,
|
||||||
|
action: MenuAction::Screenshot,
|
||||||
|
label: Some("Skärmbild".to_string()),
|
||||||
|
},
|
||||||
KeybindConfig {
|
KeybindConfig {
|
||||||
key: "alt+c".to_string(),
|
key: "alt+c".to_string(),
|
||||||
scope: KeybindScope::Global,
|
scope: KeybindScope::Global,
|
||||||
@@ -289,6 +308,8 @@ impl Config {
|
|||||||
theme: None,
|
theme: None,
|
||||||
default_editor: None,
|
default_editor: None,
|
||||||
open_with: std::collections::HashMap::new(),
|
open_with: std::collections::HashMap::new(),
|
||||||
|
screenshot_dir: None,
|
||||||
|
screenshot_format: default_screenshot_format(),
|
||||||
shadow: true,
|
shadow: true,
|
||||||
scrollback_lines: default_scrollback_lines(),
|
scrollback_lines: default_scrollback_lines(),
|
||||||
show_scrollbar: default_true(),
|
show_scrollbar: default_true(),
|
||||||
@@ -325,6 +346,21 @@ impl Config {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Skriv om hela [[keybind]]-listan i config-filen.
|
||||||
|
pub fn save_keybinds(path: &str, binds: &[KeybindConfig]) -> anyhow::Result<()> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Wrapper<'a> {
|
||||||
|
keybind: &'a [KeybindConfig],
|
||||||
|
}
|
||||||
|
let toml_str = toml::to_string(&Wrapper { keybind: binds })?;
|
||||||
|
let tmp: toml_edit::DocumentMut = toml_str.parse()?;
|
||||||
|
let content = fs::read_to_string(path).unwrap_or_default();
|
||||||
|
let mut doc: toml_edit::DocumentMut = content.parse().unwrap_or_default();
|
||||||
|
doc["keybind"] = tmp["keybind"].clone();
|
||||||
|
fs::write(path, doc.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Spara en filassociation ([open_with] ändelse → kommando).
|
/// Spara en filassociation ([open_with] ändelse → kommando).
|
||||||
pub fn save_open_with(path: &str, ext: &str, cmd: &str) -> anyhow::Result<()> {
|
pub fn save_open_with(path: &str, ext: &str, cmd: &str) -> anyhow::Result<()> {
|
||||||
let content = fs::read_to_string(path).unwrap_or_default();
|
let content = fs::read_to_string(path).unwrap_or_default();
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ fn today_filename() -> String {
|
|||||||
format!("{:04}-{:02}-{:02}.log", y, mo, d)
|
format!("{:04}-{:02}-{:02}.log", y, mo, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timestamp() -> String {
|
pub fn timestamp() -> String {
|
||||||
let secs = SystemTime::now()
|
let secs = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod kitty_gfx;
|
|||||||
mod log;
|
mod log;
|
||||||
mod pty;
|
mod pty;
|
||||||
mod render;
|
mod render;
|
||||||
|
mod screenshot;
|
||||||
mod server;
|
mod server;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
|
|||||||
@@ -390,8 +390,8 @@ 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, editing, assoc } => {
|
WindowContent::Settings { selected, editing, assoc, keys } => {
|
||||||
render_settings_content(frame, cr, *selected, editing.as_deref(), *assoc, app, th);
|
render_settings_content(frame, cr, *selected, editing.as_deref(), *assoc, *keys, app, th);
|
||||||
}
|
}
|
||||||
WindowContent::OpenWith { path, input, save_default } => {
|
WindowContent::OpenWith { path, input, save_default } => {
|
||||||
render_open_with_content(frame, cr, path, input, *save_default, th);
|
render_open_with_content(frame, cr, path, input, *save_default, th);
|
||||||
@@ -517,15 +517,71 @@ fn highlight_matches(frame: &mut Frame, rect: Rect, query: &str, style: Style) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Innehållet i inställningsdialogen: interaktiva rader + info-sektion.
|
/// Innehållet i inställningsdialogen: interaktiva rader + info-sektion.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_settings_content(
|
fn render_settings_content(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
editing: Option<&str>,
|
editing: Option<&str>,
|
||||||
assoc: Option<usize>,
|
assoc: Option<usize>,
|
||||||
|
keys: Option<(usize, bool)>,
|
||||||
app: &App,
|
app: &App,
|
||||||
th: &crate::theme::Theme,
|
th: &crate::theme::Theme,
|
||||||
) {
|
) {
|
||||||
|
// Tangentvyn: lista keybinds, ändra med Enter (fånga nästa tryck)
|
||||||
|
if let Some((sel, capturing)) = keys {
|
||||||
|
let binds = &app.config.keybinds;
|
||||||
|
let mut y = area.y;
|
||||||
|
let list_bottom = area.y + area.height.saturating_sub(2);
|
||||||
|
let visible = (list_bottom.saturating_sub(y)) as usize;
|
||||||
|
// enkel scrollning: håll markeringen i fönstret
|
||||||
|
let offset = sel.saturating_sub(visible.saturating_sub(1));
|
||||||
|
for (i, kb) in binds.iter().enumerate().skip(offset) {
|
||||||
|
if y >= list_bottom {
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Span::styled(" …fler", Style::default().fg(th.hint))),
|
||||||
|
Rect::new(area.x, y, area.width, 1),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let label = kb
|
||||||
|
.label
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| crate::app::action_display_pub(&kb.action).to_string());
|
||||||
|
let scope = match kb.scope {
|
||||||
|
crate::config::KeybindScope::Global => "global",
|
||||||
|
crate::config::KeybindScope::Wm => "wm",
|
||||||
|
};
|
||||||
|
let keytxt = if capturing && i == sel {
|
||||||
|
"<tryck ny tangent…>".to_string()
|
||||||
|
} else {
|
||||||
|
kb.key.clone()
|
||||||
|
};
|
||||||
|
let style = if i == sel {
|
||||||
|
th.hover_highlight()
|
||||||
|
} else {
|
||||||
|
Style::default().fg(th.text)
|
||||||
|
};
|
||||||
|
let line = format!(" {:<16} {:<28} [{}]", keytxt, label, scope);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Span::styled(
|
||||||
|
" ↑↓ välj Enter=byt tangent Delete=ta bort Esc=tillbaka",
|
||||||
|
Style::default().fg(th.hint),
|
||||||
|
)),
|
||||||
|
Rect::new(area.x, area.y + area.height.saturating_sub(1), area.width, 1),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Associationsvyn: lista filformat → kommandon
|
// Associationsvyn: lista filformat → kommandon
|
||||||
if let Some(sel) = assoc {
|
if let Some(sel) = assoc {
|
||||||
let mut exts: Vec<&String> = app.config.open_with.keys().collect();
|
let mut exts: Vec<&String> = app.config.open_with.keys().collect();
|
||||||
@@ -591,25 +647,32 @@ fn render_settings_content(
|
|||||||
.first()
|
.first()
|
||||||
.and_then(|p| p.file.as_deref())
|
.and_then(|p| p.file.as_deref())
|
||||||
.unwrap_or("(inline)");
|
.unwrap_or("(inline)");
|
||||||
let editor_val = match editing {
|
let edit_val = |row: usize, current: Option<String>, hint: &str| -> String {
|
||||||
Some(buf) => format!("{}▏ (Enter=spara, Esc=avbryt)", buf),
|
if selected == row {
|
||||||
None => app
|
if let Some(buf) = editing {
|
||||||
.config
|
return format!("{}▏ (Enter=spara, Esc=avbryt)", buf);
|
||||||
.default_editor
|
}
|
||||||
.clone()
|
}
|
||||||
.unwrap_or_else(|| "(ingen — Enter för att ange)".to_string()),
|
current.unwrap_or_else(|| hint.to_string())
|
||||||
};
|
};
|
||||||
|
let editor_val = edit_val(2, app.config.default_editor.clone(), "(ingen — Enter för att ange)");
|
||||||
|
let shotdir_val = edit_val(3, app.config.screenshot_dir.clone(), "skärmbilder (standard)");
|
||||||
let onoff = |b: bool| if b { "[x] på " } else { "[ ] av " };
|
let onoff = |b: bool| if b { "[x] på " } else { "[ ] av " };
|
||||||
let rows: Vec<(String, String)> = vec![
|
let rows: Vec<(String, String)> = vec![
|
||||||
("Tema".to_string(), format!("‹ {} ›", theme_name)),
|
("Tema".to_string(), format!("‹ {} ›", theme_name)),
|
||||||
("Panel".to_string(), format!("‹ {} ›", panel_name)),
|
("Panel".to_string(), format!("‹ {} ›", panel_name)),
|
||||||
("Editor".to_string(), editor_val),
|
("Editor".to_string(), editor_val),
|
||||||
|
("Skärmbilder".to_string(), shotdir_val),
|
||||||
("Skugga".to_string(), onoff(app.config.shadow).to_string()),
|
("Skugga".to_string(), onoff(app.config.shadow).to_string()),
|
||||||
("Scrollbar".to_string(), onoff(app.config.show_scrollbar).to_string()),
|
("Scrollbar".to_string(), onoff(app.config.show_scrollbar).to_string()),
|
||||||
(
|
(
|
||||||
"Filformat".to_string(),
|
"Filformat".to_string(),
|
||||||
format!("{} associationer (Enter=visa/ta bort)", app.config.open_with.len()),
|
format!("{} associationer (Enter=visa/ta bort)", app.config.open_with.len()),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Tangenter".to_string(),
|
||||||
|
format!("{} bindningar (Enter=visa/ändra)", app.config.keybinds.len()),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut y = area.y + 1;
|
let mut y = area.y + 1;
|
||||||
|
|||||||
221
src/screenshot.rs
Normal file
221
src/screenshot.rs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
//! Skärmbild av arbetsytan: sparar den compositerade skärmen som
|
||||||
|
//! textfil och/eller PNG.
|
||||||
|
//!
|
||||||
|
//! Texten är en ren avskrift av cellernas symboler. PNG:n rasteriseras
|
||||||
|
//! cell för cell med den inbäddade DejaVu Sans Mono-fonten: bakgrunds-
|
||||||
|
//! rektangel + glyf i förgrundsfärgen. Glyfer som saknas i fonten
|
||||||
|
//! (t.ex. färgemoji) ritas som en fylld ruta i förgrundsfärgen.
|
||||||
|
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::style::Color;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
const FONT_BYTES: &[u8] = include_bytes!("../assets/DejaVuSansMono.ttf");
|
||||||
|
const CELL_W: u32 = 10;
|
||||||
|
const CELL_H: u32 = 20;
|
||||||
|
const FONT_PX: f32 = 17.0;
|
||||||
|
|
||||||
|
/// Resultatet av en sparad skärmbild.
|
||||||
|
pub struct Saved {
|
||||||
|
pub text_path: Option<PathBuf>,
|
||||||
|
pub png_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spara `buf` till `dir` som text och/eller PNG beroende på `format`
|
||||||
|
/// ("text", "png" eller "both"). Returnerar sökvägarna som skrevs.
|
||||||
|
pub fn save(buf: &Buffer, dir: &str, format: &str, stamp: &str) -> std::io::Result<Saved> {
|
||||||
|
std::fs::create_dir_all(dir)?;
|
||||||
|
let base = PathBuf::from(dir).join(format!("tui-wm-{}", stamp));
|
||||||
|
let mut saved = Saved { text_path: None, png_path: None };
|
||||||
|
|
||||||
|
if format == "text" || format == "both" {
|
||||||
|
let path = base.with_extension("txt");
|
||||||
|
std::fs::write(&path, buffer_to_text(buf))?;
|
||||||
|
saved.text_path = Some(path);
|
||||||
|
}
|
||||||
|
if format == "png" || format == "both" {
|
||||||
|
let path = base.with_extension("png");
|
||||||
|
buffer_to_png(buf, &path)
|
||||||
|
.map_err(|e| std::io::Error::other(format!("PNG: {}", e)))?;
|
||||||
|
saved.png_path = Some(path);
|
||||||
|
}
|
||||||
|
Ok(saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ren text: cellsymboler rad för rad (celler som täcks av ett brett
|
||||||
|
/// tecken hoppas över, precis som i ANSI-serialiseringen).
|
||||||
|
fn buffer_to_text(buf: &Buffer) -> String {
|
||||||
|
let area = buf.area;
|
||||||
|
let mut out = String::with_capacity((area.width as usize + 1) * area.height as usize);
|
||||||
|
for row in 0..area.height {
|
||||||
|
let mut skip = 0u16;
|
||||||
|
for col in 0..area.width {
|
||||||
|
if skip > 0 {
|
||||||
|
skip -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cell = &buf.content[(row * area.width + col) as usize];
|
||||||
|
let sym = cell.symbol();
|
||||||
|
let w = unicode_width::UnicodeWidthStr::width(sym);
|
||||||
|
if w > 1 {
|
||||||
|
skip = (w as u16).saturating_sub(1);
|
||||||
|
}
|
||||||
|
out.push_str(if sym.is_empty() { " " } else { sym });
|
||||||
|
}
|
||||||
|
// trimma högerkantens mellanslag
|
||||||
|
while out.ends_with(' ') {
|
||||||
|
out.pop();
|
||||||
|
}
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standardpalett för ratatui-färger → RGB.
|
||||||
|
fn color_rgb(c: Color, is_fg: bool) -> [u8; 3] {
|
||||||
|
match c {
|
||||||
|
Color::Reset => {
|
||||||
|
if is_fg { [220, 220, 220] } else { [30, 30, 40] }
|
||||||
|
}
|
||||||
|
Color::Black => [0, 0, 0],
|
||||||
|
Color::Red => [205, 49, 49],
|
||||||
|
Color::Green => [13, 188, 121],
|
||||||
|
Color::Yellow => [229, 229, 16],
|
||||||
|
Color::Blue => [36, 114, 200],
|
||||||
|
Color::Magenta => [188, 63, 188],
|
||||||
|
Color::Cyan => [17, 168, 205],
|
||||||
|
Color::Gray => [229, 229, 229],
|
||||||
|
Color::DarkGray => [102, 102, 102],
|
||||||
|
Color::LightRed => [241, 76, 76],
|
||||||
|
Color::LightGreen => [35, 209, 139],
|
||||||
|
Color::LightYellow => [245, 245, 67],
|
||||||
|
Color::LightBlue => [59, 142, 234],
|
||||||
|
Color::LightMagenta => [214, 112, 214],
|
||||||
|
Color::LightCyan => [41, 184, 219],
|
||||||
|
Color::White => [255, 255, 255],
|
||||||
|
Color::Rgb(r, g, b) => [r, g, b],
|
||||||
|
Color::Indexed(i) => xterm256(i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// xterm-256-palettens standardvärden.
|
||||||
|
fn xterm256(i: u8) -> [u8; 3] {
|
||||||
|
match i {
|
||||||
|
0..=15 => {
|
||||||
|
const BASE: [[u8; 3]; 16] = [
|
||||||
|
[0, 0, 0], [205, 49, 49], [13, 188, 121], [229, 229, 16],
|
||||||
|
[36, 114, 200], [188, 63, 188], [17, 168, 205], [229, 229, 229],
|
||||||
|
[102, 102, 102], [241, 76, 76], [35, 209, 139], [245, 245, 67],
|
||||||
|
[59, 142, 234], [214, 112, 214], [41, 184, 219], [255, 255, 255],
|
||||||
|
];
|
||||||
|
BASE[i as usize]
|
||||||
|
}
|
||||||
|
16..=231 => {
|
||||||
|
let n = i - 16;
|
||||||
|
let steps = [0u8, 95, 135, 175, 215, 255];
|
||||||
|
[
|
||||||
|
steps[(n / 36) as usize],
|
||||||
|
steps[((n % 36) / 6) as usize],
|
||||||
|
steps[(n % 6) as usize],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
232..=255 => {
|
||||||
|
let v = 8 + (i - 232) * 10;
|
||||||
|
[v, v, v]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_to_png(buf: &Buffer, path: &std::path::Path) -> Result<(), String> {
|
||||||
|
use ratatui::style::Modifier;
|
||||||
|
|
||||||
|
let font = fontdue::Font::from_bytes(FONT_BYTES, fontdue::FontSettings::default())
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let area = buf.area;
|
||||||
|
let img_w = area.width as u32 * CELL_W;
|
||||||
|
let img_h = area.height as u32 * CELL_H;
|
||||||
|
let mut img = image::RgbImage::new(img_w, img_h);
|
||||||
|
|
||||||
|
for row in 0..area.height {
|
||||||
|
let mut skip = 0u16;
|
||||||
|
for col in 0..area.width {
|
||||||
|
if skip > 0 {
|
||||||
|
skip -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cell = &buf.content[(row * area.width + col) as usize];
|
||||||
|
let sym = cell.symbol();
|
||||||
|
let cw = unicode_width::UnicodeWidthStr::width(sym).max(1);
|
||||||
|
if cw > 1 {
|
||||||
|
skip = (cw as u16) - 1;
|
||||||
|
}
|
||||||
|
let (mut fg, mut bg) = (
|
||||||
|
color_rgb(cell.fg, true),
|
||||||
|
color_rgb(cell.bg, false),
|
||||||
|
);
|
||||||
|
if cell.modifier.contains(Modifier::REVERSED) {
|
||||||
|
std::mem::swap(&mut fg, &mut bg);
|
||||||
|
}
|
||||||
|
if cell.modifier.contains(Modifier::DIM) {
|
||||||
|
fg = [fg[0] / 2, fg[1] / 2, fg[2] / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
let x0 = col as u32 * CELL_W;
|
||||||
|
let y0 = row as u32 * CELL_H;
|
||||||
|
let cell_px_w = CELL_W * cw as u32;
|
||||||
|
// Bakgrund
|
||||||
|
for y in y0..(y0 + CELL_H).min(img_h) {
|
||||||
|
for x in x0..(x0 + cell_px_w).min(img_w) {
|
||||||
|
img.put_pixel(x, y, image::Rgb(bg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Glyf
|
||||||
|
let ch = sym.chars().next().unwrap_or(' ');
|
||||||
|
if ch == ' ' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if font.lookup_glyph_index(ch) != 0 {
|
||||||
|
let (metrics, bitmap) = font.rasterize(ch, FONT_PX);
|
||||||
|
// Baslinje ungefär 4 px över cellens botten
|
||||||
|
let gx = x0 as i64 + metrics.xmin as i64
|
||||||
|
+ ((cell_px_w as i64 - metrics.width as i64) / 2).max(0);
|
||||||
|
let gy = y0 as i64 + (CELL_H as i64 - 4) - metrics.height as i64
|
||||||
|
- metrics.ymin as i64;
|
||||||
|
for by in 0..metrics.height {
|
||||||
|
for bx in 0..metrics.width {
|
||||||
|
let a = bitmap[by * metrics.width + bx] as u32;
|
||||||
|
if a == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let px = gx + bx as i64;
|
||||||
|
let py = gy + by as i64;
|
||||||
|
if px < 0 || py < 0 || px as u32 >= img_w || py as u32 >= img_h {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let old = img.get_pixel(px as u32, py as u32).0;
|
||||||
|
let blend = |o: u8, f: u8| {
|
||||||
|
((o as u32 * (255 - a) + f as u32 * a) / 255) as u8
|
||||||
|
};
|
||||||
|
img.put_pixel(
|
||||||
|
px as u32,
|
||||||
|
py as u32,
|
||||||
|
image::Rgb([
|
||||||
|
blend(old[0], fg[0]),
|
||||||
|
blend(old[1], fg[1]),
|
||||||
|
blend(old[2], fg[2]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Glyf saknas (t.ex. emoji): fylld ruta med marginal
|
||||||
|
for y in (y0 + 3)..(y0 + CELL_H - 3).min(img_h) {
|
||||||
|
for x in (x0 + 2)..(x0 + cell_px_w - 2).min(img_w) {
|
||||||
|
img.put_pixel(x, y, image::Rgb(fg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.save(path).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
@@ -396,10 +396,10 @@ def main():
|
|||||||
text = disp.frame_text(timeout=5.0, contains="Tema:")
|
text = disp.frame_text(timeout=5.0, contains="Tema:")
|
||||||
check("inställningsdialogen öppnas från panelen", "Tema:" in text
|
check("inställningsdialogen öppnas från panelen", "Tema:" in text
|
||||||
and "Skugga" in text and "config.toml" in text)
|
and "Skugga" in text and "config.toml" in text)
|
||||||
# Ned tre rader till Skugga (0=Tema, 1=Panel, 2=Editor, 3=Skugga)
|
# Ned fyra rader till Skugga
|
||||||
disp.send(key_event(code="Down"))
|
# (0=Tema, 1=Panel, 2=Editor, 3=Skärmbilder, 4=Skugga)
|
||||||
disp.send(key_event(code="Down"))
|
for _ in range(4):
|
||||||
disp.send(key_event(code="Down"))
|
disp.send(key_event(code="Down"))
|
||||||
disp.send(key_event(code="Enter"))
|
disp.send(key_event(code="Enter"))
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
cfg_now = open(os.path.join(cfgdir, "config.toml")).read()
|
cfg_now = open(os.path.join(cfgdir, "config.toml")).read()
|
||||||
@@ -446,7 +446,7 @@ def main():
|
|||||||
text = disp.frame_text(timeout=5.0, contains="Filformat")
|
text = disp.frame_text(timeout=5.0, contains="Filformat")
|
||||||
check("inställningarna visar filformat-raden", "1 associationer" in text,
|
check("inställningarna visar filformat-raden", "1 associationer" in text,
|
||||||
f"frame: {text[:300]!r}")
|
f"frame: {text[:300]!r}")
|
||||||
for _ in range(5):
|
for _ in range(6):
|
||||||
disp.send(key_event(code="Down"))
|
disp.send(key_event(code="Down"))
|
||||||
disp.send(key_event(code="Enter"))
|
disp.send(key_event(code="Enter"))
|
||||||
text = disp.frame_text(timeout=5.0, contains=".txt")
|
text = disp.frame_text(timeout=5.0, contains=".txt")
|
||||||
@@ -461,6 +461,77 @@ def main():
|
|||||||
disp.send(key_event(code="Esc"))
|
disp.send(key_event(code="Esc"))
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
# ── 4k. Skärmbild via alt+p ──────────────────────────────────────
|
||||||
|
disp.send(key_event(ch="p", modifiers="ALT"))
|
||||||
|
text = disp.frame_text(timeout=8.0, contains="Skärmbild sparad")
|
||||||
|
check("alt+p tar skärmbild och visar bekräftelse", "Skärmbild sparad" in text)
|
||||||
|
disp.send(key_event(code="Enter")) # stäng popupen
|
||||||
|
time.sleep(0.3)
|
||||||
|
shots = sorted(os.listdir(os.path.join(cfgdir, "skärmbilder")))
|
||||||
|
txts = [s for s in shots if s.endswith(".txt")]
|
||||||
|
pngs = [s for s in shots if s.endswith(".png")]
|
||||||
|
check("skärmbild sparar .txt och .png", bool(txts) and bool(pngs),
|
||||||
|
f"filer: {shots}")
|
||||||
|
if txts:
|
||||||
|
t = open(os.path.join(cfgdir, "skärmbilder", txts[0])).read()
|
||||||
|
check("textskärmbilden innehåller panelen", "TUI-WM" in t)
|
||||||
|
if pngs:
|
||||||
|
magic = open(os.path.join(cfgdir, "skärmbilder", pngs[0]), "rb").read(8)
|
||||||
|
check("PNG-skärmbilden har giltig header",
|
||||||
|
magic == b"\x89PNG\r\n\x1a\n", f"magic: {magic!r}")
|
||||||
|
|
||||||
|
# ── 4k2. Skärmbildskatalog via inställningarna ───────────────────
|
||||||
|
disp.send(mouse_event({"Down": "Left"}, gx, 0))
|
||||||
|
disp.send(mouse_event({"Up": "Left"}, gx, 0))
|
||||||
|
disp.frame_text(timeout=5.0, contains="Skärmbilder")
|
||||||
|
for _ in range(3):
|
||||||
|
disp.send(key_event(code="Down"))
|
||||||
|
disp.send(key_event(code="Enter"))
|
||||||
|
for ch in "bilder":
|
||||||
|
disp.send(key_event(ch=ch))
|
||||||
|
disp.send(key_event(code="Enter"))
|
||||||
|
time.sleep(0.5)
|
||||||
|
cfg_now = open(os.path.join(cfgdir, "config.toml")).read()
|
||||||
|
check("skärmbildskatalogen sparas till config",
|
||||||
|
'screenshot_dir = "bilder"' in cfg_now)
|
||||||
|
disp.send(key_event(code="Esc"))
|
||||||
|
time.sleep(0.3)
|
||||||
|
disp.send(key_event(ch="p", modifiers="ALT"))
|
||||||
|
disp.frame_text(timeout=8.0, contains="Skärmbild sparad")
|
||||||
|
disp.send(key_event(code="Enter"))
|
||||||
|
time.sleep(0.3)
|
||||||
|
check("skärmbilden hamnar i den valda katalogen",
|
||||||
|
os.path.isdir(os.path.join(cfgdir, "bilder"))
|
||||||
|
and len(os.listdir(os.path.join(cfgdir, "bilder"))) >= 1)
|
||||||
|
|
||||||
|
# ── 4l. Tangent-fliken i inställningarna ─────────────────────────
|
||||||
|
disp.send(mouse_event({"Down": "Left"}, gx, 0))
|
||||||
|
disp.send(mouse_event({"Up": "Left"}, gx, 0))
|
||||||
|
disp.frame_text(timeout=5.0, contains="Tangenter")
|
||||||
|
for _ in range(7):
|
||||||
|
disp.send(key_event(code="Down"))
|
||||||
|
disp.send(key_event(code="Enter"))
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="Fönsterväxlare")
|
||||||
|
check("tangentvyn listar keybinds",
|
||||||
|
"alt+w" in text and "Fönsterväxlare" in text)
|
||||||
|
# byt tangent på första raden (ctrl+space → alt+z)
|
||||||
|
disp.send(key_event(code="Enter"))
|
||||||
|
disp.frame_text(timeout=5.0, contains="tryck ny tangent")
|
||||||
|
disp.send(key_event(ch="z", modifiers="ALT"))
|
||||||
|
time.sleep(0.5)
|
||||||
|
cfg_now = open(os.path.join(cfgdir, "config.toml")).read()
|
||||||
|
check("ombindning skrivs till config.toml", '"alt+z"' in cfg_now,
|
||||||
|
f"keybind-rader: {[l for l in cfg_now.splitlines() if 'alt+z' in l]!r}")
|
||||||
|
disp.send(key_event(code="Esc"))
|
||||||
|
disp.send(key_event(code="Esc"))
|
||||||
|
time.sleep(0.3)
|
||||||
|
# alt+z ska nu öppna kommandodialogen (var ctrl+space)
|
||||||
|
disp.send(key_event(ch="z", modifiers="ALT"))
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="Tui-run")
|
||||||
|
check("den nya bindningen fungerar direkt", "Tui-run" in text)
|
||||||
|
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