Story 37: Editor Command for Worktrees
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
143
server/src/http/settings.rs
Normal file
143
server/src/http/settings.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::store::StoreOps;
|
||||
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
const EDITOR_COMMAND_KEY: &str = "editor_command";
|
||||
|
||||
#[derive(Tags)]
|
||||
enum SettingsTags {
|
||||
Settings,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct EditorCommandPayload {
|
||||
editor_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Object, Serialize)]
|
||||
struct EditorCommandResponse {
|
||||
editor_command: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SettingsApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[OpenApi(tag = "SettingsTags::Settings")]
|
||||
impl SettingsApi {
|
||||
/// Get the configured editor command (e.g. "zed", "code", "cursor"), or null if not set.
|
||||
#[oai(path = "/settings/editor", method = "get")]
|
||||
async fn get_editor(&self) -> OpenApiResult<Json<EditorCommandResponse>> {
|
||||
let editor_command = self
|
||||
.ctx
|
||||
.store
|
||||
.get(EDITOR_COMMAND_KEY)
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()));
|
||||
Ok(Json(EditorCommandResponse { editor_command }))
|
||||
}
|
||||
|
||||
/// Set the preferred editor command (e.g. "zed", "code", "cursor").
|
||||
/// Pass null or empty string to clear the preference.
|
||||
#[oai(path = "/settings/editor", method = "put")]
|
||||
async fn set_editor(
|
||||
&self,
|
||||
payload: Json<EditorCommandPayload>,
|
||||
) -> OpenApiResult<Json<EditorCommandResponse>> {
|
||||
let editor_command = payload.0.editor_command;
|
||||
let trimmed = editor_command.as_deref().map(str::trim).filter(|s| !s.is_empty());
|
||||
match trimmed {
|
||||
Some(cmd) => {
|
||||
self.ctx.store.set(EDITOR_COMMAND_KEY, json!(cmd));
|
||||
self.ctx.store.save().map_err(bad_request)?;
|
||||
Ok(Json(EditorCommandResponse {
|
||||
editor_command: Some(cmd.to_string()),
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
self.ctx.store.delete(EDITOR_COMMAND_KEY);
|
||||
self.ctx.store.save().map_err(bad_request)?;
|
||||
Ok(Json(EditorCommandResponse {
|
||||
editor_command: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_editor_command_from_store(ctx: &AppContext) -> Option<String> {
|
||||
ctx.store
|
||||
.get(EDITOR_COMMAND_KEY)
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::context::AppContext;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn test_ctx(dir: &TempDir) -> AppContext {
|
||||
AppContext::new_test(dir.path().to_path_buf())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_command_defaults_to_null() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let ctx = test_ctx(&dir);
|
||||
let result = get_editor_command_from_store(&ctx);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_editor_command_persists_in_store() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let ctx = test_ctx(&dir);
|
||||
|
||||
ctx.store.set(EDITOR_COMMAND_KEY, json!("zed"));
|
||||
ctx.store.save().unwrap();
|
||||
|
||||
let result = get_editor_command_from_store(&ctx);
|
||||
assert_eq!(result, Some("zed".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_editor_command_from_store_returns_value() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let ctx = test_ctx(&dir);
|
||||
|
||||
ctx.store.set(EDITOR_COMMAND_KEY, json!("code"));
|
||||
let result = get_editor_command_from_store(&ctx);
|
||||
assert_eq!(result, Some("code".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_editor_command_returns_none() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let ctx = test_ctx(&dir);
|
||||
|
||||
ctx.store.set(EDITOR_COMMAND_KEY, json!("cursor"));
|
||||
ctx.store.delete(EDITOR_COMMAND_KEY);
|
||||
let result = get_editor_command_from_store(&ctx);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_command_survives_reload() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let store_path = dir.path().join(".story_kit_store.json");
|
||||
|
||||
{
|
||||
let ctx = AppContext::new_test(dir.path().to_path_buf());
|
||||
ctx.store.set(EDITOR_COMMAND_KEY, json!("zed"));
|
||||
ctx.store.save().unwrap();
|
||||
}
|
||||
|
||||
// Reload from disk
|
||||
let store2 = crate::store::JsonFileStore::new(store_path).unwrap();
|
||||
let val = store2.get(EDITOR_COMMAND_KEY);
|
||||
assert_eq!(val, Some(json!("zed")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user