oneshim-ui
데스크톱 UI를 담당하는 크레이트. 시스템 트레이, 알림, 메인 윈도우, 테마를 관리합니다.
역할
- 시스템 트레이: 백그라운드 실행, 빠른 메뉴
- 데스크톱 알림: 제안 도착 알림
- 메인 윈도우: 제안 목록, 설정, 타임라인
- 테마: 다크/라이트 모드
디렉토리 구조
oneshim-ui/src/
├── lib.rs # 크레이트 루트
├── tray.rs # SystemTray - 시스템 트레이
├── notifier.rs # DesktopNotifierImpl - 알림
├── theme.rs # Theme - 다크/라이트 테마
└── views/ # UI 뷰 컴포넌트
├── mod.rs
├── main_window.rs # 메인 윈도우
├── suggestion_popup.rs # 제안 팝업
├── context_panel.rs # 컨텍스트 패널
├── status_bar.rs # 상태바
├── timeline_view.rs # 타임라인
└── settings.rs # 설정 화면
주요 컴포넌트
SystemTray (tray.rs)
tray-icon 기반 시스템 트레이:
pub struct SystemTray {
tray_icon: TrayIcon,
menu: Menu,
}
impl SystemTray {
pub fn new() -> Result<Self, CoreError> {
let menu = Menu::new();
menu.append(&MenuItem::with_id("show", "창 열기"))?;
menu.append(&MenuSeparator)?;
menu.append(&MenuItem::with_id("status", "상태: 연결됨"))?;
menu.append(&MenuSeparator)?;
menu.append(&MenuItem::with_id("settings", "설정..."))?;
menu.append(&MenuItem::with_id("quit", "종료"))?;
let icon = Self::load_icon()?;
let tray_icon = TrayIcon::new(icon, Some("ONESHIM"), Some(&menu))?;
Ok(Self { tray_icon, menu })
}
pub fn set_status(&mut self, status: ConnectionStatus) {
let text = match status {
ConnectionStatus::Connected => "상태: 연결됨 ✅",
ConnectionStatus::Disconnected => "상태: 연결 끊김 ❌",
ConnectionStatus::Reconnecting => "상태: 재연결 중... 🔄",
};
// 메뉴 아이템 업데이트
}
pub fn show_indicator(&mut self, has_suggestions: bool) {
// 새 제안이 있을 때 아이콘에 배지 표시
}
}
트레이 메뉴:
| 항목 | 동작 |
|---|---|
| 창 열기 | 메인 윈도우 표시 |
| 상태 | 연결 상태 표시 (읽기 전용) |
| 설정... | 설정 윈도우 열기 |
| 종료 | 앱 종료 |
DesktopNotifierImpl (notifier.rs)
notify-rust 기반 데스크톱 알림 (DesktopNotifier 포트 구현):
pub struct DesktopNotifierImpl;
#[async_trait]
impl DesktopNotifier for DesktopNotifierImpl {
async fn notify(&self, suggestion: &Suggestion) -> Result<(), CoreError> {
let title = match suggestion.priority {
Priority::Critical => "🔴 긴급 제안",
Priority::High => "🟠 중요 제안",
Priority::Medium => "💡 제안",
Priority::Low => "📝 참고",
};
let body = Self::truncate(&suggestion.content, 100);
Notification::new()
.summary(title)
.body(&body)
.appname("ONESHIM")
.timeout(Timeout::Milliseconds(5000))
.show()
.map_err(|e| CoreError::Internal(e.to_string()))?;
Ok(())
}
}
Theme (theme.rs)
다크/라이트 테마 정의:
#[derive(Clone)]
pub struct Theme {
pub background: Color,
pub surface: Color,
pub primary: Color,
pub text: Color,
pub text_secondary: Color,
pub border: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
}
impl Theme {
pub fn dark() -> Self {
Self {
background: Color::from_rgb8(30, 30, 30),
surface: Color::from_rgb8(45, 45, 45),
primary: Color::from_rgb8(0, 122, 255),
text: Color::from_rgb8(255, 255, 255),
text_secondary: Color::from_rgb8(170, 170, 170),
border: Color::from_rgb8(60, 60, 60),
success: Color::from_rgb8(48, 209, 88),
warning: Color::from_rgb8(255, 159, 10),
error: Color::from_rgb8(255, 69, 58),
}
}
pub fn light() -> Self {
Self {
background: Color::from_rgb8(255, 255, 255),
surface: Color::from_rgb8(242, 242, 247),
primary: Color::from_rgb8(0, 122, 255),
text: Color::from_rgb8(0, 0, 0),
text_secondary: Color::from_rgb8(142, 142, 147),
border: Color::from_rgb8(209, 209, 214),
success: Color::from_rgb8(52, 199, 89),
warning: Color::from_rgb8(255, 149, 0),
error: Color::from_rgb8(255, 59, 48),
}
}
pub fn system() -> Self {
if Self::is_dark_mode() {
Self::dark()
} else {
Self::light()
}
}
}
Views
MainWindow (main_window.rs)
iced 기반 메인 윈도우:
pub struct MainWindow {
suggestions: Vec<SuggestionView>,
selected: Option<usize>,
theme: Theme,
}
#[derive(Debug, Clone)]
pub enum Message {
SuggestionSelected(usize),
AcceptSuggestion(String),
RejectSuggestion(String),
DismissSuggestion(String),
OpenSettings,
ThemeChanged(bool),
}
impl Application for MainWindow {
type Message = Message;
fn view(&self) -> Element<Message> {
let header = self.view_header();
let suggestion_list = self.view_suggestion_list();
let detail_panel = self.view_detail_panel();
column![
header,
row![
suggestion_list.width(Length::FillPortion(2)),
detail_panel.width(Length::FillPortion(3)),
]
]
.into()
}
}
레이아웃:
┌─────────────────────────────────────────────────────┐
│ Header │
├──────────────────┬──────────────────────────────────┤
│ │ │
│ Suggestion List │ Detail Panel │
│ (2/5 width) │ (3/5 width) │
│ │ │
│ ┌────────────┐ │ ┌─────────────────────────────┐ │
│ │ 🔴 제안 1 │ │ │ 제안 상세 내용 │ │
│ ├────────────┤ │ │ │ │
│ │ 🟠 제안 2 │◀─│ │ 긴 설명 텍스트... │ │
│ ├────────────┤ │ │ │ │
│ │ 💡 제안 3 │ │ │ [수락] [거절] [닫기] │ │
│ └────────────┘ │ └─────────────────────────────┘ │
├──────────────────┴──────────────────────────────────┤
│ Status Bar │
└─────────────────────────────────────────────────────┘
StatusBar (status_bar.rs)
하단 상태바:
pub struct StatusBar {
connection_status: ConnectionStatus,
last_sync: Option<DateTime<Utc>>,
pending_count: usize,
}
impl StatusBar {
pub fn view(&self) -> Element<Message> {
let status_indicator = match self.connection_status {
ConnectionStatus::Connected => text("● 연결됨").color(Color::GREEN),
ConnectionStatus::Disconnected => text("● 연결 끊김").color(Color::RED),
ConnectionStatus::Reconnecting => text("● 재연결 중...").color(Color::YELLOW),
};
let sync_text = self.last_sync
.map(|t| format!("마지막 동기화: {}", t.format("%H:%M:%S")))
.unwrap_or_default();
let pending_text = if self.pending_count > 0 {
format!("대기 중: {}", self.pending_count)
} else {
String::new()
};
row![
status_indicator,
horizontal_space(Length::Fill),
text(&sync_text).size(12),
text(&pending_text).size(12),
]
.padding(8)
.into()
}
}
Settings (settings.rs)
설정 화면:
pub struct SettingsView {
config: AppConfig,
theme_mode: ThemeMode,
}
#[derive(Clone)]
pub enum ThemeMode {
System,
Light,
Dark,
}
impl SettingsView {
pub fn view(&self) -> Element<Message> {
column![
text("설정").size(24),
// 서버 설정
text("서버").size(18),
text_input("서버 URL", &self.config.server.base_url),
// 모니터링 설정
text("모니터링").size(18),
slider(500..=5000, self.config.monitor.poll_interval_ms, |v| {
Message::ConfigChanged("monitor.poll_interval_ms", v)
}),
// 테마 설정
text("테마").size(18),
pick_list(&[ThemeMode::System, ThemeMode::Light, ThemeMode::Dark],
Some(self.theme_mode.clone()),
Message::ThemeModeChanged),
// 자동 업데이트
text("업데이트").size(18),
checkbox("자동 업데이트 사용", self.config.update.enabled,
Message::AutoUpdateToggled),
]
.padding(16)
.into()
}
}
플랫폼 지원
| 기능 | macOS | Windows | Linux |
|---|---|---|---|
| 시스템 트레이 | ✅ | ✅ | ✅ |
| 데스크톱 알림 | ✅ | ✅ | ✅ |
| 다크 모드 감지 | ✅ | ✅ | ⚠️ (DE 의존) |
| 메인 윈도우 | ✅ | ✅ | ✅ |
의존성
| 크레이트 | 용도 |
|---|---|
iced | GUI 프레임워크 |
tray-icon | 시스템 트레이 |
notify-rust | 데스크톱 알림 |
oneshim-suggestion | 제안 프레젠터 |
관련 문서:
- 클라이언트 개요
- oneshim-suggestion - 제안 데이터
- oneshim-core - DesktopNotifier 포트