This commit is contained in:
암냥 2026-07-03 00:36:40 +09:00
commit ba26116797
No known key found for this signature in database
67 changed files with 18730 additions and 0 deletions

7
src-tauri/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5307
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

26
src-tauri/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "githikari"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "githikari_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-dialog = { version = "2.7.1", default-features = false, features = ["xdg-portal"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
portable-pty = "0.9.0"

3
src-tauri/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View file

@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-open",
"opener:default",
"opener:allow-open-path",
"opener:allow-reveal-item-in-dir"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

913
src-tauri/src/lib.rs Normal file
View file

@ -0,0 +1,913 @@
use portable_pty::{native_pty_system, ChildKiller, CommandBuilder, MasterPty, PtySize};
use serde::Serialize;
use std::{
collections::HashMap,
env,
io::{Read, Write},
path::{Path, PathBuf},
process::Command,
sync::{Arc, Mutex},
thread,
};
use tauri::{AppHandle, Emitter, State};
struct TerminalSession {
master: Box<dyn MasterPty + Send>,
writer: Arc<Mutex<Box<dyn Write + Send>>>,
killer: Box<dyn ChildKiller + Send + Sync>,
}
#[derive(Default)]
struct TerminalState {
sessions: Arc<Mutex<HashMap<String, TerminalSession>>>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct TerminalOutput {
id: String,
data: String,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct TerminalExit {
id: String,
code: u32,
signal: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitStatusEntry {
path: String,
status: String,
index_status: String,
worktree_status: String,
old_path: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitStatusSummary {
staged: usize,
unstaged: usize,
untracked: usize,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitRecentCommit {
hash: String,
subject: String,
author: String,
relative_time: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitRepositoryStatus {
root_path: String,
branch: String,
head: Option<String>,
ahead: usize,
behind: usize,
entries: Vec<GitStatusEntry>,
summary: GitStatusSummary,
recent_commits: Vec<GitRecentCommit>,
}
const PRIMARY_DEVICE_ATTRIBUTES_RESPONSE: &[u8] = b"\x1b[?1;2c";
const TERMINAL_QUERIES_TO_ANSWER: [&[u8]; 2] = [b"\x1b[c", b"\x1b[0c"];
#[derive(Default)]
struct TerminalOutputFilter {
pending: Vec<u8>,
}
impl TerminalOutputFilter {
fn push(&mut self, input: &[u8]) -> (Vec<u8>, usize) {
let mut source = Vec::with_capacity(self.pending.len() + input.len());
source.extend_from_slice(&self.pending);
source.extend_from_slice(input);
self.pending.clear();
let mut output = Vec::with_capacity(source.len());
let mut response_count = 0;
let mut index = 0;
while index < source.len() {
if let Some(query) = TERMINAL_QUERIES_TO_ANSWER
.iter()
.find(|query| source[index..].starts_with(query))
{
response_count += 1;
index += query.len();
continue;
}
if TERMINAL_QUERIES_TO_ANSWER
.iter()
.any(|query| query.starts_with(&source[index..]))
{
self.pending.extend_from_slice(&source[index..]);
break;
}
output.push(source[index]);
index += 1;
}
(output, response_count)
}
}
fn terminal_size(cols: u16, rows: u16) -> PtySize {
PtySize {
cols,
rows,
pixel_width: 0,
pixel_height: 0,
}
}
fn default_shell() -> String {
env::var("SHELL")
.ok()
.filter(|shell| !shell.trim().is_empty())
.unwrap_or_else(|| "/bin/sh".to_string())
}
#[tauri::command]
fn spawn_terminal(
app: AppHandle,
state: State<'_, TerminalState>,
id: String,
cols: u16,
rows: u16,
cwd: Option<String>,
shell: Option<String>,
) -> Result<(), String> {
kill_terminal(state.clone(), id.clone()).ok();
let pty_system = native_pty_system();
let pair = pty_system
.openpty(terminal_size(cols, rows))
.map_err(|error| error.to_string())?;
let mut command = CommandBuilder::new(shell.unwrap_or_else(default_shell));
command.env("TERM", "xterm-256color");
command.env("COLORTERM", "truecolor");
if let Some(cwd) = cwd.filter(|cwd| !cwd.trim().is_empty()) {
command.cwd(cwd);
}
let mut child = pair
.slave
.spawn_command(command)
.map_err(|error| error.to_string())?;
let mut reader = pair
.master
.try_clone_reader()
.map_err(|error| error.to_string())?;
let writer = Arc::new(Mutex::new(
pair.master
.take_writer()
.map_err(|error| error.to_string())?,
));
let killer = child.clone_killer();
let output_writer = writer.clone();
{
let mut sessions = state.sessions.lock().map_err(|error| error.to_string())?;
sessions.insert(
id.clone(),
TerminalSession {
master: pair.master,
writer,
killer,
},
);
}
let output_app = app.clone();
let output_id = id.clone();
thread::spawn(move || {
let mut buffer = [0_u8; 8192];
let mut output_filter = TerminalOutputFilter::default();
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(size) => {
let (output, response_count) = output_filter.push(&buffer[..size]);
if response_count > 0 {
if let Ok(mut writer) = output_writer.lock() {
for _ in 0..response_count {
let _ = writer.write_all(PRIMARY_DEVICE_ATTRIBUTES_RESPONSE);
}
let _ = writer.flush();
}
}
if !output.is_empty() {
let data = String::from_utf8_lossy(&output).into_owned();
let _ = output_app.emit(
"terminal-output",
TerminalOutput {
id: output_id.clone(),
data,
},
);
}
}
Err(_) => break,
}
}
});
let exit_app = app.clone();
let exit_id = id.clone();
let sessions = state.sessions.clone();
thread::spawn(move || {
if let Ok(status) = child.wait() {
if let Ok(mut sessions) = sessions.lock() {
sessions.remove(&exit_id);
}
let _ = exit_app.emit(
"terminal-exit",
TerminalExit {
id: exit_id,
code: status.exit_code(),
signal: status.signal().map(ToString::to_string),
},
);
}
});
Ok(())
}
#[tauri::command]
fn write_terminal(state: State<'_, TerminalState>, id: String, data: String) -> Result<(), String> {
let writer = {
let sessions = state.sessions.lock().map_err(|error| error.to_string())?;
sessions
.get(&id)
.map(|session| session.writer.clone())
.ok_or_else(|| format!("terminal session not found: {id}"))?
};
let mut writer = writer.lock().map_err(|error| error.to_string())?;
writer
.write_all(data.as_bytes())
.and_then(|_| writer.flush())
.map_err(|error| error.to_string())
}
#[tauri::command]
fn resize_terminal(
state: State<'_, TerminalState>,
id: String,
cols: u16,
rows: u16,
) -> Result<(), String> {
let sessions = state.sessions.lock().map_err(|error| error.to_string())?;
let session = sessions
.get(&id)
.ok_or_else(|| format!("terminal session not found: {id}"))?;
session
.master
.resize(terminal_size(cols, rows))
.map_err(|error| error.to_string())
}
#[tauri::command]
fn kill_terminal(state: State<'_, TerminalState>, id: String) -> Result<(), String> {
let session = {
let mut sessions = state.sessions.lock().map_err(|error| error.to_string())?;
sessions.remove(&id)
};
if let Some(mut session) = session {
session.killer.kill().map_err(|error| error.to_string())?;
}
Ok(())
}
fn run_git(repo_path: &str, args: &[&str]) -> Result<Vec<u8>, String> {
let output = Command::new("git")
.arg("-C")
.arg(repo_path)
.args(args)
.output()
.map_err(|error| format!("failed to run git: {error}"))?;
if output.status.success() {
return Ok(output.stdout);
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(if stderr.is_empty() {
format!("git exited with status {}", output.status)
} else {
stderr
})
}
fn run_git_allowing_diff(repo_path: &str, args: &[&str]) -> Result<Vec<u8>, String> {
let output = Command::new("git")
.arg("-C")
.arg(repo_path)
.args(args)
.output()
.map_err(|error| format!("failed to run git: {error}"))?;
if output.status.success() || output.status.code() == Some(1) {
return Ok(output.stdout);
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(if stderr.is_empty() {
format!("git exited with status {}", output.status)
} else {
stderr
})
}
fn git_root(repo_path: &str) -> Result<String, String> {
let output = run_git(repo_path, &["rev-parse", "--show-toplevel"])?;
let root_path = String::from_utf8(output)
.map_err(|error| error.to_string())?
.trim()
.to_string();
if root_path.is_empty() {
return Err("selected folder is not a git repository".to_string());
}
Ok(root_path)
}
fn resolve_repo_file_path(root_path: &str, file_path: &str) -> Result<PathBuf, String> {
let trimmed_path = file_path.trim();
if trimmed_path.is_empty() {
return Ok(PathBuf::from(root_path));
}
if Path::new(trimmed_path).is_absolute() || trimmed_path.contains("..") {
return Err("file path must stay inside the repository".to_string());
}
Ok(Path::new(root_path).join(trimmed_path))
}
fn git_branch(root_path: &str) -> Result<String, String> {
let output = run_git(root_path, &["branch", "--show-current"])?;
let branch = String::from_utf8(output)
.map_err(|error| error.to_string())?
.trim()
.to_string();
if !branch.is_empty() {
return Ok(branch);
}
let head = git_head(root_path).unwrap_or_else(|| "unborn".to_string());
Ok(format!("detached @ {head}"))
}
fn git_head(root_path: &str) -> Option<String> {
let output = run_git(root_path, &["rev-parse", "--short", "HEAD"]).ok()?;
let head = String::from_utf8(output).ok()?.trim().to_string();
if head.is_empty() {
None
} else {
Some(head)
}
}
fn git_has_head(root_path: &str) -> bool {
run_git(root_path, &["rev-parse", "--verify", "HEAD"]).is_ok()
}
fn git_ahead_behind(root_path: &str) -> (usize, usize) {
let output = match run_git(
root_path,
&["rev-list", "--left-right", "--count", "@{upstream}...HEAD"],
) {
Ok(output) => output,
Err(_) => return (0, 0),
};
let text = String::from_utf8_lossy(&output);
let mut parts = text.split_whitespace();
let behind = parts
.next()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
let ahead = parts
.next()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
(ahead, behind)
}
fn git_recent_commits(root_path: &str) -> Vec<GitRecentCommit> {
let output = match run_git(
root_path,
&[
"log",
"-8",
"--date=relative",
"--pretty=format:%h%x1f%s%x1f%an%x1f%cr%x1e",
],
) {
Ok(output) => output,
Err(_) => return Vec::new(),
};
String::from_utf8_lossy(&output)
.split('\x1e')
.filter_map(|record| {
let mut fields = record.trim_matches('\n').split('\x1f');
Some(GitRecentCommit {
hash: fields.next()?.to_string(),
subject: fields.next()?.to_string(),
author: fields.next()?.to_string(),
relative_time: fields.next()?.to_string(),
})
})
.collect()
}
fn classify_git_status(index_status: char, worktree_status: char) -> String {
if index_status == '?' && worktree_status == '?' {
return "untracked".to_string();
}
if index_status == '!' && worktree_status == '!' {
return "ignored".to_string();
}
if index_status == 'R' || worktree_status == 'R' {
return "renamed".to_string();
}
if index_status == 'D' || worktree_status == 'D' {
return "deleted".to_string();
}
if index_status == 'A' || worktree_status == 'A' {
return "added".to_string();
}
"modified".to_string()
}
fn parse_porcelain_status(output: &[u8]) -> Vec<GitStatusEntry> {
let records: Vec<&[u8]> = output
.split(|byte| *byte == 0)
.filter(|record| !record.is_empty())
.collect();
let mut entries = Vec::new();
let mut index = 0;
while index < records.len() {
let record = String::from_utf8_lossy(records[index]);
let bytes = record.as_bytes();
if bytes.len() < 4 {
index += 1;
continue;
}
let index_status = bytes[0] as char;
let worktree_status = bytes[1] as char;
let mut path = record[3..].to_string();
let mut old_path = None;
if (index_status == 'R' || index_status == 'C') && index + 1 < records.len() {
old_path = Some(path);
index += 1;
path = String::from_utf8_lossy(records[index]).to_string();
}
entries.push(GitStatusEntry {
path,
status: classify_git_status(index_status, worktree_status),
index_status: index_status.to_string(),
worktree_status: worktree_status.to_string(),
old_path,
});
index += 1;
}
entries
}
fn summarize_status(entries: &[GitStatusEntry]) -> GitStatusSummary {
GitStatusSummary {
staged: entries
.iter()
.filter(|entry| entry.index_status != " " && entry.index_status != "?")
.count(),
unstaged: entries
.iter()
.filter(|entry| entry.worktree_status != " " && entry.worktree_status != "?")
.count(),
untracked: entries
.iter()
.filter(|entry| entry.index_status == "?" && entry.worktree_status == "?")
.count(),
}
}
#[tauri::command]
fn git_repository_status(repo_path: String) -> Result<GitRepositoryStatus, String> {
let root_path = git_root(&repo_path)?;
let status_output = run_git(
&root_path,
&[
"status",
"--porcelain=v1",
"-z",
"--untracked-files=all",
"--renames",
],
)?;
let entries = parse_porcelain_status(&status_output);
let (ahead, behind) = git_ahead_behind(&root_path);
Ok(GitRepositoryStatus {
branch: git_branch(&root_path)?,
head: git_head(&root_path),
ahead,
behind,
summary: summarize_status(&entries),
recent_commits: git_recent_commits(&root_path),
root_path,
entries,
})
}
fn normalized_paths(paths: Option<Vec<String>>) -> Vec<String> {
paths
.unwrap_or_default()
.into_iter()
.map(|path| path.trim().to_string())
.filter(|path| !path.is_empty())
.collect()
}
fn run_git_with_paths(
root_path: &str,
args_before_pathspec: &[&str],
paths: &[String],
) -> Result<(), String> {
let mut command = Command::new("git");
command.arg("-C").arg(root_path).args(args_before_pathspec);
if !paths.is_empty() {
command.arg("--");
for path in paths {
command.arg(path);
}
}
let output = command
.output()
.map_err(|error| format!("failed to run git: {error}"))?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(if stderr.is_empty() {
format!("git exited with status {}", output.status)
} else {
stderr
})
}
#[tauri::command]
fn git_stage_paths(repo_path: String, paths: Option<Vec<String>>) -> Result<(), String> {
let root_path = git_root(&repo_path)?;
let paths = normalized_paths(paths);
if paths.is_empty() {
return run_git_with_paths(&root_path, &["add", "-A"], &paths);
}
run_git_with_paths(&root_path, &["add"], &paths)
}
#[tauri::command]
fn git_unstage_paths(repo_path: String, paths: Option<Vec<String>>) -> Result<(), String> {
let root_path = git_root(&repo_path)?;
let paths = normalized_paths(paths);
run_git_with_paths(&root_path, &["reset", "HEAD"], &paths)
}
#[tauri::command]
fn git_discard_paths(repo_path: String, paths: Vec<String>) -> Result<(), String> {
let root_path = git_root(&repo_path)?;
let paths = normalized_paths(Some(paths));
if paths.is_empty() {
return Err("no paths selected".to_string());
}
run_git_with_paths(&root_path, &["restore", "--staged", "--worktree"], &paths).ok();
run_git_with_paths(&root_path, &["clean", "-fd"], &paths)
}
#[tauri::command]
fn git_commit(repo_path: String, message: String) -> Result<(), String> {
let root_path = git_root(&repo_path)?;
let message = message.trim();
if message.is_empty() {
return Err("commit message is empty".to_string());
}
let output = Command::new("git")
.arg("-C")
.arg(&root_path)
.arg("commit")
.arg("-m")
.arg(message)
.output()
.map_err(|error| format!("failed to run git: {error}"))?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(if stderr.is_empty() {
format!("git exited with status {}", output.status)
} else {
stderr
})
}
#[tauri::command]
fn git_open_path(repo_path: String, file_path: String, reveal: bool) -> Result<(), String> {
let root_path = git_root(&repo_path)?;
let target_path = resolve_repo_file_path(&root_path, &file_path)?;
let open_path = if reveal {
if target_path.is_dir() {
target_path
} else {
target_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(root_path))
}
} else {
target_path
};
Command::new("xdg-open")
.arg(open_path)
.spawn()
.map(|_| ())
.map_err(|error| format!("failed to open path: {error}"))
}
fn list_untracked_paths(root_path: &str, pathspec: Option<&str>) -> Result<Vec<String>, String> {
let output = match pathspec {
Some(pathspec) => run_git(
root_path,
&[
"ls-files",
"--others",
"--exclude-standard",
"-z",
"--",
pathspec,
],
)?,
None => run_git(
root_path,
&["ls-files", "--others", "--exclude-standard", "-z"],
)?,
};
Ok(output
.split(|byte| *byte == 0)
.filter(|record| !record.is_empty())
.map(|record| String::from_utf8_lossy(record).to_string())
.collect())
}
fn append_untracked_diff(root_path: &str, path: &str, output: &mut Vec<u8>) -> Result<(), String> {
let absolute_path = Path::new(root_path).join(path);
let absolute_path = absolute_path
.to_str()
.ok_or_else(|| "file path is not valid UTF-8".to_string())?;
let mut diff = run_git_allowing_diff(
root_path,
&[
"diff",
"--no-index",
"--no-ext-diff",
"--no-color",
"--",
"/dev/null",
absolute_path,
],
)?;
if !output.is_empty() && !output.ends_with(b"\n") {
output.push(b'\n');
}
output.append(&mut diff);
Ok(())
}
fn append_bytes(output: &mut Vec<u8>, mut next: Vec<u8>) {
if next.is_empty() {
return;
}
if !output.is_empty() && !output.ends_with(b"\n") {
output.push(b'\n');
}
output.append(&mut next);
}
#[tauri::command]
fn git_diff_file(repo_path: String, file_path: Option<String>) -> Result<String, String> {
let root_path = git_root(&repo_path)?;
let normalized_path = file_path
.as_deref()
.map(str::trim)
.filter(|path| !path.is_empty());
let untracked_paths = list_untracked_paths(&root_path, normalized_path)?;
let has_head = git_has_head(&root_path);
let mut diff_output = Vec::new();
match (has_head, normalized_path) {
(_, Some(path)) if untracked_paths.iter().any(|untracked| untracked == path) => {}
(true, Some(path)) => append_bytes(
&mut diff_output,
run_git_allowing_diff(
&root_path,
&[
"diff",
"--no-ext-diff",
"--no-color",
"--find-renames",
"HEAD",
"--",
path,
],
)?,
),
(true, None) => append_bytes(
&mut diff_output,
run_git_allowing_diff(
&root_path,
&[
"diff",
"--no-ext-diff",
"--no-color",
"--find-renames",
"HEAD",
],
)?,
),
(false, Some(path)) => {
append_bytes(
&mut diff_output,
run_git_allowing_diff(
&root_path,
&[
"diff",
"--cached",
"--no-ext-diff",
"--no-color",
"--",
path,
],
)?,
);
append_bytes(
&mut diff_output,
run_git_allowing_diff(
&root_path,
&["diff", "--no-ext-diff", "--no-color", "--", path],
)?,
);
}
(false, None) => {
append_bytes(
&mut diff_output,
run_git_allowing_diff(
&root_path,
&["diff", "--cached", "--no-ext-diff", "--no-color"],
)?,
);
append_bytes(
&mut diff_output,
run_git_allowing_diff(&root_path, &["diff", "--no-ext-diff", "--no-color"])?,
);
}
}
for untracked_path in untracked_paths {
append_untracked_diff(&root_path, &untracked_path, &mut diff_output)?;
}
String::from_utf8(diff_output).map_err(|error| error.to_string())
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(TerminalState::default())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
greet,
spawn_terminal,
write_terminal,
resize_terminal,
kill_terminal,
git_repository_status,
git_diff_file,
git_stage_paths,
git_unstage_paths,
git_discard_paths,
git_commit,
git_open_path
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(test)]
mod tests {
use super::TerminalOutputFilter;
#[test]
fn terminal_output_filter_answers_primary_device_attributes() {
let mut filter = TerminalOutputFilter::default();
let (output, response_count) = filter.push(b"hello\x1b[cworld");
assert_eq!(output, b"helloworld");
assert_eq!(response_count, 1);
}
#[test]
fn terminal_output_filter_handles_split_primary_device_attributes() {
let mut filter = TerminalOutputFilter::default();
let (first_output, first_response_count) = filter.push(b"hello\x1b[");
let (second_output, second_response_count) = filter.push(b"cworld");
assert_eq!(first_output, b"hello");
assert_eq!(first_response_count, 0);
assert_eq!(second_output, b"world");
assert_eq!(second_response_count, 1);
}
#[test]
fn terminal_output_filter_preserves_non_query_escape_sequences() {
let mut filter = TerminalOutputFilter::default();
let (first_output, first_response_count) = filter.push(b"\x1b[");
let (second_output, second_response_count) = filter.push(b"31mred");
assert!(first_output.is_empty());
assert_eq!(first_response_count, 0);
assert_eq!(second_output, b"\x1b[31mred");
assert_eq!(second_response_count, 0);
}
}

6
src-tauri/src/main.rs Normal file
View file

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
githikari_lib::run()
}

35
src-tauri/tauri.conf.json Normal file
View file

@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "githikari",
"version": "0.1.0",
"identifier": "ng.imnya.githikari",
"build": {
"beforeDevCommand": "bun run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "bun run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "wow",
"width": 1600,
"height": 900
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}