Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,4 @@ jobs:

- name: Lint ML service
working-directory: ml_service
run: |
uv sync
uv run ruff check .
run: uvx ruff check .
3 changes: 0 additions & 3 deletions ml_service/src/ok_claude_ml/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
import logging
import os
import signal
import struct
import sys

from .protocol import (
MSG_PING,
MSG_SYNTHESIZE,
MSG_TRANSCRIBE,
MSG_VAD_FEED,
HEADER_SIZE,
read_frame,
write_error,
write_frame,
Expand Down
2 changes: 0 additions & 2 deletions ml_service/src/ok_claude_ml/tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ def sample_rate(self) -> int:

@staticmethod
def _default_model_path() -> str:
import subprocess
import shutil

# Try to find a piper model in common locations
for path in [
Expand Down
5 changes: 2 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,10 @@ fn force_cleanup(config: &crate::config::Config) {
let pid_path = config.pid_path();

// Try to kill the daemon process from the PID file
if let Ok(pid_str) = std::fs::read_to_string(&pid_path) {
if let Ok(pid) = pid_str.trim().parse::<i32>() {
if let Ok(pid_str) = std::fs::read_to_string(&pid_path)
&& let Ok(pid) = pid_str.trim().parse::<i32>() {
unsafe { libc::kill(pid, libc::SIGTERM); }
}
}

// Kill any lingering ok-claude-ml processes
let _ = std::process::Command::new("pkill")
Expand Down
59 changes: 9 additions & 50 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct Config {
pub hotkey: HotkeyConfig,
pub audio: AudioConfig,
Expand Down Expand Up @@ -38,13 +39,15 @@ pub struct SttConfig {

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct TtsConfig {
pub model: Option<String>,
pub voice: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct DaemonConfig {
pub socket_path: Option<String>,
pub pid_file: Option<String>,
Expand Down Expand Up @@ -104,50 +107,31 @@ impl Config {
/// GUI-specific configuration (user-authored).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct GuiConfig {
/// Preferred editor command. Falls back to $VISUAL, then $EDITOR, then "code".
pub editor: Option<String>,
}

impl Default for GuiConfig {
fn default() -> Self {
Self { editor: None }
}
}

impl GuiConfig {
/// Resolve the editor command: config > $VISUAL > $EDITOR > "zed"
pub fn resolve_editor(&self) -> String {
if let Some(ref ed) = self.editor {
return ed.clone();
}
if let Ok(v) = std::env::var("VISUAL") {
if !v.is_empty() {
if let Ok(v) = std::env::var("VISUAL")
&& !v.is_empty() {
return v;
}
}
if let Ok(v) = std::env::var("EDITOR") {
if !v.is_empty() {
if let Ok(v) = std::env::var("EDITOR")
&& !v.is_empty() {
return v;
}
}
"zed".to_string()
}
}

impl Default for Config {
fn default() -> Self {
Self {
hotkey: HotkeyConfig::default(),
audio: AudioConfig::default(),
stt: SttConfig::default(),
tts: TtsConfig::default(),
daemon: DaemonConfig::default(),
tmux: TmuxConfig::default(),
gui: GuiConfig::default(),
}
}
}

impl Default for HotkeyConfig {
fn default() -> Self {
Expand Down Expand Up @@ -177,24 +161,7 @@ impl Default for SttConfig {
}
}

impl Default for TtsConfig {
fn default() -> Self {
Self {
model: None,
voice: None,
}
}
}

impl Default for DaemonConfig {
fn default() -> Self {
Self {
socket_path: None,
pid_file: None,
log_file: None,
}
}
}

impl Default for TmuxConfig {
fn default() -> Self {
Expand Down Expand Up @@ -222,21 +189,13 @@ pub struct GuiState {

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct PersistedSession {
pub name: String,
pub cwd: Option<PathBuf>,
pub shell_mode: bool,
}

impl Default for PersistedSession {
fn default() -> Self {
Self {
name: String::new(),
cwd: None,
shell_mode: false,
}
}
}

impl AppState {
pub fn state_path() -> PathBuf {
Expand Down
50 changes: 20 additions & 30 deletions src/gui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,12 @@ impl ClaudioApp {
if !text.is_empty() {
self.last_transcription = Some(text.clone());
}
if let Some(session) = self.sessions.iter().find(|s| s.id == session_id) {
if !text.is_empty() {
if let Some(session) = self.sessions.iter().find(|s| s.id == session_id)
&& !text.is_empty() {
// Write text to terminal without auto-submitting.
// User reviews and presses Enter manually.
session.write_raw(&text);
}
}
cx.notify();
}
DaemonEvent::SessionCreated {
Expand Down Expand Up @@ -261,12 +260,11 @@ impl ClaudioApp {
return;
}
self.needs_focus_sync = false;
if let Some(ref id) = self.focused_session_id {
if let Some(session) = self.sessions.iter().find(|s| &s.id == id) {
if let Some(ref id) = self.focused_session_id
&& let Some(session) = self.sessions.iter().find(|s| &s.id == id) {
let handle = session.terminal_view.read(cx).focus_handle().clone();
window.focus(&handle);
}
}
}

pub fn focus_session_by_id(&mut self, id: &str, cx: &mut Context<Self>) {
Expand Down Expand Up @@ -453,13 +451,12 @@ impl ClaudioApp {
let config = crate::config::Config::load().ok();
if let Some(ref config) = config {
let pid_path = config.pid_path();
if let Ok(pid_str) = std::fs::read_to_string(&pid_path) {
if let Ok(pid) = pid_str.trim().parse::<i32>() {
if let Ok(pid_str) = std::fs::read_to_string(&pid_path)
&& let Ok(pid) = pid_str.trim().parse::<i32>() {
unsafe {
libc::kill(pid, libc::SIGTERM);
}
}
}
let _ = std::process::Command::new("pkill")
.args(["-f", "ok-claude-ml"])
.status();
Expand All @@ -480,16 +477,15 @@ impl ClaudioApp {
if let Some(parent) = log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(log_file) = std::fs::File::create(&log_path) {
if let Ok(log_file2) = log_file.try_clone() {
if let Ok(log_file) = std::fs::File::create(&log_path)
&& let Ok(log_file2) = log_file.try_clone() {
let _ = std::process::Command::new(exe)
.args(["start", "--foreground"])
.stdout(std::process::Stdio::from(log_file2))
.stderr(std::process::Stdio::from(log_file))
.stdin(std::process::Stdio::null())
.spawn();
}
}
}

// Wait for daemon to come up
Expand Down Expand Up @@ -620,13 +616,12 @@ impl ClaudioApp {
}

fn toggle_autopilot(&mut self, _: &ToggleAutopilot, _window: &mut Window, cx: &mut Context<Self>) {
if let Some(ref id) = self.focused_session_id {
if let Some(session) = self.sessions.iter().find(|s| &s.id == id) {
if let Some(ref id) = self.focused_session_id
&& let Some(session) = self.sessions.iter().find(|s| &s.id == id) {
let prev = session.autopilot.load(Ordering::Relaxed);
session.autopilot.store(!prev, Ordering::Relaxed);
cx.notify();
}
}
}

fn stop_speech(&mut self, _: &StopSpeech, _window: &mut Window, _cx: &mut Context<Self>) {
Expand All @@ -650,8 +645,8 @@ impl ClaudioApp {
None
};

if let Some(raw_text) = raw_text {
if !raw_text.trim().is_empty() {
if let Some(raw_text) = raw_text
&& !raw_text.trim().is_empty() {
// Kill any ongoing speech
let _ = std::process::Command::new("pkill").arg("-f").arg("pw-play.*/tmp/claudio_tts").output();

Expand Down Expand Up @@ -711,7 +706,6 @@ impl ClaudioApp {
}
});
}
}
}

fn default_cwd(&self) -> PathBuf {
Expand Down Expand Up @@ -760,11 +754,10 @@ impl ClaudioApp {
}

pub fn inject_text_to_focused(&self, text: &str) {
if let Some(ref id) = self.focused_session_id {
if let Some(session) = self.sessions.iter().find(|s| &s.id == id) {
if let Some(ref id) = self.focused_session_id
&& let Some(session) = self.sessions.iter().find(|s| &s.id == id) {
session.write_raw(text);
}
}
}

pub fn open_in_editor(&self, dir: &Path) {
Expand Down Expand Up @@ -831,12 +824,11 @@ impl ClaudioApp {
cx.notify();
}
_ => {
if let Some(ch) = &ev.keystroke.key_char {
if !ev.keystroke.modifiers.control && !ev.keystroke.modifiers.alt {
if let Some(ch) = &ev.keystroke.key_char
&& !ev.keystroke.modifiers.control && !ev.keystroke.modifiers.alt {
self.worktree_name_input.push_str(ch);
cx.notify();
}
}
}
}
return;
Expand All @@ -851,12 +843,11 @@ impl ClaudioApp {
cx.notify();
}
_ => {
if let Some(ch) = &ev.keystroke.key_char {
if !ev.keystroke.modifiers.control && !ev.keystroke.modifiers.alt {
if let Some(ch) = &ev.keystroke.key_char
&& !ev.keystroke.modifiers.control && !ev.keystroke.modifiers.alt {
self.rename_input.push_str(ch);
cx.notify();
}
}
}
}
return;
Expand All @@ -873,12 +864,11 @@ impl ClaudioApp {
cx.notify();
}
_ => {
if let Some(ch) = &ev.keystroke.key_char {
if !ev.keystroke.modifiers.control && !ev.keystroke.modifiers.alt {
if let Some(ch) = &ev.keystroke.key_char
&& !ev.keystroke.modifiers.control && !ev.keystroke.modifiers.alt {
self.file_tree.search_query.push_str(ch);
cx.notify();
}
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/gui/file_tree.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(clippy::ptr_arg)]

use std::collections::HashSet;
use std::path::PathBuf;

Expand Down
5 changes: 2 additions & 3 deletions src/gui/ipc_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,10 @@ fn run_subscription_inner(socket_path: &Path, tx: &flume::Sender<DaemonEvent>) -
continue;
}

if let Ok(event) = serde_json::from_str::<DaemonEvent>(trimmed) {
if tx.send(event).is_err() {
if let Ok(event) = serde_json::from_str::<DaemonEvent>(trimmed)
&& tx.send(event).is_err() {
break; // GUI closed
}
}
}

Ok(())
Expand Down
24 changes: 23 additions & 1 deletion src/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ mod theme;
use std::path::Path;

use anyhow::Result;
use gpui;
use gpui::AppContext;

use self::app::ClaudioApp;

pub fn run(socket_path: &Path) -> Result<()> {
let socket = socket_path.to_path_buf();

force_x11_on_gnome();

let app = gpui::Application::new();
app.run(move |cx: &mut gpui::App| {
actions::register(cx);
Expand Down Expand Up @@ -49,3 +50,24 @@ pub fn run(socket_path: &Path) -> Result<()> {

Ok(())
}

// GNOME/Mutter refuses xdg-decoration server-side mode and GPUI does not draw
// its own CSD, leaving the window chromeless. Routing through XWayland gets
// SSD from mutter-x11-frames. Other Wayland compositors honor SSD natively.
fn force_x11_on_gnome() {
if std::env::var_os("CLAUDIO_FORCE_WAYLAND").is_some() {
return;
}
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
let on_gnome = desktop.split(':').any(|s| s.eq_ignore_ascii_case("GNOME"));
let on_wayland = std::env::var_os("WAYLAND_DISPLAY")
.map(|v| !v.is_empty())
.unwrap_or(false);
let has_x11 = std::env::var_os("DISPLAY")
.map(|v| !v.is_empty())
.unwrap_or(false);
if on_gnome && on_wayland && has_x11 {
// SAFETY: called before gpui (and therefore any threads) start.
unsafe { std::env::remove_var("WAYLAND_DISPLAY") };
}
}
Loading
Loading