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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Every update opens the native window.** The first launch of each release
switches lich to the native window, falling back to the web one where it
cannot start; switch back in Settings and lich stays on the web window until
the next release. The first time the native window opens, it says it is
experimental and links to a bug report.
- **A lighter status ring in the native window.** The ring around a
session card's icon is drawn thinner, to read at the weight the web
window gives it.
Expand Down
14 changes: 14 additions & 0 deletions internal/system/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,22 @@ const (
WindowWeb = "web"
WindowNative = "native"
WindowKindKey = "window.kind"
// NativeReleaseKey is the release that last put lich on the native window.
NativeReleaseKey = "window.nativeRelease"
)

// devVersion is main.version in a build without a release tag.
const devVersion = "dev"

// SwitchesToNative reports whether a launch of version puts lich on the
// native window, whatever was chosen before: once per release, so every
// update is tried on it while it is experimental, and a user who switched
// back keeps the web window until the next one. A dev build never does, nor
// an install without the native binary, which would only fall back.
func SwitchesToNative(version, lastSwitched string, nativeShipped bool) bool {
return nativeShipped && version != devVersion && version != lastSwitched
}

// NativeHooks are how lich drives the native window (`lich native`), wired by
// main.
type NativeHooks struct {
Expand Down
18 changes: 18 additions & 0 deletions internal/system/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,3 +600,21 @@ func TestNativeWindowThatCannotStartFallsBackToTheWebOne(t *testing.T) {
t.Fatalf("launch failing: err %v, web shown %d; want the web window", err, r.shown)
}
}

func TestSwitchesToNativeOncePerRelease(t *testing.T) {
cases := []struct {
name, version, last string
shipped, want bool
}{
{"first launch of a release", "0.69.0", "", true, true},
{"an update", "0.69.0", "0.68.0", true, true},
{"the same release again", "0.69.0", "0.69.0", true, false},
{"no native binary", "0.69.0", "0.68.0", false, false},
{"a dev build", "dev", "", true, false},
}
for _, c := range cases {
if got := SwitchesToNative(c.version, c.last, c.shipped); got != c.want {
t.Errorf("%s: SwitchesToNative(%q, %q, %v) = %v, want %v", c.name, c.version, c.last, c.shipped, got, c.want)
}
}
}
23 changes: 23 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ func registerServices(db *store.Service, term *terminal.Service, hub *events.Hub
Close: func() { hub.Emit(windowCloseEvent, nil) },
Locate: func() (string, error) { return cli.NativeExecutable(os.Getenv) },
})
switchToNativeOnRelease(db)
sys.SetWindowChoice(func() string { return windowChoice(db) })
dispatcher.Register("system", sys)
// Deleting a session for good takes any prompt parked on it, and the row is
Expand Down Expand Up @@ -951,6 +952,28 @@ func windowChoice(db *store.Service) string {
return kind
}

// switchToNativeOnRelease chooses the native window on the first launch of a
// release (system.SwitchesToNative). Failing to write leaves the choice the
// user had.
func switchToNativeOnRelease(db *store.Service) {
last, err := db.GetSetting(system.NativeReleaseKey, "")
if err != nil {
slog.Warn("read the release that last chose the native window", "err", err)
return
}
_, locateErr := cli.NativeExecutable(os.Getenv)
if !system.SwitchesToNative(version, last, locateErr == nil) {
return
}
if err := db.SetSetting(system.WindowKindKey, "", system.WindowNative); err != nil {
slog.Warn("choose the native window for this release", "err", err)
return
}
if err := db.SetSetting(system.NativeReleaseKey, "", version); err != nil {
slog.Warn("record the release that chose the native window", "err", err)
}
}

// showWindow opens the chosen window where nobody waits on the answer but
// the log: the launch and the tray.
func showWindow(sys *system.Service, from string) {
Expand Down
75 changes: 75 additions & 0 deletions native/cmd/lich-native/experimental_notice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"flag"
"log"
"sync/atomic"

"gioui.org/layout"
"gioui.org/widget"

"github.com/omartelo/lich/native/i18n"
"github.com/omartelo/lich/native/ui"
)

var flagExperimentalNotice = flag.Bool("experimental-notice", false, "open the experimental-window notice at start, for -shot")

