Wide-glyph damage expansion, screenshot dir in settings, install command on releases
- SafeBackend now tracks wide cells and explicitly overwrites the right half when a wide glyph is replaced by a narrow one — the case ratatui's diff cannot see (both buffers hold a space there) and the likely remaining source of ghost glyphs - Settings dialog: new 'Skärmbilder' row edits screenshot_dir (saved to config.toml, applied immediately) - Release workflow writes an install one-liner (arch-detecting curl) into the release body and updates it on every build - Integration suite: 50/50 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
rid=$(curl -s "$API/repos/$REPO/releases/tags/$TAG" \
|
||||||
-H "Authorization: token $TOKEN" | jq -r '.id')
|
-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" \
|
||||||
|
|||||||
34
src/app.rs
34
src/app.rs
@@ -1361,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..=7).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) {
|
||||||
@@ -1926,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;
|
||||||
@@ -1955,7 +1955,7 @@ 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 = 7;
|
const ROWS: usize = 8;
|
||||||
// Tangentvyn är öppen?
|
// Tangentvyn är öppen?
|
||||||
let keys_state = match self.settings_content(id) {
|
let keys_state = match self.settings_content(id) {
|
||||||
Some(WindowContent::Settings { keys, .. }) => *keys,
|
Some(WindowContent::Settings { keys, .. }) => *keys,
|
||||||
@@ -2068,6 +2068,19 @@ 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();
|
||||||
|
let row = match self.settings_content(id) {
|
||||||
|
Some(WindowContent::Settings { selected, .. }) => *selected,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
if row == 3 {
|
||||||
|
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 =
|
self.config.default_editor =
|
||||||
if val.is_empty() { None } else { Some(val) };
|
if val.is_empty() { None } else { Some(val) };
|
||||||
let _ = Config::save_top_level_str(
|
let _ = Config::save_top_level_str(
|
||||||
@@ -2076,6 +2089,7 @@ impl App {
|
|||||||
self.config.default_editor.as_deref(),
|
self.config.default_editor.as_deref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
*editing = None;
|
*editing = None;
|
||||||
}
|
}
|
||||||
@@ -2135,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 {
|
||||||
@@ -2142,14 +2157,19 @@ 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 == 6 {
|
if row == 7 {
|
||||||
// Tangenter: öppna keybind-vyn
|
// Tangenter: öppna keybind-vyn
|
||||||
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
if let Some(WindowContent::Settings { keys, .. }) = self.settings_content(id) {
|
||||||
*keys = Some((0, false));
|
*keys = Some((0, false));
|
||||||
@@ -2190,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -647,19 +647,22 @@ 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()),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -396,9 +396,9 @@ 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)
|
||||||
@@ -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")
|
||||||
@@ -480,11 +480,35 @@ def main():
|
|||||||
check("PNG-skärmbilden har giltig header",
|
check("PNG-skärmbilden har giltig header",
|
||||||
magic == b"\x89PNG\r\n\x1a\n", f"magic: {magic!r}")
|
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 ─────────────────────────
|
# ── 4l. Tangent-fliken i inställningarna ─────────────────────────
|
||||||
disp.send(mouse_event({"Down": "Left"}, gx, 0))
|
disp.send(mouse_event({"Down": "Left"}, gx, 0))
|
||||||
disp.send(mouse_event({"Up": "Left"}, gx, 0))
|
disp.send(mouse_event({"Up": "Left"}, gx, 0))
|
||||||
disp.frame_text(timeout=5.0, contains="Tangenter")
|
disp.frame_text(timeout=5.0, contains="Tangenter")
|
||||||
for _ in range(6):
|
for _ in range(7):
|
||||||
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="Fönsterväxlare")
|
text = disp.frame_text(timeout=5.0, contains="Fönsterväxlare")
|
||||||
|
|||||||
Reference in New Issue
Block a user