10 Commits

Author SHA1 Message Date
Uber Veng
829b6403b7 Fixed emojy in text inputs, added server switch hint 2026-05-29 14:12:57 +07:00
Uber Veng
2d9b214ff4 Fix first startup on macOS 3 2026-05-29 13:24:27 +07:00
Uber Veng
80d2e7fd71 Fix first startup on macOS 2 2026-05-29 13:17:30 +07:00
Uber Veng
03e1311e90 update install script 5 2026-05-29 13:03:35 +07:00
Uber Veng
a93f3ad9f6 Fix first startup on macOS 2026-05-29 12:58:15 +07:00
Uber Veng
b0407d6519 update install script 4 2026-05-29 12:49:58 +07:00
Uber Veng
c484f2deb8 update install script 3 2026-05-29 11:55:33 +07:00
Uber Veng
958a729880 update install script 2 2026-05-29 11:52:08 +07:00
Uber Veng
8c6ed94850 update install script 2026-05-29 11:35:20 +07:00
Uber Veng
d4ca9e7542 first-run script fix 2026-05-29 05:54:10 +07:00
5 changed files with 71 additions and 29 deletions

16
Cargo.lock generated
View File

@@ -189,6 +189,7 @@ dependencies = [
"crossterm_winapi",
"derive_more",
"document-features",
"filedescriptor",
"mio",
"parking_lot",
"rustix",
@@ -1203,7 +1204,7 @@ dependencies = [
[[package]]
name = "ostiary"
version = "1.1.1"
version = "1.1.6"
dependencies = [
"anyhow",
"base64",
@@ -1222,6 +1223,7 @@ dependencies = [
"tokio",
"tokio-util",
"toml",
"unicode-width 0.1.14",
]
[[package]]
@@ -1585,7 +1587,7 @@ dependencies = [
"thiserror 2.0.18",
"unicode-segmentation",
"unicode-truncate",
"unicode-width",
"unicode-width 0.2.2",
]
[[package]]
@@ -1636,7 +1638,7 @@ dependencies = [
"strum",
"time",
"unicode-segmentation",
"unicode-width",
"unicode-width 0.2.2",
]
[[package]]
@@ -2430,9 +2432,15 @@ checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
dependencies = [
"itertools",
"unicode-segmentation",
"unicode-width",
"unicode-width 0.2.2",
]
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.2"

View File

@@ -1,10 +1,10 @@
[package]
name = "ostiary"
version = "1.1.1"
version = "1.1.6"
edition = "2024"
[dependencies]
crossterm = "0.29"
crossterm = { version = "0.29", features = ["use-dev-tty"] }
ratatui = "0.30"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"], default-features = false }
@@ -21,3 +21,4 @@ libc = "0.2"
ed25519-dalek = { version = "2", features = ["rand_core"] }
rand = "0.8"
base64 = "0.22"
unicode-width = "0.1"

View File

@@ -108,4 +108,8 @@ fi
echo ""
ok "Launching ${BINARY}..."
exec "$INSTALL_DIR/$BINARY"
if { true </dev/tty; } 2>/dev/null; then
"$INSTALL_DIR/$BINARY"
else
ok "Run: $BINARY"
fi

View File

@@ -170,44 +170,58 @@ async fn run_exec(
/// Runs before ratatui starts. Prompts for endpoint URL and name on first launch.
fn prompt_first_endpoint() -> Result<(String, String)> {
use std::io::Write;
use std::io::{BufRead, BufReader, Write};
println!();
println!(" ╔══════════════════════════════════╗");
println!(" ║ Ostiary — первый запуск ║");
println!(" ╚══════════════════════════════════╝");
println!();
println!(" Конфиг будет сохранён в:");
println!(" {}", config::config_path().display());
println!();
// Open /dev/tty read-only for keyboard input so the wizard works even when
// stdin is a pipe (e.g. `curl | bash`). Writing goes through stdout so we
// don't touch the terminal's write-side state before crossterm takes over.
let tty_in = std::fs::File::open("/dev/tty")
.map_err(|e| anyhow::anyhow!(
"Не удалось открыть терминал ({e}). Запустите ostiary напрямую."
))?;
let mut reader = BufReader::new(tty_in);
let mut out = std::io::stdout();
writeln!(out)?;
writeln!(out, " ╔══════════════════════════════════╗")?;
writeln!(out, " ║ Ostiary — первый запуск ║")?;
writeln!(out, " ╚══════════════════════════════════╝")?;
writeln!(out)?;
writeln!(out, " Конфиг будет сохранён в:")?;
writeln!(out, " {}", config::config_path().display())?;
writeln!(out)?;
let name = loop {
print!(" Название эндпоинта: ");
std::io::stdout().flush()?;
write!(out, " Название эндпоинта: ")?;
out.flush()?;
let mut buf = String::new();
std::io::stdin().read_line(&mut buf)?;
if reader.read_line(&mut buf)? == 0 {
anyhow::bail!("Неожиданный конец ввода.");
}
let val = buf.trim().to_string();
if val.is_empty() {
println!(" Название не может быть пустым. Попробуйте ещё раз.\n");
writeln!(out, " Название не может быть пустым. Попробуйте ещё раз.\n")?;
continue;
}
break val;
};
let url = loop {
print!(" URL эндпоинта: ");
std::io::stdout().flush()?;
write!(out, " URL эндпоинта: ")?;
out.flush()?;
let mut buf = String::new();
std::io::stdin().read_line(&mut buf)?;
if reader.read_line(&mut buf)? == 0 {
anyhow::bail!("Неожиданный конец ввода.");
}
let val = buf.trim().to_string();
if val.is_empty() {
println!(" URL не может быть пустым. Попробуйте ещё раз.\n");
writeln!(out, " URL не может быть пустым. Попробуйте ещё раз.\n")?;
continue;
}
break val;
};
println!();
writeln!(out)?;
Ok((url, name))
}

View File

@@ -3,6 +3,7 @@ use crate::config::Config;
use crate::executor;
use crate::app::{App, MessageLevel, Popup, UpdatingStatus};
use crate::text_input::TextInput;
use unicode_width::UnicodeWidthChar;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
@@ -102,8 +103,11 @@ fn render_menu(f: &mut Frame, app: &mut App, area: Rect) {
let endpoint_title = app.config.endpoints.iter()
.find(|ep| ep.url == app.config.active_endpoint)
.map(|ep| format!(" {} ", ep.name))
.unwrap_or_else(|| " Меню ".to_string());
.map(|ep| Line::from(vec![
Span::raw(format!(" {} ", ep.name)),
Span::styled(" — [↹] Изменить ", Style::default().fg(Color::DarkGray)),
]))
.unwrap_or_else(|| Line::from(" Меню "));
let list = List::new(list_items).block(
Block::default()
@@ -542,10 +546,11 @@ fn render_popup(f: &mut Frame, popup: &mut Popup, config: &Config) {
// Real terminal cursor in the active field
let af = *active_field;
let cx = (chunks[af].x + 1 + fields[af].cursor as u16)
let cx = (chunks[af].x + 1 + display_cols(&fields[af].buf, fields[af].cursor))
.min(chunks[af].x + chunks[af].width.saturating_sub(2));
f.set_cursor_position((cx, chunks[af].y + 1));
let (title, hint) = if edit_index.is_some() {
(" Редактировать эндпоинт ", " Enter — далее / сохранить ")
} else {
@@ -655,7 +660,8 @@ fn render_command(
),
area,
);
let cursor_x = (area.x + 1 + input.cursor as u16).min(area.x + area.width.saturating_sub(2));
let col = if *secret { input.cursor as u16 } else { display_cols(&input.buf, input.cursor) };
let cursor_x = (area.x + 1 + col).min(area.x + area.width.saturating_sub(2));
f.set_cursor_position((cursor_x, area.y + 1));
}
@@ -814,6 +820,15 @@ fn render_command(
}
}
/// Display-column width of `buf` up to char index `cursor`.
/// Accounts for wide characters (emoji, CJK) that occupy 2 terminal columns.
fn display_cols(buf: &str, cursor: usize) -> u16 {
buf.chars()
.take(cursor)
.map(|c| UnicodeWidthChar::width(c).unwrap_or(0))
.sum::<usize>() as u16
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)