// experimentalNoticeSeenKey is set once the notice was closed, a global
// setting: the notice greets the first native window a user opens, not every
// release that switches them to it.
const experimentalNoticeSeenKey = "native.experimentalNoticeSeen"

// experimentalNotice tells a user the native window is experimental and asks
// for an issue on anything that breaks.
type experimentalNotice struct {
open atomic.Bool
dialog ui.DialogState
gotIt, report widget.Clickable
}

// checkExperimentalNotice opens the notice when it was never closed.
func (u *window) checkExperimentalNotice() {
seen, err := u.client.GetSetting(u.ctx, experimentalNoticeSeenKey, "")
if err != nil {
log.Printf("experimental notice: %v", err)
return
}
if seen == "" {
u.beta.open.Store(true)
u.win.Invalidate()
}
}

func (u *window) closeExperimentalNotice() {
u.beta.open.Store(false)
go func() {
u.logErr("experimental notice: record", u.client.SetSetting(u.ctx, experimentalNoticeSeenKey, "", "1"))
}()
}

func (u *window) layoutExperimentalNotice(gtx C, th *ui.Theme) {
n := &u.beta
if n.gotIt.Clicked(gtx) {
u.closeExperimentalNotice()
}
if n.report.Clicked(gtx) {
u.closeExperimentalNotice()
go func() {
d, err := u.client.Diagnostics(u.ctx)
if err != nil {
log.Printf("experimental notice: diagnostics: %v", err)
return
}
u.logErr("experimental notice: open bug report", u.client.OpenExternal(u.ctx, bugReportURL(d)))
}()
}
on := th.On(th.Popover)
gotIt := on.Button(&n.gotIt, i18n.T("shell.experimentalNotice.gotIt"))
gotIt.Variant = ui.VariantGhost
d := th.Dialog(&n.dialog, n.open.Load(), i18n.T("shell.experimentalNotice.title"), i18n.T("shell.experimentalNotice.description"))
d.MaxWidth = optInWidth
d.Actions = []layout.Widget{gotIt.Layout, on.Button(&n.report, i18n.T("shell.experimentalNotice.reportIssue")).Layout}
if d.Layout(gtx) {
u.closeExperimentalNotice()
}
}
6 changes: 6 additions & 0 deletions native/cmd/lich-native/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type window struct {
notifier *notifier
optIn notifyOptIn
gitMissing gitMissing
beta experimentalNotice
setup providerSetup
plugin pluginGate
appUpdate appUpdateGate
Expand Down Expand Up @@ -414,6 +415,10 @@ func (u *window) connect() error {
go u.checkGit(false)
}
go u.settings.startupPatchNotes()
u.beta.open.Store(*flagExperimentalNotice)
if !*flagExperimentalNotice {
go u.checkExperimentalNotice()
}
go u.pollAppUpdate()
return nil
}
Expand Down Expand Up @@ -471,6 +476,7 @@ func (u *window) layout(gtx C) error {
u.layoutClosePrompt(gtx, u.theme)
u.layoutNotifyOptIn(gtx, u.theme)
u.layoutGitMissing(gtx, u.theme)
u.layoutExperimentalNotice(gtx, u.theme)
var edge int
edge, act, contentErr = u.body(gtx, v)
u.sidebar.flight(gtx, u.theme, u.model)
Expand Down
6 changes: 6 additions & 0 deletions native/i18n/locales/en/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"handsOnTime": "Hands-on time",
"handsOnFailed": "Could not read hands-on time."
},
"experimentalNotice": {
"title": "This window is experimental",
"description": "The native window is new and still experimental. If anything breaks or looks wrong, please open an issue and tell us: it is how we find out what to fix.",
"gotIt": "Got it",
"reportIssue": "Report an issue"
},
"gitMissingGate": {
"title": "git is not installed",
"description": "lich reads every branch, diff and worktree through git. Sessions still run without it — the version control surfaces stay empty.",
Expand Down
6 changes: 6 additions & 0 deletions native/i18n/locales/es/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"handsOnTime": "Tiempo activo",
"handsOnFailed": "No se pudo leer el tiempo activo."
},
"experimentalNotice": {
"title": "Esta ventana es experimental",
"description": "La ventana nativa es nueva y todavía experimental. Si algo falla o se ve mal, por favor, abre un issue y cuéntanoslo: así sabemos qué corregir.",
"gotIt": "Entendido",
"reportIssue": "Reportar un problema"
},
"gitMissingGate": {
"title": "git no está instalado",
"description": "lich lee cada rama, diff y worktree a través de git. Las sesiones siguen funcionando sin él, pero las superficies de control de versiones quedan vacías.",
Expand Down
6 changes: 6 additions & 0 deletions native/i18n/locales/ja/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"handsOnTime": "作業時間",
"handsOnFailed": "作業時間を取得できませんでした。"
},
"experimentalNotice": {
"title": "このウィンドウは試験的です",
"description": "ネイティブウィンドウは新しく、まだ試験段階です。何かが壊れたり、おかしく見えたりしたら、ぜひ issue を作成して教えてください。修正すべき点を知る手がかりになります。",
"gotIt": "了解",
"reportIssue": "問題を報告"
},
"gitMissingGate": {
"title": "git がインストールされていません",
"description": "lich はブランチ、差分、ワークツリーをすべて git で読み取ります。git がなくてもセッションは動きますが、バージョン管理の画面は空のままです。",
Expand Down
6 changes: 6 additions & 0 deletions native/i18n/locales/ko/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"handsOnTime": "직접 작업 시간",
"handsOnFailed": "직접 작업 시간을 읽을 수 없습니다."
},
"experimentalNotice": {
"title": "이 창은 실험적입니다",
"description": "네이티브 창은 새로 나온 실험적 기능입니다. 문제가 생기거나 이상하게 보이면 꼭 이슈를 열어 알려 주세요. 무엇을 고쳐야 할지 알 수 있는 방법입니다.",
"gotIt": "확인",
"reportIssue": "문제 보고"
},
"gitMissingGate": {
"title": "git이 설치되어 있지 않습니다",
"description": "lich는 모든 브랜치, diff, worktree를 git으로 읽습니다. git이 없어도 세션은 실행되지만, 버전 관리 화면은 비어 있습니다.",
Expand Down
6 changes: 6 additions & 0 deletions native/i18n/locales/pt-br/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"handsOnTime": "Tempo de interação",
"handsOnFailed": "Não foi possível ler o tempo de interação."
},
"experimentalNotice": {
"title": "Esta janela é experimental",
"description": "A janela nativa é nova e ainda experimental. Se algo quebrar ou parecer errado, por favor, abra uma issue e conte pra gente: é assim que descobrimos o que corrigir.",
"gotIt": "Entendi",
"reportIssue": "Reportar um problema"
},
"gitMissingGate": {
"title": "O git não está instalado",
"description": "O lich lê cada branch, diff e worktree pelo git. As sessões continuam funcionando sem ele, mas as telas de controle de versão ficam vazias.",
Expand Down
6 changes: 6 additions & 0 deletions native/i18n/locales/ru/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"handsOnTime": "Время за работой",
"handsOnFailed": "Не удалось прочитать время за работой."
},
"experimentalNotice": {
"title": "Это окно экспериментальное",
"description": "Нативное окно новое и пока экспериментальное. Если что-то сломается или выглядит не так, пожалуйста, создайте issue и расскажите нам: так мы узнаём, что исправить.",
"gotIt": "Понятно",
"reportIssue": "Сообщить о проблеме"
},
"gitMissingGate": {
"title": "git не установлен",
"description": "lich читает все ветки, изменения и worktree через git. Сессии работают и без него, но разделы контроля версий остаются пустыми.",
Expand Down
6 changes: 6 additions & 0 deletions native/i18n/locales/zh-cn/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"handsOnTime": "亲自操作时长",
"handsOnFailed": "无法读取亲自操作时长。"
},
"experimentalNotice": {
"title": "此窗口为实验性功能",
"description": "原生窗口是新功能,仍处于实验阶段。如果遇到任何故障或显示异常,请务必提交 issue 告诉我们:这是我们了解需要修复什么的方式。",
"gotIt": "知道了",
"reportIssue": "报告问题"
},
"gitMissingGate": {
"title": "未安装 git",
"description": "lich 通过 git 读取每个分支、差异和工作树。没有 git 时会话仍可运行,但版本控制相关界面会保持为空。",
Expand Down
Loading